98

终于iOS11里,我们拥有了傻瓜化的交互式动画

 6 years ago
source link: https://zhuanlan.zhihu.com/p/30718547?
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

终于iOS11里,我们拥有了傻瓜化的交互式动画

你全力以赴都打不倒的男人.

回顾

我们先思考一个问题:iOS11 之前创建哪类动画最麻烦?

答:交互式动画和自定义的timingFunction动画。

无code无真相。我们先来看看早先版本的动画接口是如何实现交互式动画和自定义timingFunciton的。

如何实现一个交互式动画?

大家知道,iOS里面动画的实现方式主要是两种,一种是UIViewAnimation和基于Layer层的CAAnimation。

两种动画的区别很多,当然,符合越底层的接口自由度越高的这个特点。CAAnimation的可定制性更强,但是在我看来,两种动画最主要的区别用一句话形容,就是.

UIViewAnimation是开弓没有回头箭。CAAnimation是流星锤,可收可放。

我们现在,就来实现一个用手势控制的动画。效果如图。

v2-03cb5c46f81920504ff76e2c643a793f_720w.jpg

我们的目的是利用UISlider控制动画的进度,这个动画就是图片绕Y轴旋转。
代码如下。

class ViewController: UIViewController {
    
     let imageView = UIImageView.init(frame: CGRect.init(x: 0, y: 0, width: 100, height: 100))

    override func viewDidLoad() {
        super.viewDidLoad()
        
        imageView.image = UIImage.init(named: "wuyanzu.jpg")
        imageView.center = self.view.center
        imageView.layer.transform.m34 = -1.0/500
        self.view.addSubview(imageView)
        
        let basicAnimation = CABasicAnimation.init(keyPath: "transform.rotation.y")
        basicAnimation.fromValue = 0
        basicAnimation.toValue = CGFloat.pi
        basicAnimation.duration = 1
        imageView.layer.add(basicAnimation, forKey: "rotate")
        imageView.layer.speed = 0
        // Do any additional setup after loading the view, typically from a nib.
    }

    @IBAction func sliderValueChanged(sender:UISlider) {
        imageView.layer.timeOffset = CFTimeInterval(sender.value)
    }

}

在iOS11之前,可交互动画的原理很简单。过程总结如下。
1. 将layer的speed设置为0,这样,动画就处于暂停状态
2. 利用timeOffset来控制整个动画的进度

再举个例子,如果这个动画不是利用UISlider控制旋转角度,而是利用PanGesture移动的距离来控制呢?

那么这种情况,你需要找到的就是手势的距离和Rotate动画timeOffset的一种关联。

我利用Sketch做了一个简陋的草图来模拟这种情况。

其实看完图片我们已经可以建立起手势移动距离和timeOffset的关联。
以横向移动为前提,那么手指的x坐标/图片的width 总是 <= 1.0,所以,当旋转动画的总时长为1,那么动画的进度timeOffset就恰好等于x/imageView.width了。完美的关联了起来。

问题

我们也看到了这种处理方法的弊端。就是,实在太繁琐了。

所以,在今年的wwdc里,苹果为我们提供了一种非常方便的解决方案。

UIViewPropertyAnimator

其实在iOS10,苹果已经引入了另外一种基于View层的强大的动画框架,UIViewPropertyAnimator.

他提供了一个非常棒的方法来解决以前自定义timingFunction只能由CAAnimation来处理的问题。

timingFunction

说到timingFunction,相信写过动画的人都非常清楚系统提供的几种。

  • Liner (线性)
  • EaseIn (先慢后快)
  • EaseOut (先快后慢)
  • EaseInEaseOut (慢进,加速,减速)

实际上这几种timingFunction只能说是勉强够用。当你想更细致调整动画速率的时候势必会使用自定义的贝塞尔曲线来控制动画速率。

比如在http://cubic-bezier.com/,我创建了一个自定义的曲线。

他的control point 分别是(0.17, 0.67, 0.71, 0.15)
那么,如果你想用这个贝塞尔曲线当做timingFunction,在iOS10之前你只能利用CABasicAnimatin来实现。

例如,第一个旋转动画自定义timingFunction是这样的。

basicAnimation.timingFunction = CAMediaTimingFunction.init(controlPoints: 0.17, 0.67, 0.71, 0.15)

想在View层自定义timingFunction?
没门。

所幸,我们在iOS10的时候拥有了UIViewPropertyAnimator

现在,我们如此简单的就创建了一个自定义动画速率的动画。

let convenienceAnimator = UIViewPropertyAnimator.init(duration: 0.66, controlPoint1: point1, controlPoint2: point2) {
                
            }
            convenienceAnimator.addCompletion({ (position) in
                if position == .end {
                    
                }
            })
            convenienceAnimator.startAnimation()   

iOS11中更强大的UIViewPropertyAnimator

session 230中,苹果着重介绍了我们梦寐以求的简单方便的交互式动画api。

举一个session 230中的例子来看一下新版本中如何实现交互式动画。

这里,我们需要用手势来控制动画的进度。这里,动画是让小球从左向右移动100的距离。

看看代码如何简单的将动画和手势关联起来。

var animator:UIViewPropertyAnimator!
var circle:UIImageView!
    
