iOS SwiftUI - 如何在标签不旋转的情况下将标签粘贴到旋转视图?
iOS SwiftUI - How to stick a label to a rotating view without the label rotating too?
这是期望的结果
这就是我现在的样子
有人能帮忙吗?初学SwiftUI,折腾了两天
细线和旋转效果很好,但如何在任何旋转时保持标签水平?
我曾尝试使用 VSTack,但它会导致意外行为。当我只将旋转设置为矩形(细线)时,我不知道如何动态地正确放置标签。
到目前为止,这是我的代码,TodayLabel 上的那篇文章就是完成此操作的地方
struct SingleRingProgressView: View {
let startAngle: Double = 270
let progress: Float // 0 - 1
let ringWidth: CGFloat
let size: CGFloat
let trackColor: Color
let ringColor: Color
let centerText: AttributedText?
let centerTextSubtitle: AttributedText?
let todayLabel: CircleGraph.Label?
private let maxProgress: Float = 2 // allows the ring show a progress up to 200%
private let shadowOffsetMultiplier: CGFloat = 4
private var absolutePercentageAngle: Float {
percentToAngle(percent: (progress * 100), startAngle: 0)
}
private var relativePercentageAngle: Float {
// Take into account the startAngle
absolutePercentageAngle + Float(startAngle)
}
@State var position: (x: CGFloat, y: CGFloat) = (x: 0, y: 0)
var body: some View {
GeometryReader { proxy in
HStack {
Spacer()
VStack {
Spacer()
ZStack {
Circle()
.stroke(lineWidth: ringWidth)
.foregroundColor(trackColor)
.frame(width: size, height: size)
Circle()
.trim(from: 0.0, to: CGFloat(min(progress, maxProgress)))
.stroke(style: StrokeStyle(lineWidth: ringWidth, lineCap: .round, lineJoin: .round))
.foregroundColor(ringColor)
.rotationEffect(Angle(degrees: startAngle))
.frame(width: size, height: size)
if shouldShowShadow(frame: proxy.size) {
Circle()
.fill(ringColor)
.frame(width: ringWidth, height: ringWidth, alignment: .center)
.offset(y: -(size/2))
.rotationEffect(Angle.degrees(360 * Double(progress)))
.shadow(
color: Color.white,
radius: 2,
x: endCircleShadowOffset().0,
y: endCircleShadowOffset().1)
.shadow(
color: Color.black.opacity(0.5),
radius: 1,
x: endCircleShadowOffset().0,
y: endCircleShadowOffset().1)
}
// Today label
if let todayLabel = self.todayLabel {
ZStack {
StyledText(todayLabel.label)
.padding(EdgeInsets(top: 2, leading: 4, bottom: 2, trailing: 4))
.background(Color.color(token: .hint))
.cornerRadius(2)
.offset(y: -(size/1.5))
Rectangle()
.frame(width: 2, height: ringWidth + 2, alignment: .center)
.offset(y: -(size/2))
}.rotationEffect(Angle.degrees(Double(todayLabel.degrees)))
}
VStack(spacing: 4) {
if let text = centerText {
StyledText(text)
}
if let subtitle = centerTextSubtitle {
StyledText(subtitle)
.frame(maxWidth: 120)
.multilineTextAlignment(.center)
}
}
}
Spacer()
}
Spacer()
}
}
}
private func percentToAngle(percent: Float, startAngle: Float) -> Float {
(percent / 100 * 360) + startAngle
}
private func endCircleShadowOffset() -> (CGFloat, CGFloat) {
let angleForOffset = absolutePercentageAngle + Float(startAngle + 90)
let angleForOffsetInRadians = angleForOffset.toRadians()
let relativeXOffset = cos(angleForOffsetInRadians)
let relativeYOffset = sin(angleForOffsetInRadians)
let xOffset = CGFloat(relativeXOffset) * shadowOffsetMultiplier
let yOffset = CGFloat(relativeYOffset) * shadowOffsetMultiplier
return (xOffset, yOffset)
}
private func shouldShowShadow(frame: CGSize) -> Bool {
let circleRadius = min(frame.width, frame.height) / 2
let remainingAngleInRadians = CGFloat((360 - absolutePercentageAngle).toRadians())
if (progress * 100) >= 100 {
return true
} else if circleRadius * remainingAngleInRadians <= ringWidth {
return true
}
return false
}
}
只需将内部文本标签转回 -angle:
struct ContentView: View {
let startAngle: Double = 270
let progress: Float = 0.2 // 0 - 1
let ringWidth: CGFloat = 30
let size: CGFloat = 200
let trackColor: Color = .gray
let ringColor: Color = .blue
let todayLabeldegrees = 120.0
@State var position: (x: CGFloat, y: CGFloat) = (x: 0, y: 0)
var body: some View {
ZStack {
Circle()
.stroke(lineWidth: ringWidth)
.foregroundColor(trackColor)
.frame(width: size, height: size)
Circle()
.trim(from: 0.0, to: CGFloat(progress))
.stroke(style: StrokeStyle(lineWidth: ringWidth, lineCap: .round, lineJoin: .round))
.foregroundColor(ringColor)
.rotationEffect(Angle(degrees: startAngle))
.frame(width: size, height: size)
// Today label
ZStack {
Text("todayLabel")
.padding(EdgeInsets(top: 2, leading: 4, bottom: 2, trailing: 4))
.background(Color.white)
.cornerRadius(5)
.shadow(radius: 2)
.rotationEffect(Angle.degrees(-todayLabeldegrees)) // << turn back
.offset(y: -(size/1.5))
Rectangle()
.frame(width: 2, height: ringWidth + 2, alignment: .center)
.offset(y: -(size/2))
}
.rotationEffect(Angle.degrees(todayLabeldegrees))
VStack(spacing: 4) {
Text("Test").font(.title)
Text("subtitle")
.frame(maxWidth: 120)
.multilineTextAlignment(.center)
}
}
}
}
这是期望的结果
这就是我现在的样子
有人能帮忙吗?初学SwiftUI,折腾了两天
细线和旋转效果很好,但如何在任何旋转时保持标签水平?
我曾尝试使用 VSTack,但它会导致意外行为。当我只将旋转设置为矩形(细线)时,我不知道如何动态地正确放置标签。
到目前为止,这是我的代码,TodayLabel 上的那篇文章就是完成此操作的地方
struct SingleRingProgressView: View {
let startAngle: Double = 270
let progress: Float // 0 - 1
let ringWidth: CGFloat
let size: CGFloat
let trackColor: Color
let ringColor: Color
let centerText: AttributedText?
let centerTextSubtitle: AttributedText?
let todayLabel: CircleGraph.Label?
private let maxProgress: Float = 2 // allows the ring show a progress up to 200%
private let shadowOffsetMultiplier: CGFloat = 4
private var absolutePercentageAngle: Float {
percentToAngle(percent: (progress * 100), startAngle: 0)
}
private var relativePercentageAngle: Float {
// Take into account the startAngle
absolutePercentageAngle + Float(startAngle)
}
@State var position: (x: CGFloat, y: CGFloat) = (x: 0, y: 0)
var body: some View {
GeometryReader { proxy in
HStack {
Spacer()
VStack {
Spacer()
ZStack {
Circle()
.stroke(lineWidth: ringWidth)
.foregroundColor(trackColor)
.frame(width: size, height: size)
Circle()
.trim(from: 0.0, to: CGFloat(min(progress, maxProgress)))
.stroke(style: StrokeStyle(lineWidth: ringWidth, lineCap: .round, lineJoin: .round))
.foregroundColor(ringColor)
.rotationEffect(Angle(degrees: startAngle))
.frame(width: size, height: size)
if shouldShowShadow(frame: proxy.size) {
Circle()
.fill(ringColor)
.frame(width: ringWidth, height: ringWidth, alignment: .center)
.offset(y: -(size/2))
.rotationEffect(Angle.degrees(360 * Double(progress)))
.shadow(
color: Color.white,
radius: 2,
x: endCircleShadowOffset().0,
y: endCircleShadowOffset().1)
.shadow(
color: Color.black.opacity(0.5),
radius: 1,
x: endCircleShadowOffset().0,
y: endCircleShadowOffset().1)
}
// Today label
if let todayLabel = self.todayLabel {
ZStack {
StyledText(todayLabel.label)
.padding(EdgeInsets(top: 2, leading: 4, bottom: 2, trailing: 4))
.background(Color.color(token: .hint))
.cornerRadius(2)
.offset(y: -(size/1.5))
Rectangle()
.frame(width: 2, height: ringWidth + 2, alignment: .center)
.offset(y: -(size/2))
}.rotationEffect(Angle.degrees(Double(todayLabel.degrees)))
}
VStack(spacing: 4) {
if let text = centerText {
StyledText(text)
}
if let subtitle = centerTextSubtitle {
StyledText(subtitle)
.frame(maxWidth: 120)
.multilineTextAlignment(.center)
}
}
}
Spacer()
}
Spacer()
}
}
}
private func percentToAngle(percent: Float, startAngle: Float) -> Float {
(percent / 100 * 360) + startAngle
}
private func endCircleShadowOffset() -> (CGFloat, CGFloat) {
let angleForOffset = absolutePercentageAngle + Float(startAngle + 90)
let angleForOffsetInRadians = angleForOffset.toRadians()
let relativeXOffset = cos(angleForOffsetInRadians)
let relativeYOffset = sin(angleForOffsetInRadians)
let xOffset = CGFloat(relativeXOffset) * shadowOffsetMultiplier
let yOffset = CGFloat(relativeYOffset) * shadowOffsetMultiplier
return (xOffset, yOffset)
}
private func shouldShowShadow(frame: CGSize) -> Bool {
let circleRadius = min(frame.width, frame.height) / 2
let remainingAngleInRadians = CGFloat((360 - absolutePercentageAngle).toRadians())
if (progress * 100) >= 100 {
return true
} else if circleRadius * remainingAngleInRadians <= ringWidth {
return true
}
return false
}
}
只需将内部文本标签转回 -angle:
struct ContentView: View {
let startAngle: Double = 270
let progress: Float = 0.2 // 0 - 1
let ringWidth: CGFloat = 30
let size: CGFloat = 200
let trackColor: Color = .gray
let ringColor: Color = .blue
let todayLabeldegrees = 120.0
@State var position: (x: CGFloat, y: CGFloat) = (x: 0, y: 0)
var body: some View {
ZStack {
Circle()
.stroke(lineWidth: ringWidth)
.foregroundColor(trackColor)
.frame(width: size, height: size)
Circle()
.trim(from: 0.0, to: CGFloat(progress))
.stroke(style: StrokeStyle(lineWidth: ringWidth, lineCap: .round, lineJoin: .round))
.foregroundColor(ringColor)
.rotationEffect(Angle(degrees: startAngle))
.frame(width: size, height: size)
// Today label
ZStack {
Text("todayLabel")
.padding(EdgeInsets(top: 2, leading: 4, bottom: 2, trailing: 4))
.background(Color.white)
.cornerRadius(5)
.shadow(radius: 2)
.rotationEffect(Angle.degrees(-todayLabeldegrees)) // << turn back
.offset(y: -(size/1.5))
Rectangle()
.frame(width: 2, height: ringWidth + 2, alignment: .center)
.offset(y: -(size/2))
}
.rotationEffect(Angle.degrees(todayLabeldegrees))
VStack(spacing: 4) {
Text("Test").font(.title)
Text("subtitle")
.frame(maxWidth: 120)
.multilineTextAlignment(.center)
}
}
}
}