前言
不得不說炊豪,單單是文章的標題拧篮,可能不足以說明本文的內容。因此串绩,在繼續(xù)講述約束動畫之前,我先放上本文要實現(xiàn)的動畫效果高氮。
約束動畫并不是非常復雜的技巧顷牌,在你熟練使用約束之后,你總能創(chuàng)建些獨具匠心的動畫罪裹。在上一篇autolayout動畫初體驗中,我們根據(jù)監(jiān)聽列表視圖的滾動偏移來不斷改變約束值状共,從而制作出動畫的效果。但上個動畫的實現(xiàn)更像是我們制作了一幀幀連續(xù)的界面從而達成動畫的效果 —— 這未免太過繁雜箍铲。而在本文我們將拋棄這種繁雜的方式鬓椭,通過調用UIView
的重繪制視圖方法來實現(xiàn)動畫。
本文的動畫主要存在這么幾個:
- 賬戶紀錄列表的彈出和收回
- 登錄按鈕的點擊形變
- 登錄按鈕被點擊后的轉圈動畫(不做詳細講述)
實際上來說翘瓮,上述的轉圈動畫我是通過CoreAnimation
框架的動畫+定時器的方式實現(xiàn)的裤翩,當然這也意味著在本篇文章是約束動畫的終結
準備
首先我們需要把所有控件的層次弄清楚,然后搭建整個界面踊赠。在demo中,在動畫開始前可視的控件總共有五個 —— 用戶頭像今穿、賬戶輸入框伦籍、下拉按鈕、密碼輸入框以及登錄按鈕帖鸦,還有一個保存賬號信息的列表在賬戶輸入框下面。我們通過修改這個列表跟賬戶頂部約束值來使其下移出現(xiàn):
在這些控件的約束中我們需要在代碼中改變用到的約束包括:
- 紀錄列表的頂部約束
listTopConstraint
洛二,修改后可以讓列表向下移動出現(xiàn) - 紀錄列表的高度約束
listHeightConstraint
攻锰,用來設置產(chǎn)生展開動畫 - 賬戶輸入框的高度約束
accountHeightConstraint
,用來設置列表的下移量 - 登錄按鈕左右側相對于父視圖的間距約束
loginLeftConstraint
以及loginRightConstraint
变擒,通過改變這兩個約束來使按鈕縮小 - 登錄按鈕的高度約束
loginHeightConstraint
寝志,用來計算縮小后的按鈕寬度
除了約束屬性之外策添,我們還需要一些數(shù)據(jù)來支持我們的動畫:
@property(assign, nonatomic) BOOL isAnimating; // 用來判斷登錄按鈕的動畫狀態(tài)
@property(strong, nonatomic) NSArray * records; // 列表的數(shù)據(jù)源毫缆,demo中存儲五個字符串
為了保證列表出現(xiàn)的時候不被其他視圖遮擋,設置的視圖層級關系如下:
下拉按鈕 > 賬戶輸入框 > 紀錄列表 > 頭像 = 登錄按鈕 = 密碼輸入框
下拉動畫
在這里我把demo的動畫分為兩小節(jié)來講解浸颓,因為這兩個動畫的實現(xiàn)方式有很大的差別旺拉。當然了,這兩者都能通過直接修改約束的constant
值來實現(xiàn)蛾狗,但這不是本文的講解目的。
在我們點擊下拉按鈕的時候會出現(xiàn)兩個動畫谢鹊,包括下拉列表的180°旋轉以及下拉或者隱藏消失留凭。正常來說,我們需要使用一個BOOL
類型的變量來標識列表是否處在展開的狀態(tài)以此來決定動畫的方式兼耀,但在關聯(lián)事件方法的時候作為發(fā)送者的下拉按鈕已經(jīng)提供給了我們這個變量isSelected
挎扰,通過修改這個值來完成標記列表展開狀態(tài)。因此旋轉下拉按鈕的代碼如下:
/// 點擊打開或者隱藏列表
- (IBAction)actionToOpenOrCloseList:(UIButton *)sender {
[self.view endEditing: YES];
[self animateToRotateArrow: sender.selected];
sender.isSelected ? [self showRecordList] : [self hideRecordList];
}
/// 按鈕轉向動畫
- (void)animateToRotateArrow: (BOOL)selected
{
CATransform3D transform = selected ? CATransform3DIdentity : CATransform3DMakeRotation(M_PI, 0, 0, 1);
[_dropdownButton setSelected: !selected];
[UIView animateWithDuration: 0.25 animations: ^{
_dropdownButton.layer.transform = transform;
}];
}
可以看到我們的代碼中根據(jù)按鈕的isSelected
屬性來決定列表的展開或者收回遵倦,對此我們需要修改列表的listHeightConstraint
和listTopConstraint
來設置列表的大小和位置官撼,而且我們需要給展開的列表加上一個彈出來的動畫:
/// 顯示紀錄列表
- (void)showRecordList
{
[UIView animateWithDuration: 0.25 delay: 0 usingSpringWithDamping: 0.4 initialSpringVelocity: 5 options: UIViewAnimationOptionCurveEaseInOut | UIViewAnimationOptionAllowUserInteraction animations: ^{
_listTopConstraint.constant = _accountHeightConstraint.constant;
_listHeightConstraint.constant = _accountHeightConstraint.constant * 5;
} completion: nil];
}
/// 隱藏紀錄列表
- (void)hideRecordList
{
[UIView animateWithDuration: 0.25 animations: ^{
_listTopConstraint.constant = 0;
_listHeightConstraint.constant = 0;
} completion: nil];
}
好傲绣,運行你的代碼,看看效果秃诵,這肯定不會是你想要的效果。
在UIView
動畫中有趣的一件事情是:如果你直接在動畫的block
中提交修改了視圖的相關屬性時禁舷,它們會按照你預期的效果執(zhí)行產(chǎn)生動畫。但在你修改約束值的時候會直接計算出約束生效后的布局結果派近,并且直接顯示 —— 即便你把修改約束的代碼放在了動畫的block
當中執(zhí)行洁桌。
針對這個問題,iOS為所有視圖提供了一個方法- (void)layoutIfNeeded
用來立刻刷新界面另凌,這個方法會調用當前視圖上面所有的子視圖的- (void)layoutSubviews
讓子視圖進行重新布局。如果我們先設置好約束值碟嘴,然后在動畫的執(zhí)行代碼調用layoutIfNeeded
就能讓界面不斷重新繪制產(chǎn)生動畫效果囊卜。因此,上面的展開收回代碼改成下面這樣:
/// 顯示紀錄列表
- (void)showRecordList
{
_listTopConstraint.constant = _accountHeightConstraint.constant;
_listHeightConstraint.constant = _accountHeightConstraint.constant * 5;
[UIView animateWithDuration: 0.25 delay: 0 usingSpringWithDamping: 0.4 initialSpringVelocity: 5 options: UIViewAnimationOptionCurveEaseInOut | UIViewAnimationOptionAllowUserInteraction animations: ^{
[self.view layoutIfNeeded];
} completion: nil];
}
/// 隱藏紀錄列表
- (void)hideRecordList
{
_listTopConstraint.constant = 0;
_listHeightConstraint.constant = 0;
[UIView animateWithDuration: 0.25 animations: ^{
[self.view layoutIfNeeded];
} completion: nil];
}
現(xiàn)在再次運行你的代碼雀瓢,紀錄列表已經(jīng)能夠正常的實現(xiàn)彈出展開以及收回的動畫了
登錄按鈕動畫
毫不客氣的說玉掸,在本次的demo中,我最喜歡的是登錄按鈕點擊之后的動畫效果司浪。當然,這也意味著這個動畫效果是耗時最長的吁伺。
由于如demo動畫所示租谈,在點擊登錄動畫的時候按鈕中間會有進度的旋轉動畫。如果在controller中實現(xiàn)這個效果割去,需要涉及到layer
層的操作,而這不應該是控制器的職能夸赫,因此我將登錄按鈕單獨封裝出來處理咖城,并提供了兩個接口:- (void)start
和- (void)stop
方便控制器調用來開始和停止動畫呼奢。這兩個方法內部實現(xiàn)如下:
const NSTimeInterval duration = 1.2;
///動畫開始隱藏文字
- (void)start
{
[self addAnimate];
if (_timer) {
[_timer invalidate];
}
NSTimer * timer = [NSTimer scheduledTimerWithTimeInterval: duration target: self selector: @selector(addAnimate) userInfo: nil repeats: YES];
_timer = timer;
[UIView animateWithDuration: 0.5 animations: ^{
_circle.opacity = 1;
[self setTitleColor: [UIColor colorWithWhite: 1 alpha: 0] forState: UIControlStateNormal];
}];
}
///動畫結束時顯示文字
- (void)stop
{
if (_timer) {
[_timer invalidate];
}
[self.circle removeAllAnimations];
[UIView animateWithDuration: 0.5 animations: ^{
_circle.opacity = 0;
[self setTitleColor: [UIColor colorWithWhite: 1 alpha: 1] forState: UIControlStateNormal];
}];
}
前文我已經(jīng)說過按鈕的轉圈動畫基于定時器和CoreAnimation
框架動畫實現(xiàn)滓彰,由于這不屬于約束動畫范疇,我就不對具體實現(xiàn)進行闡述了弓候,感興趣的可以到本文的demo中去查看實現(xiàn)他匪。
除了按鈕自身轉圈、文字隱藏顯示的動畫之外邦蜜,還包括了自身的尺寸變化代碼。在縮小之后按鈕依舊保持在視圖的x軸中心位置贱迟,因此如果我們修改左右約束絮供,那么要保證這兩個值是相等的。在動畫前后按鈕的高度都沒有發(fā)生變化壤靶,在縮小的過程中寬度縮小成和高度一樣的大小,我們現(xiàn)在有按鈕的高度height
忧换,通過代碼計算出左右新約束:
/// 點擊登錄動畫
- (IBAction)actionToSignIn:(UIButton *)sender {
_isAnimating = !_isAnimating;
if (_isAnimating) {
[_signInButton start];
[self animateToMakeButtonSmall];
} else {
[_signInButton stop];
[self animateToMakeButtonBig];
}
}
///縮小動畫
- (void)animateToMakeButtonSmall {
CGFloat height = _loginHeightConstraint.constant;
CGFloat screenWidth = CGRectGetWidth(self.view.frame);
CGFloat spacingConstant = (screenWidth - height) / 2;
_loginLeftConstraint.constant = _loginRightConstraint.constant = spacingConstant;
[UIView animateWithDuration: 0.15 delay: 0 options: UIViewAnimationOptionCurveEaseOut animations: ^{
[self.view layoutIfNeeded];
} completion: nil];
}
///放大動畫
- (void)animateToMakeButtonBig {
_loginLeftConstraint.constant = _loginRightConstraint.constant = 0;
[UIView animateWithDuration: 0.15 delay: 0 options: UIViewAnimationOptionCurveEaseOut animations: ^{
[self.view layoutIfNeeded];
} completion: nil];
}
我們通過了計算左右間隔設置好了登錄按鈕的動畫向拆,這很好,但是我們想想才写,上面的動畫實現(xiàn)思路是:
獲取按鈕高度和父視圖寬度 -> 計算按鈕左右間隔 -> 實現(xiàn)動畫
可是我們最開始的實現(xiàn)思路是什么奖蔓?
按鈕變小讹堤,保持寬高一致 -> 按鈕居中 -> 實現(xiàn)動畫
這說起來有些荒謬,雖然動畫實現(xiàn)了洲守,但是這并不應該是我們的實現(xiàn)方式沾凄。因此知允,為了保證我們的思路能夠正確執(zhí)行,我們的操作步驟應該如下:
1保屯、移除登錄按鈕左右約束
2涤垫、添加寬高等比約束
3、添加按鈕相對于父視圖的居中約束
在執(zhí)行這些步驟之前蝠猬,我們先來看看關于約束的計算公式以及這些計算的變量在NSLayoutConstraint
對象中代表的屬性:
根據(jù)這個公式假設我現(xiàn)在要給當前視圖上的一個按鈕添加水平居中的約束榆芦,那么約束的創(chuàng)建代碼如下:
NSLayoutConstraint * centerXConstraint = [NSLayoutConstraint
constraintWithItem: _button //firstItem
attribute: NSLayoutAttributeCenterX //firstAttribute
relatedBy: NSLayoutRelationEqual //relation
toItem: _button.superview //secondItem
attribute: NSLayoutAttributeCenterX //secondAttribute
multiplier: 1.0 //multiplier
constant: 0]; //constant
我們可以通過上面這段代碼清楚的看到布局的相關屬性和代碼的對應,如果你在xcode中通過查找進入到了NSLayoutConstraint
的類文件中驻右,你還會發(fā)現(xiàn)這些屬性中只有constant
是可寫的犬绒,這意味著你沒辦法通過正常方式設置multipier
這樣的值來改變某個控件在父視圖中的寬度。盡管KVC可以做到這一點凯力,但這不應該是解決方式。
因此拗秘,我們需要通過創(chuàng)建新的約束并且移除舊的約束來實現(xiàn)登錄按鈕的動畫效果祈惶。在iOS8之前這個工作無疑是繁雜的,我們需要通過
- (void)addConstraint:(NSLayoutConstraint *)constraint;
- (void)addConstraints:(NSArray<__kindof NSLayoutConstraint *> *)constraints;
- (void)removeConstraint:(NSLayoutConstraint *)constraint;
- (void)removeConstraints:(NSArray<__kindof NSLayoutConstraint *> *)constraints;
這一系列方法來增刪約束捧请,但在iOS8之后,NSLayoutConstraint
提供了active
的BOOL類型的變量供我們提供設置約束是否有效活箕,這個值設置NO
的時候約束就失效可款。同樣我們創(chuàng)建了一個約束對象之后只需要設置active
為YES
之后就會自動生效了克蚂。因此筋讨,根據(jù)上面的公式,我們修改代碼如下:
/// 縮小按鈕
- (void)animateToMakeButtonSmall {
_loginLeftConstraint.active = NO;
_loginRightConstraint.active = NO;
//創(chuàng)建寬高比約束
NSLayoutConstraint * ratioConstraint = [NSLayoutConstraint constraintWithItem: _signInButton attribute: NSLayoutAttributeWidth relatedBy: NSLayoutRelationEqual toItem: _signInButton attribute: NSLayoutAttributeHeight multiplier: 1. constant: 0];
ratioConstraint.active = YES;
_loginRatioConstraint = ratioConstraint;
//創(chuàng)建居中約束
NSLayoutConstraint * centerXConstraint = [NSLayoutConstraint constraintWithItem: _signInButton attribute: NSLayoutAttributeCenterX relatedBy: NSLayoutRelationEqual toItem: _signInButton.superview attribute: NSLayoutAttributeCenterX multiplier: 1. constant: 0.];
centerXConstraint.active = YES;
_loginCenterXConstraint = centerXConstraint;
[UIView animateWithDuration: 0.15 delay: 0 options: UIViewAnimationOptionCurveEaseIn animations: ^{
[self.view layoutIfNeeded];
} completion: nil];
}
/// 還原按鈕
- (void)animateToMakeButtonBig {
_loginCenterXConstraint.active = NO;
_loginRatioConstraint.active = NO;
NSLayoutConstraint * leftConstraint = [NSLayoutConstraint constraintWithItem: _signInButton attribute: NSLayoutAttributeLeading relatedBy: NSLayoutRelationEqual toItem: _signInButton.superview attribute: NSLayoutAttributeLeading multiplier: 1. constant: 25];
_loginLeftConstraint = leftConstraint;
leftConstraint.active = YES;
NSLayoutConstraint * rightConstraint = [NSLayoutConstraint constraintWithItem: _signInButton attribute: NSLayoutAttributeTrailing relatedBy: NSLayoutRelationEqual toItem: _signInButton.superview attribute: NSLayoutAttributeTrailing multiplier: 1. constant: -25];
_loginRightConstraint = rightConstraint;
rightConstraint.active = YES;
[UIView animateWithDuration: 0.15 delay: 0 options: UIViewAnimationOptionCurveEaseOut animations: ^{
[self.view layoutIfNeeded];
} completion: nil];
}
增刪約束實現(xiàn)動畫時赤屋,你還要記住的是當一個約束的active
屬性被設為NO
之后蛮粮,即便我們重新將其激活,這個約束依舊是無效的莺奔,必須重新創(chuàng)建变泄。
在上面的代碼中我還添加了兩個屬性loginRatioConstraint
和loginCenterXConstraint
使其分別指向每次動畫創(chuàng)建的新約束,方便在停止動畫時使約束無效化妨蛹。當然,除了這種引用的方式狠半,我們還可以直接通過判斷約束雙方對象以及約束的屬性類型來獲取對應的約束并使其無效:
[_signInButton.constraints enumerateObjectsWithOptions: NSEnumerationReverse usingBlock: ^(__kindof NSLayoutConstraint * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
if (obj.firstItem == _signInButton && obj.firstAttribute == NSLayoutAttributeCenterX) {
obj.active = NO;
} else if (obj.firstAttribute == NSLayoutAttributeWidth && obj.secondAttribute == NSLayoutAttributeHeight) {
obj.active = NO;
}
}];
這段代碼就等同于上面的
_loginCenterXConstraint.active = NO;
_loginRatioConstraint.active = NO;
雖然使用代碼移除約束的方式更加復雜颤难,但是在我們封裝控件的時候,總是有可能用到的行嗤,所以這也是我們需要掌握的技巧。當然了飘千,這種判斷的方式也確實過于麻煩栈雳,NSLayoutConstraint
還提供了類型為字符串identifier
屬性幫助我們識別約束。在故事板中我們可以通過右側的屬性欄直接看到該屬性并且進行設置:
這樣上面的判斷代碼就可以簡化成簡單的判斷id:
static NSString * centerXIdentifier = @"centerXConstraint";
static NSString * ratioIdentifier = @"ratioIdentifier";
/// 縮小按鈕
- (void)animateToMakeButtonSmall {
......
//創(chuàng)建寬高比約束
NSLayoutConstraint * ratioConstraint = ...//create ratioConstraint
ratioConstraint.identifier = ratioIdentifier;
//創(chuàng)建居中約束
NSLayoutConstraint * centerXConstraint = ...//create centerXConstraint
centerXConstraint.identifier = centerXIdentifier;
......
}
/// 還原按鈕
- (void)animateToMakeButtonBig {
......
[_signInButton.constraints enumerateObjectsWithOptions: NSEnumerationReverse usingBlock: ^(__kindof NSLayoutConstraint * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
if ([obj.identifier isEqualToString: centerXIdentifier]) {
obj.active = NO;
} else if ([obj.identifier isEqualToString: ratioIdentifier]) {
obj.active = NO;
}
}];
......
}
尾言
距離約束動畫的開篇也有一段時間了逆济,約束動畫的制作對于我來說很長一段時間以來都是空白磺箕,直到這段時間開筆了動畫文章才接觸使用,感觸頗深松靡。同之前的文章一樣,這篇文章意味著我對約束動畫的文章的終結岛马,在本文的demo中我還摻雜了核心動畫的內容屠列,或許這也說明了我對于約束動畫使用的淺薄之處。對于動畫而言笛洛,我所了解以及掌握的太少太少,或者說思想力不足以支撐我對于動畫制作的野心沟蔑。當然狱杰,我會繼續(xù)努力追求更酷炫易用的動畫。
掌握約束應該是我們iOS開發(fā)者的必備本領仿畸。一方面,IB可視化編程在我的工作日常中已經(jīng)是離不開的簿晓,它極大的提高了我的開發(fā)效率(以往需要動畫的地方我都是純代碼創(chuàng)建控件)甥捺;另一方面,蘋果手機的尺寸難保不會繼續(xù)增加镰禾,class size
和autolayout
都是我們適配不同屏幕的最佳幫手。因此屋休,希望這篇文章能給大家?guī)砑s束使用上的更多內容备韧。
上一篇:layout動畫初體驗
下一篇:碎片動畫
本文demo
轉載請注明原文作者和文章地址