英文:
Vertically center text over custom shape - SwiftUI
问题
以下是要翻译的内容:
我创建了一个自定义的环形段形状。最终,我想在我的应用程序中将这个形状用作按钮,因此我需要能够将文本放置在它的中间位置。下面的图片显示了环形段(蓝色)和我希望在蓝色段内垂直居中显示的文本(红色)。
[![点击查看图片描述](https://i.stack.imgur.com/ZlWdl.png)](https://i.stack.imgur.com/ZlWdl.png)
以下代码创建了自定义的蓝色形状段:
```swift
struct CurvedButtonData {
enum ButtonLocation {
case top
case right
case bottom
case left
}
/// 按钮的宽度,以角度表示。
let width: Double = 90.0
/// 按钮的起始位置,以角度表示。
fileprivate(set) var start = 0.0
/// 按钮的结束位置,以角度表示。
fileprivate(set) var end = 0.0
init(position: ButtonLocation) {
switch position {
case .top:
start = -135
case .right:
start = -45
case .bottom:
start = 45
case .left:
start = 135
}
end = start + width
}
}
struct CurvedButton: Shape, InsettableShape {
let data: CurvedButtonData
var insetAmount = 0.0
func path(in rect: CGRect) -> Path {
let points = CurvedButtonGeometry(curvedButtonData: data, rect: rect)
var path = Path()
path.addArc(center: points.center, radius: points.innerRadius, startAngle: .degrees(data.start), endAngle: .degrees(data.end), clockwise: false)
path.addArc(center: points.center, radius: points.outerRadius - insetAmount, startAngle: .degrees(data.end), endAngle: .degrees(data.start), clockwise: true)
path.closeSubpath()
return path
}
func inset(by amount: CGFloat) -> CurvedButton {
var button = self
button.insetAmount += amount
return button
}
}
struct CurvedButtonGeometry {
let data: CurvedButtonData
let center: CGPoint
let innerRadius: CGFloat
let outerRadius: CGFloat
init(curvedButtonData: CurvedButtonData, rect: CGRect) {
center = CGPoint(x: rect.midX, y: rect.midY)
let radius = min(rect.width, rect.height) / 4
innerRadius = radius
outerRadius = radius * 2
data = curvedButtonData
}
/// 返回单位空间位置`unitPoint`处楔形中的点的视图位置,其中`p`的X轴围绕楔形弧移动,Y轴从内半径到外半径移动。
subscript(unitPoint: UnitPoint) -> CGPoint {
let radius = lerp(innerRadius, outerRadius, by: unitPoint.y)
let angle = lerp(data.start, data.end, by: Double(unitPoint.x))
return CGPoint(x: center.x + CGFloat(cos(angle)) * radius,
y: center.y + CGFloat(sin(angle)) * radius)
}
/// 通过分数`amount`线性插值从`from`到`to`。
private func lerp<T: BinaryFloatingPoint>(_ fromValue: T, _ toValue: T, by amount: T) -> T {
return fromValue + (toValue - fromValue) * amount
}
}
这是我尝试创建具有自定义形状并将“Click me”文本居中放置在蓝色区域内的代码:
import SwiftUI
struct CircularCVBControl: View {
var body: some View {
Button {
} label: {
ZStack(alignment: .top) {
CurvedButton(data: .init(position: .top))
.fill(.blue)
.overlay(alignment: .top) {
Text("点击我")
.foregroundColor(.red)
}
}
}
.buttonStyle(.plain)
}
}
struct CircularCVBControl_Previews: PreviewProvider {
static var previews: some View {
CircularCVBControl()
}
}
我如何整洁地(而且不使用Text上的补丁)在我的自定义形状中垂直居中文本?我正在寻找一种能够完成任务的自定义对齐指南/逻辑。另外,该形状也可以以其他方向旋转,文本应随之旋转。
英文:
I created a custom shape in the form of a ring segment. I eventually want to use this shape as a button in my app, and therefore I need to be able to position text right in the middle of it. Below picture shows the ring segment (blue) and the text (red) I wish to center vertically inside the blue segment.
The code below creates the custom blue shape segment:
struct CurvedButtonData {
enum ButtonLocation {
case top
case right
case bottom
case left
}
/// The button's width as an angle in degrees.
let width: Double = 90.0
/// The button's start location, as an angle in degrees.
fileprivate(set) var start = 0.0
/// The button's end location, as an angle in degrees.
fileprivate(set) var end = 0.0
init(position: ButtonLocation) {
switch position {
case .top:
start = -135
case .right:
start = -45
case .bottom:
start = 45
case .left:
start = 135
}
end = start + width
}
}
struct CurvedButton: Shape, InsettableShape {
let data: CurvedButtonData
var insetAmount = 0.0
func path(in rect: CGRect) -> Path {
let points = CurvedButtonGeometry(curvedButtonData: data, rect: rect)
var path = Path()
path.addArc(center: points.center, radius: points.innerRadius, startAngle: .degrees(data.start), endAngle: .degrees(data.end), clockwise: false)
path.addArc(center: points.center, radius: points.outerRadius - insetAmount, startAngle: .degrees(data.end), endAngle: .degrees(data.start), clockwise: true)
path.closeSubpath()
return path
}
func inset(by amount: CGFloat) -> CurvedButton {
var button = self
button.insetAmount += amount
return button
}
}
struct CurvedButtonGeometry {
let data: CurvedButtonData
let center: CGPoint
let innerRadius: CGFloat
let outerRadius: CGFloat
init(curvedButtonData: CurvedButtonData, rect: CGRect) {
center = CGPoint(x: rect.midX, y: rect.midY)
let radius = min(rect.width, rect.height) / 4
innerRadius = radius
outerRadius = radius * 2
data = curvedButtonData
}
/// Returns the view location of the point in the wedge at unit-
/// space location `unitPoint`, where the X axis of `p` moves around the
/// wedge arc and the Y axis moves out from the inner to outer
/// radius.
subscript(unitPoint: UnitPoint) -> CGPoint {
let radius = lerp(innerRadius, outerRadius, by: unitPoint.y)
let angle = lerp(data.start, data.end, by: Double(unitPoint.x))
return CGPoint(x: center.x + CGFloat(cos(angle)) * radius,
y: center.y + CGFloat(sin(angle)) * radius)
}
/// Linearly interpolate from `from` to `to` by the fraction `amount`.
private func lerp<T: BinaryFloatingPoint>(_ fromValue: T, _ toValue: T, by amount: T) -> T {
return fromValue + (toValue - fromValue) * amount
}
}
And this is my pityful attempt to create a button with my custom shape and center the "Click me" text inside the blue area:
import SwiftUI
struct CircularCVBControl: View {
var body: some View {
Button {
} label: {
ZStack(alignment: .top) {
CurvedButton(data: .init(position: .top))
.fill(.blue)
.overlay(alignment: .top) {
Text("Click me")
.foregroundColor(.red)
}
}
}
.buttonStyle(.plain)
}
}
struct CircularCVBControl_Previews: PreviewProvider {
static var previews: some View {
CircularCVBControl()
}
}
How can I now neatly (and without using a hacky padding on the Text) center the text vertically in my custom shape? I am looking for some sort of custom alignment guide/logic that does the job. Also, the shape can be rotated in other directions too, and the text should rotate with it.
答案1
得分: 1
以下是您要翻译的部分:
问题是您的曲线未在框架中央呈现。为了解决这个问题,我认为您需要偏移您的中心。
像这样:
center = CGPoint(x: rect.midX, y: rect.midY + innerRadius + (outerRadius - innerRadius)/2)
这仅适用于“.top”位置。其它位置留给您解决
以下是完整的代码:
import SwiftUI
struct CurvedButtonData {
enum ButtonLocation {
case top
case right
case bottom
case left
}
/// The button's width as an angle in degrees.
let width: Double = 90.0
/// The button's start location, as an angle in degrees.
fileprivate(set) var start = 0.0
/// The button's end location, as an angle in degrees.
fileprivate(set) var end = 0.0
init(position: ButtonLocation) {
switch position {
case .top:
start = -135
case .right:
start = -45
case .bottom:
start = 45
case .left:
start = 135
}
end = start + width
}
}
struct CurvedButton: Shape, InsettableShape {
let data: CurvedButtonData
var insetAmount = 0.0
func path(in rect: CGRect) -> Path {
let points = CurvedButtonGeometry(curvedButtonData: data, rect: rect)
var path = Path()
path.addArc(center: points.center, radius: points.innerRadius, startAngle: .degrees(data.start), endAngle: .degrees(data.end), clockwise: false)
path.addArc(center: points.center, radius: points.outerRadius - insetAmount, startAngle: .degrees(data.end), endAngle: .degrees(data.start), clockwise: true)
path.closeSubpath()
return path
}
func inset(by amount: CGFloat) -> CurvedButton {
var button = self
button.insetAmount += amount
return button
}
}
struct CurvedButtonGeometry {
let data: CurvedButtonData
let center: CGPoint
let innerRadius: CGFloat
let outerRadius: CGFloat
init(curvedButtonData: CurvedButtonData, rect: CGRect) {
let radius = min(rect.width, rect.height) / 4
innerRadius = radius
outerRadius = radius * 2
center = CGPoint(x: rect.midX, y: rect.midY + innerRadius + (outerRadius - innerRadius)/2)
data = curvedButtonData
}
/// Returns the view location of the point in the wedge at unit-
/// space location `unitPoint`, where the X axis of `p` moves around the
/// wedge arc and the Y axis moves out from the inner to outer
/// radius.
subscript(unitPoint: UnitPoint) -> CGPoint {
let radius = lerp(innerRadius, outerRadius, by: unitPoint.y)
let angle = lerp(data.start, data.end, by: Double(unitPoint.x))
return CGPoint(x: center.x + CGFloat(cos(angle)) * radius,
y: center.y + CGFloat(sin(angle)) * radius)
}
/// Linearly interpolate from `from` to `to` by the fraction `amount`.
private func lerp<T: BinaryFloatingPoint>(_ fromValue: T, _ toValue: T, by amount: T) -> T {
return fromValue + (toValue - fromValue) * amount
}
}
struct CircularCVBControl: View {
var body: some View {
Button {
} label: {
ZStack() {
CurvedButton(data: .init(position: .top))
.fill(.blue)
Text("Click me")
.foregroundColor(.red)
}
}
.buttonStyle(.plain)
}
}
struct CircularCVBControl_Previews: PreviewProvider {
static var previews: some View {
CircularCVBControl()
}
}
英文:
The problem is that your curve does not render in the middle of the frame. To solve this, I believe you need to offset your center.
Like this:
center = CGPoint(x: rect.midX, y: rect.midY + innerRadius + (outerRadius - innerRadius)/2)
This is a solution only for the '.top' position. I will leave the other positions to you
Here is the full code:
import SwiftUI
struct CurvedButtonData {
enum ButtonLocation {
case top
case right
case bottom
case left
}
/// The button's width as an angle in degrees.
let width: Double = 90.0
/// The button's start location, as an angle in degrees.
fileprivate(set) var start = 0.0
/// The button's end location, as an angle in degrees.
fileprivate(set) var end = 0.0
init(position: ButtonLocation) {
switch position {
case .top:
start = -135
case .right:
start = -45
case .bottom:
start = 45
case .left:
start = 135
}
end = start + width
}
}
struct CurvedButton: Shape, InsettableShape {
let data: CurvedButtonData
var insetAmount = 0.0
func path(in rect: CGRect) -> Path {
let points = CurvedButtonGeometry(curvedButtonData: data, rect: rect)
var path = Path()
path.addArc(center: points.center, radius: points.innerRadius, startAngle: .degrees(data.start), endAngle: .degrees(data.end), clockwise: false)
path.addArc(center: points.center, radius: points.outerRadius - insetAmount, startAngle: .degrees(data.end), endAngle: .degrees(data.start), clockwise: true)
path.closeSubpath()
return path
}
func inset(by amount: CGFloat) -> CurvedButton {
var button = self
button.insetAmount += amount
return button
}
}
struct CurvedButtonGeometry {
let data: CurvedButtonData
let center: CGPoint
let innerRadius: CGFloat
let outerRadius: CGFloat
init(curvedButtonData: CurvedButtonData, rect: CGRect) {
let radius = min(rect.width, rect.height) / 4
innerRadius = radius
outerRadius = radius * 2
center = CGPoint(x: rect.midX, y: rect.midY + innerRadius + (outerRadius - innerRadius)/2)
data = curvedButtonData
}
/// Returns the view location of the point in the wedge at unit-
/// space location `unitPoint`, where the X axis of `p` moves around the
/// wedge arc and the Y axis moves out from the inner to outer
/// radius.
subscript(unitPoint: UnitPoint) -> CGPoint {
let radius = lerp(innerRadius, outerRadius, by: unitPoint.y)
let angle = lerp(data.start, data.end, by: Double(unitPoint.x))
return CGPoint(x: center.x + CGFloat(cos(angle)) * radius,
y: center.y + CGFloat(sin(angle)) * radius)
}
/// Linearly interpolate from `from` to `to` by the fraction `amount`.
private func lerp<T: BinaryFloatingPoint>(_ fromValue: T, _ toValue: T, by amount: T) -> T {
return fromValue + (toValue - fromValue) * amount
}
}
struct CircularCVBControl: View {
var body: some View {
Button {
} label: {
ZStack() {
CurvedButton(data: .init(position: .top))
.fill(.blue)
Text("Click me")
.foregroundColor(.red)
}
}
.buttonStyle(.plain)
}
}
struct CircularCVBControl_Previews: PreviewProvider {
static var previews: some View {
CircularCVBControl()
}
}
答案2
得分: 1
我明白你想将多个曲线按钮组合成一个“圆形”的效果。在这种情况下,你确实需要偏移文本而不是形状。
你可以使用GeometryReader来实现这个效果。我已经为显示在顶部位置的情况下进行了实现,你可以将其他位置添加到getOffset()
方法中。虽然这段代码有些坐标计算不止一次出现,我并不完全满意,但我想演示核心原理而不改变一切。希望这有所帮助。
英文:
I see that you want to combine multiple Curvedbuttons together as one 'circle'. In that case you indeed have to offset the Text instead of the Shape.
You can do it with GeometryReader. I've implemented it for the .top position as shown below. You can add the other positions into the getOffset() method. I am not entirely happy with this code as some of the coordinates are not computed in more than one place. You can/should clean this code a lot but I wanted to demonstrate the core principle without changing everything. Hope this helps.
import SwiftUI
struct CurvedButtonData {
enum ButtonLocation {
case top
case right
case bottom
case left
}
/// The button's width as an angle in degrees.
let width: Double = 90.0
/// The button's start location, as an angle in degrees.
fileprivate(set) var start = 0.0
/// The button's end location, as an angle in degrees.
fileprivate(set) var end = 0.0
init(position: ButtonLocation) {
switch position {
case .top:
start = -135
case .right:
start = -45
case .bottom:
start = 45
case .left:
start = 135
}
end = start + width
}
}
struct CurvedButton: Shape, InsettableShape {
let data: CurvedButtonData
var insetAmount = 0.0
func path(in rect: CGRect) -> Path {
let points = CurvedButtonGeometry(curvedButtonData: data, rect: rect)
var path = Path()
path.addArc(center: points.center, radius: points.innerRadius, startAngle: .degrees(data.start), endAngle: .degrees(data.end), clockwise: false)
path.addArc(center: points.center, radius: points.outerRadius - insetAmount, startAngle: .degrees(data.end), endAngle: .degrees(data.start), clockwise: true)
path.closeSubpath()
return path
}
func inset(by amount: CGFloat) -> CurvedButton {
var button = self
button.insetAmount += amount
return button
}
}
struct CurvedButtonGeometry {
let data: CurvedButtonData
let center: CGPoint
let innerRadius: CGFloat
let outerRadius: CGFloat
init(curvedButtonData: CurvedButtonData, rect: CGRect) {
let radius = min(rect.width, rect.height) / 4
innerRadius = radius
outerRadius = radius * 2
center = CGPoint(x: rect.midX, y: rect.midY)
data = curvedButtonData
}
/// Returns the view location of the point in the wedge at unit-
/// space location `unitPoint`, where the X axis of `p` moves around the
/// wedge arc and the Y axis moves out from the inner to outer
/// radius.
subscript(unitPoint: UnitPoint) -> CGPoint {
let radius = lerp(innerRadius, outerRadius, by: unitPoint.y)
let angle = lerp(data.start, data.end, by: Double(unitPoint.x))
return CGPoint(x: center.x + CGFloat(cos(angle)) * radius,
y: center.y + CGFloat(sin(angle)) * radius)
}
/// Linearly interpolate from `from` to `to` by the fraction `amount`.
private func lerp<T: BinaryFloatingPoint>(_ fromValue: T, _ toValue: T, by amount: T) -> T {
return fromValue + (toValue - fromValue) * amount
}
}
struct CircularCVBControl: View {
var body: some View {
Button {
} label: {
GeometryReader { geo in
CurvedButton(data: .init(position: .top))
.foregroundColor(.blue)
.overlay(
Text("Click me")
.foregroundColor(.red)
.offset(y: getOffset(geo))
)
}
}
.buttonStyle(.plain)
}
func getOffset(_ geo : GeometryProxy) -> CGFloat {
let radius = min(geo.size.width, geo.size.height) / 4
let innerRadius = radius
let outerRadius = radius * 2
let offset = -1 * innerRadius - (outerRadius - innerRadius)/2
return offset
}
}
struct CircularCVBControl_Previews: PreviewProvider {
static var previews: some View {
CircularCVBControl()
}
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论