在 iOS 上创建动画指示器箭头
Creating an animating indicator arrow on iOS
我正在尝试实现与 iOS 的音乐应用中的指示箭头类似的效果,当您滚动经过某个点时,它会从箭头变为单行。我在下面附上了一个放慢速度的例子。
我很难理解的一点是箭头本身,我尝试使用两个旋转的 UIView
s 作为与约束一起定位的线,并使用 UIViewPropertyAnimator
成功创建了动画和关键帧。但不幸的是,当从一个箭头转到一条线时,这些线会相互干扰。
我不确定实现此示例的正确方法。任何朝着正确方向推动实现这一目标的努力都将受到高度赞赏。
最简单的方法是为 CAShapeLayer 的路径设置动画。这为您提供了圆角末端和免费的斜接连接。你需要一条直路和一条弯路;从一个切换到另一个。直线路径必须由两段组成,以匹配弯曲路径的两段。
这是我对你的动画的模拟:
形状图层及其路径的配置如下:
let shape = CAShapeLayer()
shape.frame = // ...
shape.lineCap = .round
shape.lineJoin = .miter
shape.lineWidth = 6
shape.strokeColor = UIColor.gray.cgColor
shape.fillColor = UIColor.clear.cgColor
let p1 = UIBezierPath()
p1.move(to: CGPoint(x:0, y:35))
p1.addLine(to: CGPoint(x:20, y:35))
p1.addLine(to: CGPoint(x:40, y:35))
let straight = p1.cgPath
let p2 = UIBezierPath()
p2.move(to: CGPoint(x:0, y:25))
p2.addLine(to: CGPoint(x:20, y:30))
p2.addLine(to: CGPoint(x:40, y:25))
let bent = p2.cgPath
那么只需根据需要在 straight
和 bent
之间交替 shape
的 path
即可。
对于那些希望使用 SwiftUI 实现此目的的人,您可以通过创建符合 Shape
协议的对象来实现。这样做的好处是你可以利用 animatableData
属性 作为变量来定义你的形状,简化动画过程(Advanced SwiftUI Animations – Part 1: Paths 很好地介绍了这个概念)。
struct ChevronShape: Shape {
var pointingUp: Bool
private var heightOffset: CGFloat
init(pointingUp: Bool) {
self.pointingUp = pointingUp
self.heightOffset = pointingUp ? 1 : 0
}
var animatableData: CGFloat {
get { return heightOffset }
set { heightOffset = newValue }
}
func path(in rect: CGRect) -> Path {
var path = Path()
let width = rect.width
let height = rect.height
let horizontalCenter = width / 2
let horizontalCenterOffset = width * 0.15
let arrowTipStartingPoint = height - heightOffset * height * 0.7
path.move(to: .init(x: 0, y: height))
path.addLine(to: .init(x: horizontalCenter - horizontalCenterOffset, y: arrowTipStartingPoint))
/// User of quad-curve to achieve "roudning effect" at arrow tip
path.addQuadCurve(to: .init(x: horizontalCenter + horizontalCenterOffset, y: arrowTipStartingPoint), control: .init(x: horizontalCenter, y: height * (1 - heightOffset)))
path.addLine(to: .init(x: width, y: height))
return path
}
}
以上形状现在可以用作任何其他 SwiftUI 视图,考虑以下因素-
pointingUp
参数确定形状的状态,因此需要封装在影响视图层次结构状态的 属性 中(我在中使用了 @State
下面的例子)。
- 您可以使用显式或隐式动画通过修改上述状态来触发动画。我在下面使用了明确的动画,因为它可以更清楚地传达此时发生的动作。
Shape
对象的表示方式非常灵活。不利的一面是,他们需要额外的修饰符才能获得所需的外观。
struct ContentView: View {
@State private var pointingUp = true
var body: some View {
VStack {
ChevronShape(pointingUp: pointingUp)
/// Modifiers affecting general shape
.stroke(style: StrokeStyle(lineWidth: 10, lineCap: .round))
.frame(width: 60, height: 15)
/// Non-essential Modifiers
.foregroundColor(.gray)
.opacity(0.6)
/// Interaction modifiers
.contentShape(Rectangle())
.onTapGesture {
withAnimation(.easeInOut(duration: 1)) { self.pointingUp.toggle() }
}
}
.frame(width: 100, height: 100, alignment: .center)
}
}
这是最终结果-
我正在尝试实现与 iOS 的音乐应用中的指示箭头类似的效果,当您滚动经过某个点时,它会从箭头变为单行。我在下面附上了一个放慢速度的例子。
我很难理解的一点是箭头本身,我尝试使用两个旋转的 UIView
s 作为与约束一起定位的线,并使用 UIViewPropertyAnimator
成功创建了动画和关键帧。但不幸的是,当从一个箭头转到一条线时,这些线会相互干扰。
我不确定实现此示例的正确方法。任何朝着正确方向推动实现这一目标的努力都将受到高度赞赏。
最简单的方法是为 CAShapeLayer 的路径设置动画。这为您提供了圆角末端和免费的斜接连接。你需要一条直路和一条弯路;从一个切换到另一个。直线路径必须由两段组成,以匹配弯曲路径的两段。
这是我对你的动画的模拟:
形状图层及其路径的配置如下:
let shape = CAShapeLayer()
shape.frame = // ...
shape.lineCap = .round
shape.lineJoin = .miter
shape.lineWidth = 6
shape.strokeColor = UIColor.gray.cgColor
shape.fillColor = UIColor.clear.cgColor
let p1 = UIBezierPath()
p1.move(to: CGPoint(x:0, y:35))
p1.addLine(to: CGPoint(x:20, y:35))
p1.addLine(to: CGPoint(x:40, y:35))
let straight = p1.cgPath
let p2 = UIBezierPath()
p2.move(to: CGPoint(x:0, y:25))
p2.addLine(to: CGPoint(x:20, y:30))
p2.addLine(to: CGPoint(x:40, y:25))
let bent = p2.cgPath
那么只需根据需要在 straight
和 bent
之间交替 shape
的 path
即可。
对于那些希望使用 SwiftUI 实现此目的的人,您可以通过创建符合 Shape
协议的对象来实现。这样做的好处是你可以利用 animatableData
属性 作为变量来定义你的形状,简化动画过程(Advanced SwiftUI Animations – Part 1: Paths 很好地介绍了这个概念)。
struct ChevronShape: Shape {
var pointingUp: Bool
private var heightOffset: CGFloat
init(pointingUp: Bool) {
self.pointingUp = pointingUp
self.heightOffset = pointingUp ? 1 : 0
}
var animatableData: CGFloat {
get { return heightOffset }
set { heightOffset = newValue }
}
func path(in rect: CGRect) -> Path {
var path = Path()
let width = rect.width
let height = rect.height
let horizontalCenter = width / 2
let horizontalCenterOffset = width * 0.15
let arrowTipStartingPoint = height - heightOffset * height * 0.7
path.move(to: .init(x: 0, y: height))
path.addLine(to: .init(x: horizontalCenter - horizontalCenterOffset, y: arrowTipStartingPoint))
/// User of quad-curve to achieve "roudning effect" at arrow tip
path.addQuadCurve(to: .init(x: horizontalCenter + horizontalCenterOffset, y: arrowTipStartingPoint), control: .init(x: horizontalCenter, y: height * (1 - heightOffset)))
path.addLine(to: .init(x: width, y: height))
return path
}
}
以上形状现在可以用作任何其他 SwiftUI 视图,考虑以下因素-
pointingUp
参数确定形状的状态,因此需要封装在影响视图层次结构状态的 属性 中(我在中使用了@State
下面的例子)。- 您可以使用显式或隐式动画通过修改上述状态来触发动画。我在下面使用了明确的动画,因为它可以更清楚地传达此时发生的动作。
Shape
对象的表示方式非常灵活。不利的一面是,他们需要额外的修饰符才能获得所需的外观。
struct ContentView: View {
@State private var pointingUp = true
var body: some View {
VStack {
ChevronShape(pointingUp: pointingUp)
/// Modifiers affecting general shape
.stroke(style: StrokeStyle(lineWidth: 10, lineCap: .round))
.frame(width: 60, height: 15)
/// Non-essential Modifiers
.foregroundColor(.gray)
.opacity(0.6)
/// Interaction modifiers
.contentShape(Rectangle())
.onTapGesture {
withAnimation(.easeInOut(duration: 1)) { self.pointingUp.toggle() }
}
}
.frame(width: 100, height: 100, alignment: .center)
}
}
这是最终结果-