如何對使用了autolayout的UIView添加動畫,autolayoutuiview
當我們對一個UIView使用了autolayout自動布局之後,也就意味著我們放棄了
傳統的通過設定view的frame等方式手動的修改、確定這個view的位置、尺寸屬性。
甚至從某種程度上講,我們應該忘記view的frame屬性:它的確定不再取決於我
(手動的直接修改),而是通過我們在storyboard或者code中提供的約束條件
(constraints),通過一個自動布局引擎(蘋果為autolayout採用的是Cassowary
布局引擎,參考文檔:點擊開啟連結),計算出這個view的frame。因此我們可以
認為使用了autolayout的view的frame屬性是一個唯讀屬性。在代碼裡認為的改動
這個view的frame並不能對這個view的frame產生真正的效果(事實也確實如此)。
現在問題就來了,在以前我們經常通過對一個view的frame的修改產生view移動
的動畫效果,那麼在使用了autolayout的view世界中我們該如何?相同的效果呢?
答案是,我們“將計就計”,通過改變這個view上的某個約束constraint然後在uiview的
animation block中觸發layout來實現。
一、預期效果
下面我們以一個簡單的例子來進行詳細的說明:
如所示,整個介面都使用了autolayout,現在我們想實現這樣一個效果:
當我們點擊顯示生日的按鈕的時候,整個view向上滑動,同時向上推出一個日期
選取器(date picker),類似於點擊textfield,彈出鍵盤後整個介面為了避免被遮
住而向上移動的效果。選取完成日期後點擊生日日期按鈕或者完成按鈕整個view向
下縮回,同時date picker向下滑出可視範圍。
二、實現細節
首先來看一眼storyboard中view的層級結構:如所示,我們可以
看到,整個view的布局相當簡單,就兩級:根view和我們的date picker view,其
中date picker view包含了一個完成按鈕和系統的date picker。這樣的話,要實現
整個view和date picker view同時上移的效果,我們只需要對根view和date picker
view同時做動畫即可。
考慮如何?根view的動畫效果,這裡我們可以巧妙的通過修改根view的
bounds屬性來實現根view的上移效果。注意這裡我們需要明白view的bounds屬性
和frame屬性的區別,前者是相對於當前view的本地座標系而言的,而後者則是相
對於當前view的父view的座標系而言的。
簡單的講,frame決定了一個view相對於父view的position和size資訊。而
bounds則決定了當前view展示的內容相對於本地座標系的位置。這裡我們將view
自身的可視內容和subviews可以看做一頁紙上的內容資訊,而view本身可以看成
是一枚放於紙上的放大鏡,放大鏡的大小不一定是和紙(content size)相同大小
的。bounds屬性的作用就是確定這枚放大鏡相對於紙的位置:一個bounds =
(0, 200, 300, 300)就意味著我們要將這枚放大鏡向紙的下方移動200個points,但
放大鏡相對於父view的位置仍是保持不變的,這樣給我們的效果就是這個view(顯
示的內容)向上移動了200個points.
改動bounds的origin屬性並不會改動這個view的frame,通過這種展示內容的
移動給我們產生一種view向上移動了的幻覺。如中,“哪個位置...”為成為我們
放大鏡中看到的第一行。
根view上移動畫的效果解決了,下面我們再來看日期選擇器date picker,在
storyboard中對其增加的約束如下:定高207、trailing/leading/top相對於super
view (根view)的位置。
確定date picker view y軸方向上下移動的約束顯然是top約束,點開top約束,
可以看到該約束的詳細內容:
一個約束可以描述為:firstItem.attributeA = secondItem.attributeB * multipler
+ constant。
結合我們可以得出date picker view的top約束為
datePickerView.Top = topLayoutGuide.bottom * 1 + 400
我們可以通過修改這裡的constant值來修改這個top約束以達到預期效果,事實
上通過修改而不是刪除舊的constraint再添加新的constraint也正是蘋果所推薦的,
在NSLayoutConstraint.h標頭檔中有如下說明:
這樣,date picker view的上下移動就可以通過擷取並修改其top約束來實現。
需要注意的是在代碼中擷取datepicker view的top約束實際上是要在其父view的
constraints數組中尋找,這是因為每個view的constraints數組中儲存的實際上是
layout 子view所需的約束的集合。
我們還要定義個輔助BOOL變數,已判斷date picker view是否以彈出:
<span style="font-size:18px;">@property (nonatomic, assign) BOOL hasShowPickerView;</span>
接下來定義一個輔助函數,用於尋找date picker view的top約束並修改其
constant屬性為給定的值:
- (void)replacePickerContainerViewTopConstraintWithConstant:(CGFloat)constant{ for (NSLayoutConstraint *constraint in self.pickerContainerView.superview.constraints) { if (constraint.firstItem == self.pickerContainerView && constraint.firstAttribute == NSLayoutAttributeTop) { constraint.constant = constant; } }}
代碼裡我們在picker container view (即文中的date picker view)的
superview的constraints屬性中尋找,如果發現firstItem和firstAttribute屬性分別是
date picker view和top,則該constraint即為目標約束,然後修改其constant屬性。
在view首次被載入的時候我們想確保date picker view 處於整個view的最底部即隱
藏的狀態,因而我們在viewcontroller的viewDidLoad方法中調用輔助方法修改一下
date picker view的top約束:
<span style="font-size:18px;">[self replacePickerContainerViewTopConstraintWithConstant:self.view.frame.size.height];</span>
在首次點擊birthday button的時候動畫修改根view的bounds和date picker
view的top constraint,注意上移gap的計算。再次點擊birthday button的時候將根
view的bounds恢複到正常值,date picker view的top constraint也恢複到viewDidLoad
中設定的值:
<span style="font-size:18px;">- (IBAction)didTapOnBirthdayButton:(id)sender{ self.hasShowPickerView = !self.hasShowPickerView; if (self.hasShowPickerView) { CGRect birthdayButtonFrame = self.birthdayButton.frame; birthdayButtonFrame = [self.view convertRect:birthdayButtonFrame fromView:self.birthdayButton.superview]; CGFloat birthdayButtonYOffset = birthdayButtonFrame.origin.y + birthdayButtonFrame.size.height; CGFloat gap = birthdayButtonYOffset - (self.view.frame.size.height - self.pickerContainerView.frame.size.height); CGRect bounds = self.view.bounds; if (gap > 0) { bounds.origin.y = gap; } else { gap = 0; } [self replacePickerContainerViewTopConstraintWithConstant:birthdayButtonYOffset]; [UIView animateWithDuration:0.25 animations:^{ self.view.bounds = bounds; [self.view layoutIfNeeded]; }]; } else { [self replacePickerContainerViewTopConstraintWithConstant:self.view.frame.size.height]; CGRect bounds = self.view.bounds; bounds.origin.y = 0; [UIView animateWithDuration:0.25 animations:^{ self.view.bounds = bounds; [self.view layoutIfNeeded]; }]; }}</span>
上述代碼中的[self.view layoutIfNeed]去掉也是沒問題的。可能比較費解的是
根view.bounds.origin.y的上移gap的計算以及top constraint的constant值的計算,
關鍵實在真正理解view的frame和bounds的意義。
至此程式達到了預期的效果。
三、小結
在使用autolayout之前我們寫程式控制介面的構成就好比是開一輛手動擋的汽
車,雖然頻繁換擋(修改frame)很繁瑣,卻也很享受那種可以完全控制汽車檔位的
自由感。使用了autolayout之後則一下子升級為了自動擋汽車,切換檔位的活不再由
我們直接操作,而只能通過油門(constraints)的大小來間接的改變汽車的檔位。在
自動擋汽車裡,我們必須要放棄直接控制檔位的想法,那是不可能的了,我們必須要
學會通過熟練掌握腳下的油門和刹車來控制車速!在習慣了自動擋之後,相信大家也
一樣能夠得心應手的做自己想做的事情。
使用AutoLayout的時怎實現動畫
先解除約束,然後添加約束,然後layoutIfNeeded
可以同時執行多個UIView動畫?
可以用CABasicAnimation來給CALayer做動畫Cocos2D的動畫還是基於layer的