食材飘出动画 & 搅拌动画
飘出动画&搅拌动画.gif
一.食材飘出动画
需求: 如上图所示,当炒菜机进入到对应的工作步骤, 需要在炒菜界面展示对应步骤中的图片信息, 要求图片有右侧飘出, 并且在此过程中每张图片底部的两只脚有"走路"的感觉, 当图片飘到左侧时, 动画停止.
分析: 为了达到图片自己"走路"出来的效果, 其实需要进行两组动画(右侧到左侧飘出的动画 & 图片底部两只脚摆动的动画), 两者同时进行.
飘出动画层级.png
实现方案: 1. 基于图片的数量考虑, 使用集合视图UICollectionView展示整组图片, 通过调整UICollectionView的x坐标实现飘出效果; 2.定制cell,通过UIImageView的animationImages属性将走路姿态的两张图片设置上去, 并且通过属性animationDuration设置一组动画持续的时间, 就可以实现"走路"效果, 这样两组动画可以独立运行并且完美配合.
问题点: 由于需要实现的效果是需要将图片放到"走路"图片中的圆弧中, 而且由于"走路"图片是非居中对齐的, 上下左右的间隙都不一样, 所以涉及到图片的偏移量计算(适应不同尺寸屏幕), 图片缩放尺寸计算, 图片的裁剪等问题(其实这些问题可以有更简单的解决方法, 下面会说到).
关键代码:
/// 飘出动画
UIView.animate(withDuration: 2.5, animations: {
weakSelf?.collectionView.contentOffset = CGPoint(x: 0, y: 0)
}, completion: { (true) in
weakSelf?.emanateAnimationIsRunnig = false
weakSelf?.collectionView.reloadData()
})
/// "走路"动画
animationImageView.animationImages = [UIImage(named:"zoulu1")!, UIImage(named: "zoulu2")!]
animationImageView.animationDuration = 0.25
/// 图片操作: 位移 & 缩放尺寸 & 裁剪
// cell宽(根据不同屏幕尺寸)
let realWidth = CGFloat(Ruler.iPhoneVertical(83, 83, 100, 112, 112).value)
// cell长(根据不同屏幕尺寸)
let realHeight = CGFloat(Ruler.iPhoneVertical(79.5, 79.5, 96.1, 107.6, 107.6).value)
// 半径
pathRadius = realWidth * 170 / 254 / 2
// 底部飘动画图片圆中心点坐标x
pathStartX = realWidth * 25 / 254
// 底部飘动画图片圆中心点坐标y
pathStartY = realHeight * 42 / 244
showImageView = UIImageView(frame: CGRect(x: pathStartX, y: pathStartY, w: pathRadius * 2, h: pathRadius * 2))
showImageView.contentMode = .scaleToFill
addSubview(showImageView)
// 起始弧度
let startAngle = Double.pi * 0.90
// 结束弧度
let endAngle = Double.pi * 0.15
// 起点坐标(0.87是 根号3 / 2)
let startX = pathRadius * 0.13
let startY = pathRadius * 1.5
let path = UIBezierPath()
path.move(to: CGPoint(x: CGFloat(startX), y: CGFloat(startY)))
/// center:圆心的坐标, radius:半径, startAngle:起始的弧度, endAngle:圆弧结束的弧度, clockwise:true为顺时针,false为逆时针
path.addArc(withCenter: CGPoint(x: pathRadius, y: pathRadius), radius: CGFloat(pathRadius), startAngle: CGFloat(startAngle), endAngle: CGFloat(endAngle), clockwise: true)
path.close()
let maskLayer = CAShapeLayer()
maskLayer.path = path.cgPath
maskLayer.fillColor = UIColor.white.cgColor
maskLayer.strokeColor = UIColor.red.cgColor
maskLayer.frame = showImageView.bounds
showImageView.layer.mask = maskLayer
走路动画底图测量.png
以上问题简化方案: 1. 由于UI设计师设计的时候为"走路"动画中的两张图片设计了阴影效果导致需要展示图片的位置非居中对齐, 后期开发中发现, 由于展示界面的背景是纯黑背景, 所以添加了阴影也是看不出来的, 不如直接去掉阴影, 这样图片直接居中对齐就ok了, 不需要计算x,y中的偏移量, 减少计算量; 2. 关于将图片裁剪并且放到"走路"动画底图中圆弧中的方式, 上面的方式是根据弧度, 半径等参数进行裁剪而实现的, 其实有另一种更为简单直接的方式, 让UI设计师把圆弧中间的位置直接扣空, 然后app只需要将需要展示的图片UIImageView放到"走路"动画底图UIImageView的下方就可以了, 展示效果比上面的还要好, 并且不需要计算弧度等数据, 不需要通过layer.mask对图片进行裁剪, 减少计算量和代码, 何乐而不为.
二.搅拌动画
需求: 机器的档位总共分为8挡, 每个档位都有一个预设的速度值, 档位越高, 搅拌速度越快, 搅拌棒根据不同的档位按照预设的速度沿着锅的内侧来回摆动, 并根据搅拌的方向动态改变"球"的位置, 以实现搅拌食物的效果.
分析: 其实就是搅拌棒做绕"Z"轴做圆周运动, 只不过不是做完整的圆周运动, 而是在两个固定点之间做圆周运动.
实现方案: 直接上层级图
搅拌动画层级.png
关键代码:
// 搅拌动画
fileprivate func stirAnimation(_ fromValue: Float, toValue: Float, duration: CFTimeInterval) {
positionAnimation?.fromValue = NSNumber.init(value: fromValue)
positionAnimation?.toValue = NSNumber.init(value: toValue)
positionAnimation?.duration = duration
positionAnimation?.fillMode = kCAFillModeForwards
positionAnimation?.repeatCount = 1
positionAnimation?.delegate = self
/// 保持动画结束时状态
positionAnimation?.fillMode = kCAFillModeForwards
positionAnimation?.isRemovedOnCompletion = false
stirView.layer.add(positionAnimation ?? CABasicAnimation.init(keyPath: "transform.rotation.z"), forKey: "stirAnimation")
stirView.layer.speed = 1
}
/// 暂停搅拌动画
fileprivate func pauseStirAnimation() {
stirAnimationIsRunning = false
let pauseTime = stirView.layer.convertTime(CACurrentMediaTime(), from: nil)
stirView.layer.timeOffset = pauseTime
stirView.layer.speed = 0
}
/// 重启搅拌动画
fileprivate func restartStirAnimation() {
if (currentLevel ?? 0) > 1 {
stirAnimationIsRunning = true
if positionAnimation == nil {
positionAnimation = CABasicAnimation.init(keyPath: "transform.rotation.z")
stirAnimation(stirFromValue, toValue: stirToValue, duration: stirDurationArray[currentLevel! - 1])
} else {
positionAnimation?.duration = stirDurationArray[currentLevel! - 1]
let pauseTime = stirView.layer.timeOffset
let timeSincePause = CACurrentMediaTime() - pauseTime
stirView.layer.timeOffset = 0
stirView.layer.beginTime = timeSincePause
stirView.layer.speed = 1
}
}
}
// MARK: - CAAnimationDelegate
func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
if deviceLink?.deviceStateModel.deviceWorkState == DeviceConstants.MACHINE_WORK_STATE_COOKING {
leftStirBall.isHidden = !leftStirBall.isHidden
rightStirBall.isHidden = !rightStirBall.isHidden
let middleValue = stirFromValue
stirFromValue = stirToValue
stirToValue = middleValue
stirAnimation(stirFromValue, toValue: stirToValue, duration: stirDurationArray[currentLevel! - 1])
}
}
总结: 刚开始这些需求的时候, 没有思路, 甚至想去找一些动画的三方库, 最后还是坚持自己写了. 通过上述的分析, 会发现我是通过将最终的效果拆解成由一个个独立的效果组合形成的. 这其实是给自己好的提醒: 1.是否有必要这么复杂? 2.复杂的事情如何简化?
网友评论