func handlePan(recognizer:UIPanGestureRecognizer) {
        switch recognizer.state {
        case .began:
            animator = UIViewPropertyAnimator.init(duration: 1, curve: .easeOut, animations: {
                self.circle.frame = self.circle.frame.offsetBy(dx: 100, dy: 0)
            })
            animator.pauseAnimation()
        case .changed:
            let translation = recognizer.translation(in: self.circle)
            animator.fractionComplete = translation.x/100
            
        case .ended:
            animator.continueAnimation(withTimingParameters: nil, durationFactor: 0)
            
        default:
            break
        }
    }  
  1. 手势开始的时候创建animator。然后暂停,在这里,动画暂停的本质同样是将Layer的speed设置为0。
  2. 动画的完成率等同于手势移动的距离除以总距离。
  3. 当手势结束的时候,我们调用了continueAnimation让动画继续执行到结束。其实这种需求比较少见,最常见的应该是当手势结束的时候让动画停留在这个阶段而不是继续进行动画。

在这里,我们改造一下这个动画,让它更符合我们的用户习惯。

首先,在手势事件的外部定义好这个animator。

circle.backgroundColor = UIColor.red
        circle.layer.cornerRadius = 10
        circle.frame = CGRect.init(x: 10, y: 100, width: 20, height: 20)
        circle.isUserInteractionEnabled = true
        self.view.addSubview(circle)
        
        animator = UIViewPropertyAnimator.init(duration: 1, curve: .easeOut, animations: {
            self.circle.frame = self.circle.frame.offsetBy(dx: 100, dy: 0)
        })
        animator.pauseAnimation()

然后,手势的事件代码如下。

func handlePan(recognizer:UIPanGestureRecognizer) {
        switch recognizer.state {
        case .began:
            progress = animator.fractionComplete

        case .changed:
            let translation = recognizer.translation(in: self.circle)
            animator.fractionComplete = translation.x/100 + progress
            
        case .ended:
            break
            
        default:
            break
        }
    }

在这里,我们多了一个叫做progress的变量,这个变量的作用就是记录当前动画的进度,在每次手势变化的时候,让动画保持连贯性。不然,每一次动画都重新执行了。
建议同学们这里自己用代码试验一下效果。

出现了一些问题?

话说讲到这里,我不知道有没有同学会对一个非常重要的问题感到疑惑。
什么问题呢?
就是创建animator的时候的timingFunction是EaseOut,先快后慢,那么理论上应该是手势移动了一半,动画早就进行的超过了一半才对。

因为EaseOut的动画曲线是这样的

注意看这张图的横纵坐标。
X坐标代表Time的进度,Y坐标代表动画的进度。
当X走到51%的时候,动画已经进行了72%。
在我们的场景中,这意味着,当手势移动了51个pixel的时候,circle这个view已经跑了72个pixel。

想想这会造成什么问题?

问题就是,用户在交互的时候完全摸不着头脑。

再举个形象的例子。

加入有一个UISlider控制一个Animator的进度,这个Animator是作用于View的透明度Alpha从1到0。

然后Animator的timingFunction是EaseOut,那么用户拖动UISlider的结果很可能是Slider还没滑动到底,这个View的alpha已经变成了0.

为了避免这种情况,当你的Animator是Interactive状态的时候,苹果会自动把你的timingFunction转变为Linear.

那么如果你真的希望可交互式动画的timingFunction不是自动转变为Liner,能不能做到呢?

答案是可以的。
苹果在iOS11中为UIViewPropertyAnimator提供了一个Bool值scrubsLinearly,只要设置为No,那么动画就会按照你设置的timingFunction执行了。

第二个问题,动画执行完了怎么办?

其实在手势执行完毕的时候,调用animator.continueAnimation(withTimingParameters: nil, durationFactor: 0)
会将动画执行完成,但是有一个问题是,动画一旦执行完成,动画的状态就会从Interactive转变为Active,也就是说,不可以再进行交互了。这时候,你需要把animator的pauseOnCompletion设置为false。那么动画就会一直保持Interactive状态了。

SpringAnimation

说实话,这期session中,让我比较失望的就是苹果对于SpringAnimation的支持只是简单地增加了一个under-damping的概念。并没有加入springAnimation中很重要的两个属性。

  • Fricition
  • Tension

为什么这两个属性非常重要。这里,我需要给大家介绍一个国外非常流行的app。Principle

他是国外做交互式prd的非常好用的一个app,我最近在做的一个app在做交互原型的时候大量的使用了这个app。

我们来看看这个app中对于spring动画的一些设置。

用damping这个参数调spring最大的问题就是.....无法当伸手党,直接拿来参数用。
所以,目前来说,最好用的SpringAnimation还是facebook得pop。

比如...... 一个pop伸手党的日常是这样的。

let alphaSpring = POPSpringAnimation.init(propertyNamed: kPOPViewAlpha)
        alphaSpring?.fromValue = 0.67
        alphaSpring?.toValue = 1
        alphaSpring?.dynamicsFriction = 20.17
        alphaSpring?.dynamicsTension = 381.47
        alphaSpring?.delegate = self
        alphaSpring?.name = "alpha"
        self.pop_add(alphaSpring, forKey: "alpha")

只能说,用pop好省心。

补充

cornerRadius终于可动画了。

提出两个问题

  1. iOS11之前真的没有支持手势交互的api么?
  2. 如果存在这样的api,那么这个api的原理是什么呢?是怎样实现无论是UIViewAnimation还是CABasicAnimation都能无缝和手势关联的呢?

这是两个很有意思的问题,大家有空可以思考一下。

参考

Session 230

文章收录在WWDC 2017内参里

有闲钱的可以支持一下这本书
https://item.taobao.com/item.htm?id=554854402409


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK