移动端访问不佳,请访问我的
先上效果图和,有其他好的动效大家也可以交流~
动效的,在 uimovement 网站上看到这个动效时感觉特别 6,就想自己实现一下,费了很长时间,换了几种方案终于实现出来了,下面是实现的步骤:
写一个动效的第一步就应该仔细的去分析它,把它的每一帧展开来看,找一个最合适的方式来实现它,下面是我分析过程:
和
- CAShapeLayer
这一对搭档,相对于
- UIBezierPath
而言,它即简单有高效;
- CoreGraphics
加上一个参考的
- CADisplayLink
,以参考
- view
为
- view
的一个
- UIBezierPath
,移动参考
- controlPoint
来实现曲线拉拽的效果;
- view
配合
- CAKeyframeAnimation
来使用,本来打算使用
- CAShapeLayer
来实现,但是考虑它是
- CASpringanimation
出的,而我的轮子最低支持 iOS8,就放弃用它了;
- iOS9
来实现小球,用
- CAShapeLayer
来实现小球的移动;
- CABasicAnimation
来实现圆环,然后配合
- CAShapeLayer
控制
- CABasicAnimation
的
- CAShapeLayer
和
- strokeEnd
一直来实现外层圆环旋转的效果;
- transform.rotation.z
在动画的过程中实时的去监听小球和曲线的位置,计算出
- CADisplayLink
用一个
- UIBezierPath
来精确的连接小球和曲线部分。
- CAShapeLayer
好了,以上是大概过程,如果大家有另外的更好的实现方式,也可以一起来讨论。
我们用
和
- CAShapeLayer
这一对搭档来实现曲线的绘制,下面以一个参考
- UIBezierPath
来给大家演示一下,下面是主要代码和效果图:
- view
- // 通过传递的y坐标来绘制曲线
- func wave(_ y: CGFloat, execute: CGFloat) {
- self.execute = execute
- waveLayer.path = wavePath(x: 0, y: y)
- if !isAnimation {
- var trans = CGAffineTransform.identity
- trans = trans.translatedBy(x: 0, y: y)
- reference.transform = trans
- }
- }
- // 计算path
- private func wavePath(x: CGFloat, y: CGFloat) -> CGPath {
- let w = frame.width
- let path = UIBezierPath()
- if y < execute {
- path.move(to: .zero)
- path.addLine(to: .init(x: w, y: 0))
- path.addLine(to: .init(x: w, y: y))
- path.addLine(to: .init(x: 0, y: y))
- path.addLine(to: .zero)
- }else {
- path.move(to: .zero)
- path.addLine(to: .init(x: w, y: 0))
- path.addLine(to: .init(x: w, y: execute))
- path.addQuadCurve(to: .init(x: 0, y: execute), controlPoint: .init(x: w/2, y: y))
- path.addLine(to: .zero)
- }
- return path.cgPath
- }
曲线的回弹使用
加到参考的
- CAKeyframeAnimation
上,然后用
- view
监听参考
- CADisplayLink
的坐标做为
- view
来实现曲线的回弹效果,下面是主要代码和效果图:
- controlPoint
- // 开始动画
- func startAnimation() {
- isAnimation = true
- addDisPlay()
- boundAnimation(x: 0, y: execute)
- }
- // CAKeyframeAnimation动画
- private func boundAnimation(x: CGFloat, y: CGFloat) {
- let bounce = CAKeyframeAnimation(keyPath: "transform.translation.y")
- bounce.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)
- bounce.duration = bounceDuration
- bounce.values = [
- reference.frame.origin.y,
- y * 0.5,
- y * 1.2,
- y * 0.8,
- y * 1.1,
- y
- ]
- bounce.isRemovedOnCompletion = true
- bounce.fillMode = kCAFillModeForwards
- bounce.delegate = self
- reference.layer.add(bounce, forKey: "return")
- }
- // 添加和移除CADisplayLink
- private func addDisPlay() {
- displayLink = CADisplayLink(target: self, selector: #selector(displayAction))
- displayLink?.add(to: .main, forMode: .commonModes)
- }
- private func removeDisPlay() {
- displayLink?.invalidate()
- displayLink = nil
- }
- // CADisplayLink绑定的方法
- @objc private func displayAction() {
- if let frame = reference.layer.presentation()?.frame {
- DispatchQueue.global().async {
- let path = self.displayWavePath(x: 0, y: frame.origin.y + referenceHeight/2)
- DispatchQueue.main.async {
- self.waveLayer.path = path
- }
- }
- }
- }
- // 通过这个方法获取path
- private func displayWavePath(x: CGFloat, y: CGFloat) -> CGPath {
- let w = frame.width
- let path = UIBezierPath()
- path.move(to: .zero)
- path.addLine(to: .init(x: w, y: 0))
- path.addLine(to: .init(x: w, y: execute))
- path.addQuadCurve(to: .init(x: 0, y: execute), controlPoint: .init(x: w/2, y: y))
- path.addLine(to: .zero)
- return path.cgPath
- }
小球和外层圆环我们用
来绘制,这里主要讲的是动画的实现,动画主要由两个部分组成:
- CAShapeLayer
控制外层圆环的
- CABasicAnimation
的动画;
- strokeEnd
控制外层圆环的
- CABasicAnimation
的旋转动画;
- transform.rotation.z
外层圆环的 动画 | 外层圆环的 的旋转动画 |
---|---|
下面是关键代码:
- func animation() {
- self.isHidden = false
- let rotate = CABasicAnimation(keyPath: "transform.rotation.z")
- rotate.fromValue = 0
- rotate.toValue = M_PI * 2
- rotate.duration = 1
- rotate.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
- rotate.repeatCount = HUGE
- rotate.fillMode = kCAFillModeForwards
- rotate.isRemovedOnCompletion = false
- self.add(rotate, forKey: rotate.keyPath)
- strokeEndAnimation()
- }
- func strokeEndAnimation() {
- let endPoint = CABasicAnimation(keyPath: "strokeEnd")
- endPoint.fromValue = 0
- endPoint.toValue = 1
- endPoint.duration = 1.8
- endPoint.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
- endPoint.repeatCount = HUGE
- endPoint.fillMode = kCAFillModeForwards
- endPoint.isRemovedOnCompletion = false
- endPoint.delegate = self
- add(endPoint, forKey: endPoint.keyPath)
- }
小球上升动画很简单,一个
动画就实现了,主要麻烦的是连接处的动画实现,我的方案是在小球动画过程中通过
- CABasicAnimation
实时监听小球和参考
- CADisplayLink
的位置,计算出贝斯尔曲线,然后通过一个名为
- view
的
- linkLayer: CAShapeLayer
来连接它们,然后让它们在特定的地方断开,下面是主要代码和效果图:
- layer
- @objc private func displayAction() {
- let offY = ballLayer.circleLayer.presentation()?.frame.origin.y
- let frame1 = ballLayer.frame
- let frame2 = wavelayer.reference.layer.presentation()?.frame
- if let offY = offY, let frame2 = frame2 {
- DispatchQueue.global().async {
- // 判断是球是向上还是下,false为上,速度快时,获取的位置不及时,向下时需要调整位置
- let isIncrement = (offY - self.previousOffY) > 0
- let path = UIBezierPath()
- let x1 = frame1.origin.x + (isIncrement ? 4 : 0)
- let y1 = frame1.origin.y + offY
- let w1 = frame1.size.width - (isIncrement ? 8 : 0)
- let h1 = frame1.size.height
- let x2 = frame2.origin.x
- let y2 = frame2.origin.y
- let w2 = frame2.size.width
- let h2 = frame2.size.height
- let subY = y2 - y1
- // y1和y2的间距
- let subScale = subY/self.execute/2
- // 断开的距离为10
- let executeSub = self.ballLayer.circleLayer.moveUpDist + offY
- if executeSub < 10 {
- if !isIncrement {
- let executeSubScale = executeSub/10
- path.move(to: .init(x: x1 - 15, y: y2 + h2/2 + 15))
- path.addLine(to: .init(x: x1 + w1 + 15, y: y2 + h2/2 + 15))
- path.addQuadCurve(to: .init(x: x1 - 15, y: y2 + h2/2 + 15), controlPoint: .init(x: x1 + w1/2, y: y2 + h2/2 - self.execute/6 * executeSubScale))
- }
- }else {
- path.move(to: .init(x: x2 , y: y2 + h2))
- path.addLine(to: .init(x: x2 + w2, y: y2 + h2))
- path.addQuadCurve(to: .init(x: x1 + w1, y: y1 + h1/2), controlPoint: .init(x: x1 + w1 - w1*2*subScale, y: y1 + (y2 - y1)/2 + h1/2 + h2/2))
- path.addLine(to: .init(x: x1, y: y1 + h1/2))
- path.addQuadCurve(to: .init(x: x2 , y: y2 + h2), controlPoint: .init(x: x1 + w1*2*subScale, y: y1 + (y2 - y1)/2 + h1/2 + h2/2))
- if y1 + h1 <= self.execute, isIncrement {
- DispatchQueue.main.async {
- self.wavelayer.startDownAnimation()
- }
- }
- }
- DispatchQueue.main.async {
- self.linkLayer.path = path.cgPath
- }
- self.previousOffY = offY
- }
- }
- }
我觉得我这个地方的处理不是很好,但是简单粗暴的解决了问题,如果大家有更好的建议,可以提出来,大家一起交流学习~
来源: http://www.bubuko.com/infodetail-1986229.html