Swift: 打造滑动解锁文字动画
最近木事,找出来玩了玩facebook的paper。到处都是那个"slide to unlock your phone"的效果啊。忽闪忽闪的小有点炫酷的感觉。于是准备研究一下。木有想到的是居然可以用CAGradientLayer和一个小小的动画就可以实现这个效果。“滑动解锁”的效果:
当然啦,首先你需要显示出这个“滑动解锁”的文本。这里咱们就用一个简单的UILabel来解决这个问题。
var textExampleLabel: UILabel! override func viewDidLoad() { super.viewDidLoad() textExampleLabel = UILabel(frame: CGRectMake(10, 100, UIScreen.mainScreen().bounds.size.width - 20, 30)) textExampleLabel.text = "slide to unlock your phone, slide to unlock your" self.view.addSubview(textExampleLabel) }
label作为成员变量(属性),在viewDidLoad方法中初始化并赋给“滑动解锁”的文本。
之后就用Gradient Layer来mask这段文本。来看看怎么准备这mask。要使用CALayer这个东东,千万不能少得就是前提条件需要引入QuartzCore。
import QuartzCore
之后为了迎接随后到来的gradient mask,需要重构一部分代码。让整个背景的颜色都为黑色,让label的文字为白色。这样看起来这个解锁的动画效果在强烈的黑白对比下更加明显。重构之后的代码:
self.view.backgroundColor = UIColor.blackColor() textExampleLabel = UILabel(frame: CGRectMake(10, 100, UIScreen.mainScreen().bounds.size.width - 20, 30)) textExampleLabel.text = "slide to unlock your phone, slide to unlock yo" textExampleLabel.backgroundColor = UIColor.blackColor() // background color -> black textExampleLabel.textColor = UIColor.whiteColor() // foreground color -> white self.view.addSubview(textExampleLabel)
这些都是在viewDidLoad方法中的。
下面开始添加mask:
var gradientMask = CAGradientLayer() let colors: Array<AnyObject> = [UIColor.blackColor().CGColor, UIColor.whiteColor().CGColor, UIColor.blackColor().CGColor] gradientMask.colors = colors textExampleLabel.layer.mask = gradientMask
运行出来之后,你会吓一跳。因为,全部都是黑得了。。。
这是因为,mask属性需要用到的是颜色的透明部分。也就是说给定的变化的颜色需要出现一部分透明或者半透明的颜色,这样的gradient layer作为另外一个layer的mask才能发挥作用。另外需要千万注意的一个问题是,上面的gradientMask还需要有一个frame。一般这个值是mask属性所在view的bounds。不要错用了frame,那样gradientMask就mask到错误的地方了。那么补全代码之后,如下:
var testGradient = CAGradientLayer() testGradient.frame = self.textExampleLabel.bounds testGradient.colors = [UIColor(white: 1.0, alpha: 0.3).CGColor, UIColor.yellowColor().CGColor, UIColor(white: 1.0, alpha: 0.3).CGColor] testGradient.startPoint = CGPointMake(0, 0.5) testGradient.endPoint = CGPointMake(1, 0.5) testGradient.locations = [0, 0.15, 0.3]
start point和end point这两个点分别是在layer的坐标体系中的表示。不是一般的定位frame中使用的x和y。比如,当(0.5,0.5)时,表示的是这个layer的中心,center point。(0,0.5)表示的是layer的伤沿的中点,而对应的(0,0.5)表示的下沿的中点。这里的start point和end point分别是左侧边线和右侧边线的中点。最后出现的渐变色的分界线和这点得连线垂直。这也是一个很重要的规律。
gradient layer的locations是用来指定各个颜色的终止位置的。这里分别是0,0.15和0.3。这里可以把这些点理解为顺着颜色渐变线,也就是start point和end point的连线,的百分比的分布。不过,最后的一点效果是没有的。所以,最后的一种颜色一直延续到gradient layer的终点。
这里可以清楚的看到颜色的变化是从UIColor(white: 1.0, alpha: 0.3).CGColor一直到白色然后剩下的都是UIColor(white: 1.0, alpha: 0.3).CGColor。
到这里,就应该让我们的这个效果动起来了。不动的怎么能试动画呢?!
先补上刚刚漏掉的一句:
self.textExampleLabel.layer.mask = testGradient
添加动画的方式就比较简单了。用得就是远古的core animation:CABasicAnimation。这个animation是作用在gradient layer上得locations属性的。所以这个animation的初始化应该是这样的:
var testAnimation = CABasicAnimation(keyPath: "locations")
然后,这个animation就是从一个locations到另外的一个locations。就像我们看到的iphone的解锁画面一样,高亮一部分的文字,从头到尾,一次又一次的重复。一直到屏幕变暗为止。
var testAnimation = CABasicAnimation(keyPath: "locations") testAnimation.fromValue = [0, 0.15, 0.3] testAnimation.toValue = [1 - 0.3, 1 - 0.15, 1.0]; testAnimation.repeatCount = 10000 testAnimation.duration = 0.3 testAnimation.delegate = self
我们这里,设定的颜色渐变是从0到0.15,然后到0.3。这是开始,那么最后的应该是什么样呢,就是长度为0.3的长度上又三个颜色,所以是从1.0 - 0.3,然后到1.0 - 0.15最后到1.0。中间所缺少的由动画自动补上。
好的,让动画起作用:
testGradient.addAnimation(testAnimation, forKey: "TEST")
只需要给layer添加刚刚初始化并配置好的动画之后。layer的动画就会开始运作。至于forKey的值可以很随意,什么都不给也可以。运行代码你就会看到这个动画的效果了。
但是,如果你仔细观察这个看似很完美的动画,就会发现。使用白色高亮的文字效果没有作用在最开始的几个字母上。而在最后,这个白色的高亮效果也没有出现在最后的几个文字上。所以,这个时候就会用到我们前面讲到的layer的坐标系了。start point和end point的x值都是制定在了0和1上。也就是说灰色会在0到0.15上出现。在动画最后的时候灰色会在1.0 - 0.15到1.0上出现。所以,需要把开始的x值往前移,而把最后的x值往后移动。也就是开始的x值变为负数,而最后的x值应该是1.0 + 某个值。所以,这里我们把start point和end point分别设定为:
testGradient.startPoint = CGPointMake(-0.3, 0.5) testGradient.endPoint = CGPointMake(1 + 0.3, 0.5)
这时,在运行这个动画。嗯,一切都完美了。。。
但是。。。又是但是,如果在app中有很多的地方都出现这个效果呢?难道我们要用最简单,最直接的方法来复用这段代码么?这是非常初级和非常可耻的行为,也会给自己埋下定时炸弹。如果需要修改渐变颜色等情况出现,你又忘记修改某一处的代码的时候。。。
我们要重构这段代码,这样在任何的地方使用这个效果的时候可以直接使用我们重构出来的功能代码非常简单的实现这个效果。
添加一个新的swift文件。在这个文件中抽象出我们的“滑动解锁”动画功能:
这里的新的类是继承自NSObject的,因为我们只是需要作用在以后在这里添加的UIView属性,而本身不需要是UIView的子类。
在这个类里面我们需要什么?一个可以设置mask的UIView子类。mask的gradient layer的半透明颜色(么有高亮的时候)和高亮的颜色(白色)。这个动画要持续的重复执行多少次,每次执行的时间是多长...总之,大体上就是这些东西。那么来看看我们的定义:
var animationView: UIView? var notHighlightColor: UIColor! var highlightColor: UIColor! var currentAnimation: CABasicAnimation? var effectWidth: CGFloat = 20.0 // width of the gradient colors will take effect let repeatCount: Float = 10000000 // here, we will let the animation repeat like forever let animationDuration: CFTimeInterval = 0.5 let kTextAnimationKey = "TextAnimation"
出了上面我们说的主要需要的东西以外,就是一些动画的成员变量和动画的key值。为之后动画停止执行的时候删除相对应的动画,而不是删除可能给这个layer添加的别的动画。
开始执行动画:
override init(){ notHighlightColor = UIColor(white: 1.0, alpha: 0.3) highlightColor = UIColor.whiteColor() }
在初始化的时候,初始化非高亮的颜色和高亮的颜色。
开始执行动画的方法:
func start(){ if self.animationView == nil { print("animtion view is nil!") return } // clear things used last time self.stop() var gradientMask = CAGradientLayer() gradientMask.frame = self.animationView!.bounds var gradientSize = self.effectWidth / CGRectGetWidth(self.animationView!.frame) var startLocations: Array<AnyObject> = [0, gradientSize / 2.0, gradientSize] var endLocations: Array<AnyObject> = [1.0 - gradientSize, 1.0 - (gradientSize / 2.0), 1.0] var colors: Array<AnyObject> = [self.notHighlightColor.CGColor, self.highlightColor.CGColor, self.notHighlightColor.CGColor] gradientMask.colors = colors gradientMask.locations = startLocations gradientMask.startPoint = CGPointMake(-gradientSize, 0.5) gradientMask.endPoint = CGPointMake(1 + gradientSize, 0.5) self.animationView!.layer.mask = gradientMask self.currentAnimation = CABasicAnimation(keyPath: "locations") self.currentAnimation!.fromValue = startLocations self.currentAnimation!.toValue = endLocations self.currentAnimation!.repeatCount = self.repeatCount self.currentAnimation!.duration = self.animationDuration self.currentAnimation!.delegate = self gradientMask.addAnimation(self.currentAnimation, forKey: kTextAnimationKey) }
在方法中首先判断动画作用的UIView子类是否存在,如果不存在的时候则立即返回。如果存在,则把刚才我们重构的主要功能的代码全部都放在这里来执行动画。这里有变化的是渐变颜色的区域是在程序中可以指定的,不是刚开始的时候是我们hard code在代码中得。在编程中需要注意,最好不要出现hard code的情况。最不好的情况也需要把这个设定为常量属性。遗留在代码中得hard code代码几乎肯定会产生bug!
下面看看在start方法中调用的stop方法。主要是清楚在上一次的动画执行中的相关的东西。避免干扰。
func stop(){ if self.animationView != nil && self.animationView?.layer.mask != nil { self.animationView?.layer.mask.removeAnimationForKey(kTextAnimationKey) self.animationView?.layer.mask = nil self.currentAnimation = nil } }
这里清楚了layer得mask和在mask上面的动画。
最后是代理的方法animationDidStop(anim: CAAnimation!, finished flag: Bool) 。执行这个方法的时候就表明动画已经停止。这里也可以执行我们的stop方法。
override func animationDidStop(anim: CAAnimation!, finished flag: Bool) { if anim == self.currentAnimation { self.stop() } }
重构完成之后,看看应该怎么使用。我们暴露给其他代码的调用接口,前面已经提到过。执行这个动画的UIView子类,和渐变色的长度:
self.view.backgroundColor = UIColor.blackColor() textExampleLabel = UILabel(frame: CGRectMake(10, 100, UIScreen.mainScreen().bounds.size.width - 20, 30)) textExampleLabel.text = "slide to unlock your phone, slide to unlock yo" textExampleLabel.backgroundColor = UIColor.blackColor() // background color -> black textExampleLabel.textColor = UIColor.whiteColor() // foreground color -> white self.view.addSubview(textExampleLabel) self.textAnimation = TextAnimation() self.textAnimation.animationView = textExampleLabel self.textAnimation.effectWidth = 50.0
然后在view appear之后执行动画:
self.textAnimation.start()
好的,现在来看重构之后的全部的代码:
import UIKit import QuartzCore class ViewController: UIViewController { var textExampleLabel: UILabel! var textAnimation: TextAnimation! override func viewDidLoad() { super.viewDidLoad() self.view.backgroundColor = UIColor.blackColor() textExampleLabel = UILabel(frame: CGRectMake(10, 100, UIScreen.mainScreen().bounds.size.width - 20, 30)) textExampleLabel.text = "slide to unlock your phone, slide to unlock yo" textExampleLabel.backgroundColor = UIColor.blackColor() // background color -> black textExampleLabel.textColor = UIColor.whiteColor() // foreground color -> white self.view.addSubview(textExampleLabel) self.textAnimation = TextAnimation() self.textAnimation.animationView = textExampleLabel self.textAnimation.effectWidth = 50.0 } override func viewDidAppear(animated: Bool) { super.viewDidAppear(animated) self.textAnimation.start() } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() // Dispose of any resources that can be recreated. } }
全文完!
参考:https://github.com/jonathantribouharet/JTSlideShadowAnimation