在移动开发中,有时会遇到需要垂直排版文字的场景,特别是在一些中文设计中,竖排文本可以营造出一种独特的视觉效果。这篇文章将详细讲解如何实现一个支持竖排中文和英文的 SwiftUI
组件,并提供详细的代码实现和思路。
在竖排文本的排版中,需要考虑以下几个要点:
为了解决这些问题,我们需要定义一个配置类 VerticalTextConfig
,然后在 VerticalText
组件中实现竖排文本的具体逻辑。
我们首先定义一个 VerticalTextConfig
结构体,用于配置竖排文本的各种属性,例如字符间距、对齐方式、是否为英文等。
struct VerticalTextConfig {
/// 字间距
var itemSpacing: CGFloat = 0.0
/// 列间距
var lineSpacing: CGFloat = 10.0
/// 左右对齐方式, 默认居中,仅支持leading, center, trailing
var horizontalAlignment: String = "center"
/// 上下对齐方式, 默认上对齐,仅支持top, center, bottom
var verticalAlignment: String = "top"
/// 旋转角度,正直为顺时针旋转,负值为逆时针旋转。
var rotationDegress: CGFloat = 0.0
/// 是否是英文
var isEnglish: Bool = false
// 中文场景下的左右对齐方式
fileprivate var chineseHorizontalAlignment: Alignment {
switch horizontalAlignment {
case "leading": return .leading
case "center": return .center
case "trailing": return .trailing
default: return .center
}
}
// 中文场景下的上下对齐方式
fileprivate var chineseVerticalAlignment: VerticalAlignment {
switch verticalAlignment {
case "top": return .top
case "center": return .center
case "bottom": return .bottom
default: return .top
}
}
// 英文场景下的左右对齐方式
fileprivate var englishHorizontalAlignment: Alignment {
switch horizontalAlignment {
case "leading": return .bottom
case "center": return .center
case "trailing": return .top
default: return .top
}
}
// 英文场景下的上下对齐方式
fileprivate var englishVerticalAlignment: TextAlignment {
switch verticalAlignment {
case "top": return .leading
case "center": return .center
case "bottom": return .trailing
default: return .leading
}
}
}
该结构体定义了一系列属性,包括字符间距、列间距、对齐方式等。中文和英文的对齐方式不同,因此分别定义了中文和英文的对齐处理,公开的只有horizontalAlignment
和verticalAlignment
两个属性,而内部则会将其转换使用内部定义的fileprivate
修饰的属性。
接下来,我们创建 VerticalText
组件,负责渲染竖排文本。
struct VerticalText: View {
/// 要显示的文字
let text: String
/// 字体名字
@Binding var fontName: String
/// 字体大小
@Binding var fontSize: CGFloat
/// 文字颜色
@Binding var fontColor: String
/// 配置文件,包含样式参数
let config: VerticalTextConfig
init(text: String, fontName: Binding<String>, fontSize: Binding<CGFloat>, fontColor: Binding<String>, config: VerticalTextConfig) {
self.text = text
_fontName = fontName
_fontSize = fontSize
_fontColor = fontColor
self.config = config
}
var body: some View {
GeometryReader { geometry in
if config.isEnglish {
Text(text)
.font(.custom(fontName, size: fontSize))
.multilineTextAlignment(config.englishVerticalAlignment)
.frame(width: geometry.size.height, height: geometry.size.width, alignment: config.englishHorizontalAlignment)
.background(Color.red)
.rotationEffect(.degrees(90 + config.rotationDegress))
.offset(x: -(geometry.size.height - geometry.size.width) / 2, y: (geometry.size.height - geometry.size.width) / 2)
} else {
// 计算单个字符的尺寸
let charSize = calculateCharacterSize(fontName: fontName, fontSize: fontSize)
// 计算单个字符的宽度
let charWidth = charSize.width
// 计算单个字符的高度
let charHeight = charSize.height
// 计算一列可以容纳的字符数
let charactersPerColumn = max(1, Int(geometry.size.height / charHeight))
// 将文本拆分成列
let columns = splitTextIntoColumns(text: text, charactersPerColumn: charactersPerColumn)
// 计算一列的总宽度,包括列间距
let columnTotalWidth = charWidth + config.lineSpacing
// 计算可以显示的最大列数
let maxColumns = Int((geometry.size.width + config.lineSpacing) / columnTotalWidth)
// 计算列数是否超界,如果超界,去掉多余的列,然后将可显示的列最后的字符打点。
let newColumns = getNewColumnsIfNeeded(columns: columns, maxColumns: maxColumns, charactersPerColumn: charactersPerColumn)
HStack(alignment: config.chineseVerticalAlignment, spacing: config.lineSpacing) {
ForEach(newColumns, id: \.self) { column in
VStack(spacing: config.itemSpacing) {
ForEach(column, id: \.self) { character in
Text(character)
.font(.custom(fontName, fixedSize: fontSize))
}
}
}
}
.frame(maxWidth: .infinity, alignment: config.chineseHorizontalAlignment)
.rotationEffect(.degrees(config.rotationDegress))
}
}
}
}
这个组件中,我们使用了 GeometryReader
来计算容器的大小,并根据文本内容以及配置参数来决定如何布局中文或英文竖排文本。主要分为两种情况:
Text
组件,通过设置多行对齐、旋转角度实现竖排显示。HStack
和 VStack
组合来实现垂直排列。为了实现中文文本的拆分和字符大小计算,我们还需要在VerticalText
组件中添加一些辅助函数:
// 计算列数是否超界,如果超界,去掉多余的列,然后将可显示的列最后的字符打点。
func getNewColumnsIfNeeded(columns: [[String]], maxColumns: Int, charactersPerColumn: Int) -> [[String]] {
// 如果列数超过最大可显示列数,则进行截断处理
if columns.count > maxColumns {
var fixedColumns = Array(columns.suffix(maxColumns))
// 在最后一列的末尾添加 "..."
if let lastColumn = fixedColumns.first {
let newLastColumn = truncateColumnWithEllipsis(column: lastColumn, maxCharacters: charactersPerColumn)
fixedColumns[0] = newLastColumn
}
return fixedColumns
}
return columns
}
// 将文本拆分成列
func splitTextIntoColumns(text: String, charactersPerColumn: Int) -> [[String]] {
var columns: [[String]] = []
var currentColumn: [String] = []
for (index, character) in text.map({ String($0) }).enumerated() {
currentColumn.append(character)
if (index + 1) % charactersPerColumn == 0 || index == text.count - 1 {
columns.append(currentColumn)
currentColumn = []
}
}
return columns.reversed() // 逆序以实现从右到左的排列
}
// 计算单个字符的尺寸
func calculateCharacterSize(fontName: String, fontSize: CGFloat) -> CGSize {
let uiFont = UIFont(name: fontName, size: fontSize)
let label = UILabel()
label.font = uiFont
label.text = "测" // 用一个中文字符来测量高度
label.sizeToFit()
return label.frame.size
}
// 在最后一列的末尾添加 "..."
func truncateColumnWithEllipsis(column: [String], maxCharacters: Int) -> [String] {
var truncatedColumn = column
if truncatedColumn.count >= maxCharacters {
truncatedColumn.removeLast()
truncatedColumn.append("︙")
}
return truncatedColumn
}
这些函数分别负责将文本拆分为多列、计算字符大小,以及在文本过长时对列进行截断。
struct VerticalTextDemo: View {
@State private var fontName: String = "HelveticaNeue"
@State private var fontSize: CGFloat = 16.0
@State private var isEnglish: Bool = false
@State private var fontColor: String = "#000000"
var body: some View {
VStack {
VStack {
Text("Helvetica-Bold")
.font(.custom("Helvetica-Bold", size: 22))
.onTapGesture {
fontName = "Helvetica-Bold"
fontSize = 22
}
Text("HelveticaNeue-Light")
.font(.custom("HelveticaNeue-Light", size: 16))
.onTapGesture {
fontName = "HelveticaNeue-Light"
fontSize = 16
}
Text("KohinoorBangla-Semibold")
.font(.custom("KohinoorBangla-Semibold", size: 30))
.onTapGesture {
fontName = "KohinoorBangla-Semibold"
fontSize = 30
}
.multilineTextAlignment(/*@START_MENU_TOKEN@*/.leading/*@END_MENU_TOKEN@*/)
}
if isEnglish {
VerticalText(text: "Hello world, this is a beautifull world, i love it!", fontName: $fontName, fontSize: $fontSize, fontColor: $fontColor, config: VerticalTextConfig(horizontalAlignment: "center", verticalAlignment: "center", isEnglish: isEnglish))
.frame(width: 100, height: 240) // 设置宽度和高度
.background(Color.yellow)
} else {
VerticalText(text: "这是一个实现竖排文本的例子,不知道好不好用!", fontName: $fontName, fontSize: $fontSize, fontColor: $fontColor, config: VerticalTextConfig(horizontalAlignment: "trailing", verticalAlignment: "top", isEnglish: isEnglish))
.frame(width: 100, height: 240) // 设置宽度和高度
.background(Color.yellow)
.rotationEffect(.degrees(45))
}
}
}
}
示例效果如下:
通过本文介绍的 VerticalText
组件,你可以轻松实现中文和英文的竖排文本排版效果。该组件不仅支持字符间距、对齐方式等个性化配置,还能处理文本过长的情况,让你的竖排文本显示更加灵活。
最后,希望能够帮助到有需要的朋友,如果觉得有帮助,还望点个赞,添加个关注,笔者也会不断地努力,写出更多更好用的文章。
import SwiftUI
struct VerticalTextDemo: View {
@State private var fontName: String = "HelveticaNeue"
@State private var fontSize: CGFloat = 16.0
@State private var isEnglish: Bool = true
@State private var fontColor: String = "#000000"
var body: some View {
VStack {
VStack {
Text("Helvetica-Bold")
.font(.custom("Helvetica-Bold", size: 22))
.onTapGesture {
fontName = "Helvetica-Bold"
fontSize = 22
}
Text("HelveticaNeue-Light")
.font(.custom("HelveticaNeue-Light", size: 16))
.onTapGesture {
fontName = "HelveticaNeue-Light"
fontSize = 16
}
Text("KohinoorBangla-Semibold")
.font(.custom("KohinoorBangla-Semibold", size: 30))
.onTapGesture {
fontName = "KohinoorBangla-Semibold"
fontSize = 30
}
.multilineTextAlignment(/*@START_MENU_TOKEN@*/.leading/*@END_MENU_TOKEN@*/)
}
if isEnglish {
VerticalText(text: "Hello world, this is a beautifull world, i love it!", fontName: $fontName, fontSize: $fontSize, fontColor: $fontColor, config: VerticalTextConfig(horizontalAlignment: "center", verticalAlignment: "center", isEnglish: isEnglish))
.frame(width: 100, height: 240) // 设置宽度和高度
.background(Color.yellow)
} else {
VerticalText(text: "这是一个实现竖排文本的例子,不知道好不好用!", fontName: $fontName, fontSize: $fontSize, fontColor: $fontColor, config: VerticalTextConfig(horizontalAlignment: "trailing", verticalAlignment: "top", isEnglish: isEnglish))
.frame(width: 100, height: 240) // 设置宽度和高度
.background(Color.yellow)
.rotationEffect(.degrees(45))
}
}
}
}
#Preview {
VerticalTextDemo()
}
struct VerticalTextConfig {
/// 字间距
var itemSpacing: CGFloat = 0.0
/// 列间距
var lineSpacing: CGFloat = 10.0
/// 左右对齐方式, 默认居中,仅支持leading, center, trailing
var horizontalAlignment: String = "center"
/// 上下对齐方式, 默认上对齐,仅支持top, center, bottom
var verticalAlignment: String = "top"
/// 旋转角度,正直为顺时针旋转,负值为逆时针旋转。
var rotationDegress: CGFloat = 0.0
/// 是否是英文
var isEnglish: Bool = false
/// 中文场景使用
fileprivate var chineseHorizontalAlignment: Alignment {
switch horizontalAlignment {
case "leading":
return .leading
case "center":
return .center
case "trailing":
return .trailing
default:
return .center
}
}
/// 中文场景使用
fileprivate var chineseVerticalAlignment: VerticalAlignment {
switch verticalAlignment {
case "top":
return .top
case "center":
return .center
case "bottom":
return .bottom
default:
return .top
}
}
/// 英文场景使用
fileprivate var englishHorizontalAlignment: Alignment {
switch horizontalAlignment {
case "leading":
return .bottom
case "center":
return .center
case "trailing":
return .top
default:
return .top
}
}
/// 英文场景使用
fileprivate var englishVerticalAlignment: TextAlignment {
switch verticalAlignment {
case "top":
return .leading
case "center":
return .center
case "bottom":
return .trailing
default:
return .leading
}
}
}
struct VerticalText: View {
/// 要显示的文字
let text: String
/// 字体名字
@Binding var fontName: String
/// 字体大小
@Binding var fontSize: CGFloat
/// 文字颜色
@Binding var fontColor: String
/// 配置文件,包含样式参数
let config: VerticalTextConfig
init(text: String, fontName: Binding<String>, fontSize: Binding<CGFloat>, fontColor: Binding<String>, config: VerticalTextConfig) {
self.text = text
_fontName = fontName
_fontSize = fontSize
_fontColor = fontColor
self.config = config
}
var body: some View {
GeometryReader { geometry in
if config.isEnglish {
Text(text)
.font(.custom(fontName, size: fontSize))
.multilineTextAlignment(config.englishVerticalAlignment)
.frame(width: geometry.size.height, height: geometry.size.width, alignment: config.englishHorizontalAlignment)
.background(Color.red)
.rotationEffect(.degrees(90 + config.rotationDegress))
.offset(x: -(geometry.size.height - geometry.size.width) / 2, y: (geometry.size.height - geometry.size.width) / 2)
} else {
// 计算单个字符的尺寸
let charSize = calculateCharacterSize(fontName: fontName, fontSize: fontSize)
// 计算单个字符的宽度
let charWidth = charSize.width
// 计算单个字符的高度
let charHeight = charSize.height
// 计算一列可以容纳的字符数
let charactersPerColumn = max(1, Int(geometry.size.height / charHeight))
// 将文本拆分成列
let columns = splitTextIntoColumns(text: text, charactersPerColumn: charactersPerColumn)
// 计算一列的总宽度,包括列间距
let columnTotalWidth = charWidth + config.lineSpacing
// 计算可以显示的最大列数
let maxColumns = Int((geometry.size.width + config.lineSpacing) / columnTotalWidth)
// 计算列数是否超界,如果超界,去掉多余的列,然后将可显示的列最后的字符打点。
let newColumns = getNewColumnsIfNeeded(columns: columns, maxColumns: maxColumns, charactersPerColumn: charactersPerColumn)
HStack(alignment: config.chineseVerticalAlignment, spacing: config.lineSpacing) {
ForEach(newColumns, id: \.self) { column in
VStack(spacing: config.itemSpacing) {
ForEach(column, id: \.self) { character in
Text(character)
.font(.custom(fontName, fixedSize: fontSize))
}
}
}
}
.frame(maxWidth: .infinity, alignment: config.chineseHorizontalAlignment)
.rotationEffect(.degrees(config.rotationDegress))
}
}
}
// 计算列数是否超界,如果超界,去掉多余的列,然后将可显示的列最后的字符打点。
func getNewColumnsIfNeeded(columns: [[String]], maxColumns: Int, charactersPerColumn: Int) -> [[String]] {
// 如果列数超过最大可显示列数,则进行截断处理
if columns.count > maxColumns {
var fixedColumns = Array(columns.suffix(maxColumns))
// 在最后一列的末尾添加 "..."
if let lastColumn = fixedColumns.first {
let newLastColumn = truncateColumnWithEllipsis(column: lastColumn, maxCharacters: charactersPerColumn)
fixedColumns[0] = newLastColumn
}
return fixedColumns
}
return columns
}
// 将文本拆分成列
func splitTextIntoColumns(text: String, charactersPerColumn: Int) -> [[String]] {
var columns: [[String]] = []
var currentColumn: [String] = []
for (index, character) in text.map({ String($0) }).enumerated() {
currentColumn.append(character)
if (index + 1) % charactersPerColumn == 0 || index == text.count - 1 {
columns.append(currentColumn)
currentColumn = []
}
}
return columns.reversed() // 逆序以实现从右到左的排列
}
// 计算单个字符的尺寸
func calculateCharacterSize(fontName: String, fontSize: CGFloat) -> CGSize {
let uiFont = UIFont(name: fontName, size: fontSize)
let label = UILabel()
label.font = uiFont
label.text = "测" // 用一个中文字符来测量高度
label.sizeToFit()
return label.frame.size
}
// 在最后一列的末尾添加 "..."
func truncateColumnWithEllipsis(column: [String], maxCharacters: Int) -> [String] {
var truncatedColumn = column
if truncatedColumn.count >= maxCharacters {
truncatedColumn.removeLast()
truncatedColumn.append("︙")
}
return truncatedColumn
}
}