我有几十个文本,我想lastTextBaseline放置它们的前导基线 ( ) 位于特定坐标处。position只能设置中心。例如:
import SwiftUI
import PlaygroundSupport
struct Location: Identifiable {
let id = UUID()
let point: CGPoint
let angle: Double
let string: String
}
let locations = [
Location(point: CGPoint(x: 54.48386479999999, y: 296.4645408), angle: -0.6605166885682314, string: "Y"),
Location(point: CGPoint(x: 74.99159120000002, y: 281.6336352), angle: -0.589411952788817, string: "o"),
]
struct ContentView: View {
var body: some View {
ZStack {
ForEach(locations) { run in
Text(verbatim: run.string)
.font(.system(size: 48))
.border(Color.green)
.rotationEffect(.radians(run.angle))
.position(run.point)
Circle() // Added to show where `position` is
.frame(maxWidth: 5)
.foregroundColor(.red)
.position(run.point)
}
}
}
}
PlaygroundPage.current.setLiveView(ContentView())
Run Code Online (Sandbox Code Playgroud)
这将定位字符串,使其中心位于所需点(标记为红色圆圈):
我想对此进行调整,使领先基线位于此红点处。在此示例中,正确的布局会将字形向上和向右移动。
我曾尝试将.topLeading对齐添加到 ZStack,然后使用offset而不是position. 这将让我根据最顶端的角落对齐,但这不是我想要布局的角落。例如:
ZStack(alignment: .topLeading) { // add alignment
Rectangle().foregroundColor(.clear) // to force ZStack to full size
ForEach(locations) { run in
Text(verbatim: run.string)
.font(.system(size: 48))
.border(Color.green)
.rotationEffect(.radians(run.angle), anchor: .topLeading) // rotate on top-leading
.offset(x: run.point.x, y: run.point.y)
}
}
Run Code Online (Sandbox Code Playgroud)
我还尝试更改文本的“顶部”对齐指南:
.alignmentGuide(.top) { d in d[.lastTextBaseline]}
Run Code Online (Sandbox Code Playgroud)
这会移动红点而不是文本,所以我不相信这是在正确的道路上。
我正在考虑尝试调整位置本身以考虑文本的大小(我可以使用 Core Text 预测),但我希望避免计算大量额外的边界框。
因此,据我所知,目前还不能以这种方式使用对齐指南。希望这很快就会到来,但与此同时,我们可以做一些填充和覆盖技巧来获得所需的效果。
CTFont来初始化我的Font实例并检索指标。displayScale环境值(和派生pixelLength值)在操场甚至预览中默认设置不正确。因此,如果您想要具有代表性的布局 (FB7280058),则必须在这些环境中手动设置。我们将结合许多 SwiftUI 功能来获得我们想要的结果。具体来说,转换、叠加和GeometryReader视图。
首先,我们将字形的基线与视图的基线对齐。如果我们有字体的度量,我们可以使用字体的“下降”将我们的字形向下移动一点,使其与基线齐平——我们可以使用padding视图修饰符来帮助我们解决这个问题。
接下来,我们将用重复视图覆盖我们的字形视图。为什么?因为在叠加层中,我们能够获取下方视图的确切指标。事实上,我们的叠加层将是用户看到的唯一视图,原始视图将仅用于其指标。
几个简单的变换将我们的叠加放置在我们想要的位置,然后我们将隐藏位于下面的视图以完成效果。
首先,我们需要一些额外的属性来帮助我们的计算。在适当的项目中,您可以将其组织成视图修改器或类似的,但为了简洁起见,我们将它们添加到我们现有的视图中。
@Environment(\.pixelLength) var pixelLength: CGFloat
@Environment(\.displayScale) var displayScale: CGFloat
Run Code Online (Sandbox Code Playgroud)
我们还需要一个初始化为 a 的字体,CTFont以便我们可以获取它的指标:
let baseFont: CTFont = {
let desc = CTFontDescriptorCreateWithNameAndSize("SFProDisplay-Medium" as CFString, 0)
return CTFontCreateWithFontDescriptor(desc, 48, nil)
}()
Run Code Online (Sandbox Code Playgroud)
然后进行一些计算。这会为文本视图计算一些 EdgeInsets,这将具有将文本视图的基线移动到封闭填充视图的底部边缘的效果:
var textPadding: EdgeInsets {
let baselineShift = (displayScale * baseFont.descent).rounded(.down) / displayScale
let baselineOffsetInsets = EdgeInsets(top: baselineShift, leading: 0, bottom: -baselineShift, trailing: 0)
return baselineOffsetInsets
}
Run Code Online (Sandbox Code Playgroud)
我们还将向 CTFont 添加几个辅助属性:
extension CTFont {
var ascent: CGFloat { CTFontGetAscent(self) }
var descent: CGFloat { CTFontGetDescent(self) }
}
Run Code Online (Sandbox Code Playgroud)
最后,我们创建了一个新的辅助函数来生成使用CTFont我们上面定义的Text 视图:
private func glyphView(for text: String) -> some View {
Text(verbatim: text)
.font(Font(baseFont))
}
Run Code Online (Sandbox Code Playgroud)
glyphView(_:)在我们的主body调用中采用我们的这一步很简单,我们采用了glyphView(_:)上面定义的辅助函数:
var body: some View {
ZStack {
ForEach(locations) { run in
self.glyphView(for: run.string)
.border(Color.green, width: self.pixelLength)
.position(run.point)
Circle() // Added to show where `position` is
.frame(maxWidth: 5)
.foregroundColor(.red)
.position(run.point)
}
}
}
Run Code Online (Sandbox Code Playgroud)
这让我们在这里:
接下来,我们移动文本视图的基线,使其与封闭的填充视图的底部齐平。这只是向我们glyphView(_:)使用我们上面定义的填充计算的新函数添加填充修饰符的情况。
private func glyphView(for text: String) -> some View {
Text(verbatim: text)
.font(Font(baseFont))
.padding(textPadding) // Added padding modifier
}
Run Code Online (Sandbox Code Playgroud)
请注意字形现在如何与其封闭视图的底部齐平。
我们需要获得字形的度量,以便我们能够准确地放置它。但是,在我们列出我们的观点之前,我们无法获得这些指标。解决此问题的一种方法是复制我们的视图并使用一个视图作为隐藏的指标来源,然后使用我们收集的指标呈现我们定位的重复视图。
我们可以使用叠加修饰符和GeometryReader视图来做到这一点。我们还将添加一个紫色边框并使我们的覆盖文本为蓝色,以区别于上一步。
self.glyphView(for: run.string)
.border(Color.green, width: self.pixelLength)
.overlay(GeometryReader { geometry in
self.glyphView(for: run.string)
.foregroundColor(.blue)
.border(Color.purple, width: self.pixelLength)
})
.position(run.point)
Run Code Online (Sandbox Code Playgroud)
利用我们现在可以使用的指标,我们可以将叠加层向上和向右移动,以便字形视图的左下角位于我们的红色定位点上。
self.glyphView(for: run.string)
.border(Color.green, width: self.pixelLength)
.overlay(GeometryReader { geometry in
self.glyphView(for: run.string)
.foregroundColor(.blue)
.border(Color.purple, width: self.pixelLength)
.transformEffect(.init(translationX: geometry.size.width / 2, y: -geometry.size.height / 2))
})
.position(run.point)
Run Code Online (Sandbox Code Playgroud)
现在我们的视图就位,我们终于可以旋转了。
self.glyphView(for: run.string)
.border(Color.green, width: self.pixelLength)
.overlay(GeometryReader { geometry in
self.glyphView(for: run.string)
.foregroundColor(.blue)
.border(Color.purple, width: self.pixelLength)
.transformEffect(.init(translationX: geometry.size.width / 2, y: -geometry.size.height / 2))
.rotationEffect(.radians(run.angle))
})
.position(run.point)
Run Code Online (Sandbox Code Playgroud)
最后一步是隐藏我们的源视图并将我们的覆盖字形设置为其适当的颜色:
self.glyphView(for: run.string)
.border(Color.green, width: self.pixelLength)
.hidden()
.overlay(GeometryReader { geometry in
self.glyphView(for: run.string)
.foregroundColor(.black)
.border(Color.purple, width: self.pixelLength)
.transformEffect(.init(translationX: geometry.size.width / 2, y: -geometry.size.height / 2))
.rotationEffect(.radians(run.angle))
})
.position(run.point)
Run Code Online (Sandbox Code Playgroud)
//: A Cocoa based Playground to present user interface
import SwiftUI
import PlaygroundSupport
struct Location: Identifiable {
let id = UUID()
let point: CGPoint
let angle: Double
let string: String
}
let locations = [
Location(point: CGPoint(x: 54.48386479999999, y: 296.4645408), angle: -0.6605166885682314, string: "Y"),
Location(point: CGPoint(x: 74.99159120000002, y: 281.6336352), angle: -0.589411952788817, string: "o"),
]
struct ContentView: View {
@Environment(\.pixelLength) var pixelLength: CGFloat
@Environment(\.displayScale) var displayScale: CGFloat
let baseFont: CTFont = {
let desc = CTFontDescriptorCreateWithNameAndSize("SFProDisplay-Medium" as CFString, 0)
return CTFontCreateWithFontDescriptor(desc, 48, nil)
}()
var textPadding: EdgeInsets {
let baselineShift = (displayScale * baseFont.descent).rounded(.down) / displayScale
let baselineOffsetInsets = EdgeInsets(top: baselineShift, leading: 0, bottom: -baselineShift, trailing: 0)
return baselineOffsetInsets
}
var body: some View {
ZStack {
ForEach(locations) { run in
self.glyphView(for: run.string)
.border(Color.green, width: self.pixelLength)
.hidden()
.overlay(GeometryReader { geometry in
self.glyphView(for: run.string)
.foregroundColor(.black)
.border(Color.purple, width: self.pixelLength)
.transformEffect(.init(translationX: geometry.size.width / 2, y: -geometry.size.height / 2))
.rotationEffect(.radians(run.angle))
})
.position(run.point)
Circle() // Added to show where `position` is
.frame(maxWidth: 5)
.foregroundColor(.red)
.position(run.point)
}
}
}
private func glyphView(for text: String) -> some View {
Text(verbatim: text)
.font(Font(baseFont))
.padding(textPadding)
}
}
private extension CTFont {
var ascent: CGFloat { CTFontGetAscent(self) }
var descent: CGFloat { CTFontGetDescent(self) }
}
PlaygroundPage.current.setLiveView(
ContentView()
.environment(\.displayScale, NSScreen.main?.backingScaleFactor ?? 1.0)
.frame(width: 640, height: 480)
.background(Color.white)
)
Run Code Online (Sandbox Code Playgroud)
就是这样。它并不完美,但直到 SwiftUI 为我们提供一个 API 允许我们使用对齐锚来锚定我们的转换,它可能会让我们过得去!
| 归档时间: |
|
| 查看次数: |
2031 次 |
| 最近记录: |