使用锚点定位视图 [英] Positioning View using anchor point

查看:29
本文介绍了使用锚点定位视图的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有几十个想要定位的文本,以便它们的前导基线 (lastTextBaseline) 位于特定坐标处.position 只能设置中心.例如:

I have several dozen Texts that I would like to position such that their leading baseline (lastTextBaseline) is at a specific coordinate. position can only set the center. For example:

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())

这会定位字符串,使其中心位于所需点(标记为红色圆圈):

This locates the strings such that their center is at the desired point (marked as a red circle):

我想对此进行调整,使领先基线位于此红点处.在此示例中,正确的布局会将字形向上和向右移动.

I would like to adjust this so that the leading baseline is at this red dot. In this example, a correct layout would move the glyphs up and to the right.

我尝试将 .topLeading 对齐添加到 ZStack,然后使用 offset 而不是 position.这将让我根据最顶端的角落对齐,但这不是我想要布局的角落.例如:

I have tried adding .topLeading alignment to the ZStack, and then using offset rather than position. This will let me align based on the top-leading corner, but that's not the corner I want to layout. For example:

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)
     }
}

我还尝试更改文本的顶部"对齐指南:

I've also tried changing the "top" alignment guide for the Texts:

.alignmentGuide(.top) { d in d[.lastTextBaseline]}

这会移动红点而不是文本,所以我不相信这是在正确的道路上.

This moves the red dots rather than the text, so I don't believe this is on the right path.

我正在考虑尝试调整位置本身以考虑文本的大小(我可以使用 Core Text 进行预测),但我希望避免计算大量额外的边界框.

I am considering trying to adjust the locations themselves to take into account the size of the Text (which I can predict using Core Text), but I am hoping to avoid calculating a lot of extra bounding boxes.

推荐答案

所以,据我所知,目前还不能以这种方式使用对齐指南.希望这很快就会到来,但与此同时,我们可以做一些填充和覆盖技巧来获得所需的效果.

So, as far as I can tell, alignment guides can't be used in this way – yet. Hopefully this will be coming soon, but in the meantime we can do a little padding and overlay trickery to get the desired effect.

  • 您需要某种方式来检索字体指标——我正在使用 CTFont 来初始化我的 Font 实例并以这种方式检索指标.
  • 据我所知,Playgrounds 并不总是代表 SwiftUI 布局在设备上的布局方式,并且会出现某些不一致的情况.我已经确定的一个是 displayScale 环境值(以及派生的 pixelLength 值)在操场甚至预览中默认设置不正确.因此,如果您想要具有代表性的布局 (FB7280058),则必须在这些环境中手动设置.
  • You will need to have some way of retrieving the font metrics – I'm using CTFont to initialise my Font instances and retrieving metrics that way.
  • As far as I can tell, Playgrounds aren't always representative of how a SwiftUI layout will be laid out on the device, and certain inconsistencies arise. One that I've identified is that the displayScale environment value (and the derived pixelLength value) is not set correctly by default in playgrounds and even previews. Therefore, you have to set this manually in these environments if you want a representative layout (FB7280058).

我们将结合许多 SwiftUI 功能来获得我们想要的结果.具体来说,转换、叠加和 GeometryReader 视图.

We're going to combine a number of SwiftUI features to get the outcome we want here. Specifically, transforms, overlays and the GeometryReader view.

首先,我们将字形的基线与视图的基线对齐.如果我们有字体的指标,我们可以使用字体的下降"将我们的字形向下移动一点,使其与基线齐平——我们可以使用 padding 视图修饰符来帮助我们解决这个问题.

First, we'll align the baseline of our glyph to the baseline of our view. If we have the font's metrics we can use the font's 'descent' to shift our glyph down a little so it sits flush with the baseline – we can use the padding view modifier to help us with this.

接下来,我们要用一个重复的视图覆盖我们的字形视图.为什么?因为在叠加层中,我们能够获取下方视图的确切指标.事实上,我们的叠加层将是用户看到的唯一视图,原始视图将仅用于其指标.

Next, we're going to overlay our glyph view with a duplicate view. Why? Because within an overlay we're able to grab the exact metrics of the view underneath. In fact, our overlay will be the only view the user sees, the original view will only be utilised for its metrics.

几个简单的变换会将我们的叠加层放置在我们想要的位置,然后我们将隐藏位于下方的视图以完成效果.

A couple of simple transforms will position our overlay where we want it, and we'll then hide the view that sits underneath to complete the effect.

首先,我们需要一些额外的属性来帮助我们进行计算.在适当的项目中,您可以将其组织成一个视图修饰符或类似的东西,但为了简洁起见,我们会将它们添加到我们现有的视图中.

First, we're going to need some additional properties to help with our calculations. In a proper project you could organise this into a view modifier or similar, but for conciseness we'll add them to our existing view.

@Environment(\.pixelLength) var pixelLength: CGFloat
@Environment(\.displayScale) var displayScale: CGFloat

我们还需要将字体初始化为 CTFont 以便我们可以获取其指标:

We'll also need a our font initialised as a CTFont so we can grab its metrics:

let baseFont: CTFont = {
    let desc = CTFontDescriptorCreateWithNameAndSize("SFProDisplay-Medium" as CFString, 0)
    return CTFontCreateWithFontDescriptor(desc, 48, nil)
}()

然后是一些计算.这会为文本视图计算一些 EdgeInsets,这将具有将文本视图的基线移动到封闭填充视图的底部边缘的效果:

Then some calculations. This calculates some EdgeInsets for a text view that will have the effect of moving the text view's baseline to the bottom edge of the enclosing padding view:

var textPadding: EdgeInsets {
    let baselineShift = (displayScale * baseFont.descent).rounded(.down) / displayScale
    let baselineOffsetInsets = EdgeInsets(top: baselineShift, leading: 0, bottom: -baselineShift, trailing: 0)
    return baselineOffsetInsets
}

我们还将向 CTFont 添加几个辅助属性:

We'll also add a couple of helper properties to CTFont:

extension CTFont {
    var ascent: CGFloat { CTFontGetAscent(self) }
    var descent: CGFloat { CTFontGetDescent(self) }
}

最后,我们创建了一个新的辅助函数来生成使用我们上面定义的 CTFont 的 Text 视图:

And finally we create a new helper function to generate our Text views that uses the CTFont we defined above:

private func glyphView(for text: String) -> some View {
    Text(verbatim: text)
        .font(Font(baseFont))
}

第 2 步:在我们的主要 body 调用中采用我们的 glyphView(_:)

这一步很简单,我们采用了上面定义的 glyphView(_:) 辅助函数:

Step 2: Adopt our glyphView(_:) in our main body call

This step is simple and has us adopt the glyphView(_:) helper function we define above:

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)
        }
    }
}

这让我们在这里:

接下来,我们移动文本视图的基线,使其与封闭的填充视图的底部齐平.这只是向我们新的 glyphView(_:) 函数添加填充修饰符的一种情况,该函数利用我们上面定义的填充计算.

Next we shift the baseline of our text view so that it sits flush with the bottom of our enclosing padding view. This is just a case of adding a padding modifier to our new glyphView(_:)function that utilises the padding calculation we define above.

private func glyphView(for text: String) -> some View {
    Text(verbatim: text)
        .font(Font(baseFont))
        .padding(textPadding) // Added padding modifier
}

注意现在字形如何与其封闭视图的底部齐平.

Notice how the glyphs are now sitting flush with the bottom of their enclosing views.

我们需要获取字形的度量,以便我们能够准确地放置它.但是,在我们列出我们的观点之前,我们无法获得这些指标.解决此问题的一种方法是复制我们的视图并使用一个视图作为隐藏的指标来源,然后展示我们使用收集到的指标定位的重复视图.

We need to get the metrics of our glyph so that we are able to accurately place it. However, we can't get those metrics until we've laid out our view. One way around this is to duplicate our view and use one view as a source of metrics that is otherwise hidden, and then present a duplicate view that we position using the metrics we've gathered.

我们可以使用叠加修饰符和 GeometryReader 视图来完成此操作.我们还将添加一个紫色边框并使我们的覆盖文本为蓝色,以区别于上一步.

We can do this with the overlay modifier together with a GeometryReader view. And we'll also add a purple border and make our overlay text blue to differentiate it from the previous step.

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)

利用我们现在可用的指标,我们可以将叠加层向上和向右移动,以便字形视图的左下角位于我们的红色定位点上.

Making use of the metrics we now have available for us to use, we can shift our overlay up and to the right so that the bottom left corner of the glyph view sits on our red positioning spot.

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)

现在我们的视图就位,我们终于可以旋转了.

Now we have our view in position we can finally rotate.

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)

最后一步是隐藏我们的源视图并将我们的叠加字形设置为其正确的颜色:

Last step is to hide our source view and set our overlay glyph to its proper colour:

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)

//: 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)
)

就是这样.它并不完美,但直到 SwiftUI 为我们提供一个 API 允许我们使用对齐锚来锚定我们的变换,它可能会让我们顺利!

And that's it. It's not perfect, but until SwiftUI gives us an API that allows us to use alignment anchors to anchor our transforms, it might get us by!

这篇关于使用锚点定位视图的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