最近木事,找出來玩了玩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