標籤:style blog http color io os 使用 ar for
當我們對一個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中設定的值:
</pre><p class="p1"></p><pre name="code" class="objc"><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的UIView添加動畫