基於ClipPathLayout轉場動畫布局的實現
基於ClipPathLayout轉場動畫布局的實現
在上篇 Android中不規則形狀View的佈局實現 中講解了ClipPathLayout的使用及核心原理實現,這篇將講解基於ClipPathLayout擴展出來的轉場動畫布局的實現.
擴充套件的轉場動畫布局目前暫且有兩種,一種是針對View的切換的,一種是針對Fragment切換的.
依賴
轉場動畫的佈局存在於ClipPathLayout中,所以新增如下依賴即可
implementation 'com.yxf:clippathlayout:1.0.+'
TransitionFrameLayout
這是一個用於View轉場切換的一個佈局,其繼承關係如下
TransitionFrameLayout -> ClipPathFrameLayout -> FrameLayout
其中ClipPathFrameLayout具備完全的FrameLayout的功能,並且增加了對不規則圖形的佈局支援.
TransitionFrameLayout,首先這個ViewGroup的設定就是用於做場景切換的,那麼其實他只需要顯示一個子View,所以在TransitionFrameLayout中修改了顯示邏輯,新增的子View只有最後一個View會獲得顯示,其他View都是GONE隱藏狀態.然後既然設定如此,那麼addView和setVisibility將不建議使用,這兩個方法會破壞這個ViewGroup的場景設定.然後子View的大小也要求是和TransitionFrameLayout一致的,即使用match_parent的方式,不然可能會導致出現一些不和諧的切換效果.
使用
TransitionFrameLayout 的使用非常簡單,如果需要新增或者將隱藏的View顯示出來只需要呼叫TransitionFrameLayout.switchView即可,這個方法將會把switchView的View顯示出來,然後將原來顯示的View隱藏.
具體的使用以簡單的兩個TextView為例
<com.yxf.clippathlayout.transition.TransitionFrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/blue_view" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#880000ff" android:gravity="center" android:text="藍色介面" android:textSize="30sp" /> <TextView android:id="@+id/green_view" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#8800ff00" android:gravity="center" android:text="綠色介面" android:textSize="30sp" /> </com.yxf.clippathlayout.transition.TransitionFrameLayout>
mLayout = (TransitionFrameLayout) inflater.inflate(R.layout.fragment_view_transition, null);
現在綠色介面在上面顯示,藍色隱藏.
如果需要將藍色介面切換出來,可以呼叫如下程式碼.
TransitionAdapter adapter = mLayout.switchView(mBlueView);
switchView有兩個方法
@Override public TransitionAdapter switchView(View view) { return switchView(view, false); } /** * if you want add a view , just invoke switchView directly , * do not invoke addView , it may cause some problem . * * @param view * @return */ @Override public TransitionAdapter switchView(final View view, boolean reverse) { //................. }
reverse為false表示動畫擴張,為true表示收縮.
在switchView後獲得一個adapter物件,此時藍色介面還沒有展示出來.
可以通過adapter獲得一個ValueAnimator物件或者一個Controller物件.
可以直接呼叫
adapter.animate();
來啟動場景切換動畫效果.
也可以通過
adapter.getAnimator();
獲得一個屬性動畫,自己控制動畫過程.
還可以獲得一個Controller物件
mController = adapter.getController();
然後通過
mController.setProgress
來控制動畫的實現進度.當到達1時(進度範圍0~1),即動畫結束時,呼叫
adapter.finish();
來通知轉場結束了.
直接使用adapter.animate()的效果如下

image
也可以通過自己通過Controller控制進度,比如關聯滑動,可以獲得如下效果

image
具體實現請自行查閱原始碼.
原理
現在是實現原理時間,這個實現是基於ClipPathLayout實現的,由於ClipPathLayout具備裁剪子View實現任意形狀View的能力.通過這一點,其實可以讓子View的Path資訊發生變化,進而讓子View的繪製形狀發生變化.嗯,原理就是這麼簡單.
當前做的靜態的Path形態的變化效果,如果需要做動態的,對於Path生成器的實現會比較複雜,當然動態的Path可以增加一些貝塞爾曲線之類的,實現更加炫酷的效果,有興趣的同學可以自己嘗試去實現.
實現這個動畫需要解決幾個問題:
- 動畫的擴散點(收縮點)定在哪裡?
不同的擴散點,Path是不一樣的,但是也不能每次都重新寫一個Path吧,這樣太麻煩了,那把擴散點以引數的方式傳給Path生成器嗎?這樣做,增加了Path生成器的工作,並不合適.最終想到的方式是通過矩陣變幻的方式,通過Path.transform方法生成一個新的Path,這樣就可以實現Path的平移和縮放效果了.
- 還有一個問題是動畫何時停止?
擴散肯定有個度,需要知道多大的Scale可以讓Path內的區域覆蓋整個View.這是一個非常難的問題,如果是一個完全閉合而且裡面完全填充的Path,還可以通過類似二分查詢的方式找到合適的位置,但是這種方式會非常耗費時間,計算量比較大,如果Path如果是裡面有鏤空的怎麼辦?就像半個陰陽魚或者一個圓環,在這種情況下沒法很好的辦法可以找到一個非常合適的Scale去讓擴大後的Path覆蓋掉整個View.基於以上問題的存在,這裡決定把這個位置確定的過程交給Path生成器來做(呵呵呵,其實是寫演算法是非常麻煩的事情,不想寫罷了,再其實是寫不出來).所以轉場動畫的Path生成器(TransitionPathGenerator)比普通的Path生成器多了一個方法(maxContainSimilarRange),用於確定限定範圍內的Path可以包含的最大矩形區域,這個區域當然最好是和外面傳進來的區域相似的,不相似其實也沒關係,關鍵是得有,然後獲得這個區域後就可以通過計算來獲得一個合適的Scale,讓經過變幻後的Path可以剛好覆蓋整個View.
由於直接又Path生成器獲得的Path是不能直接使用的,需要轉換,所以也有了一個介面卡(TransitionAdapter),這個介面卡負責將原始的Path進行變幻,然後再交給ClipPathLayout處理.
嗯,原理大概是這樣吧!具體詳情參見原始碼,還是寫的很簡單的.
TransitionFragmentContainer
這個是Fragment的容器,用於Fragment的場景切換,其繼承關係如下
TransitionFragmentContainer -> TransitionFrameLayout -> ClipPathFrameLayout -> FrameLayout
沒錯,這個是TransitionFrameLayout的子View.
使用
這個的使用就巨簡單了,將常用的Fragment容器FrameLayout在xml中替換成TransitionFragmentContainer即可
<com.yxf.clippathlayout.transition.TransitionFragmentContainer xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/fragment_container" android:layout_width="match_parent" android:layout_height="match_parent" app:layout_behavior="@string/appbar_scrolling_view_behavior" tools:context=".MainActivity" tools:showIn="@layout/app_bar_main"> </com.yxf.clippathlayout.transition.TransitionFragmentContainer>
由於這個View是繼承於TransitionFrameLayout的,所以這個View支援替換掉預設的介面卡,可以設定預設的動畫時間,插值器,擴散中心等資訊.
mContainer = findViewById(R.id.fragment_container); RandomTransitionPathGenerator generator = new RandomTransitionPathGenerator(new CircleTransitionPathGenerator()); generator.add(new OvalTransitionPathGenerator()); generator.add(new RhombusTransitionPathGenerator()); mContainer.setAdapter(new TransitionAdapter(generator));
但是這個動畫是自動的,不支援主動控制,所以不應該直接獲得其Animator或者Controller物件.
切換效果如下

image
原理
這個大部分的實現還是基於TransitionFrameLayout實現的,TransitionFragmentContainer需要做處理的是Fragment的新增和刪除過程.
Fragment的新增和刪除過程在容器中的表現就是可見性的控制和View的增加刪除.
所以TransitionFragmentContainer重寫了Fragment新增和刪除所會用到的addView和removeView和removeViewAt方法.
直接的新增過程還是很簡單的,直接在addView中呼叫switchView即可,但是Fragment的replace過程有點讓人頭疼.Fragment的replace會先呼叫刪除然後再新增,這樣的話就有個問題,如何判斷他是replace,而不是remove或者add呢?這裡使用的方法是remove的時候使用一個屬性動畫,然後在動畫結束才會真正的把View刪掉,如果是替換的話,還會呼叫到addView方法,然後在addView中取消之前remove的動畫,並且繼承其需要remove的View,在新的addView動畫結束時將需要remove的View刪除,寫這部分邏輯的時候栽坑裡好多次,都是時序問題導致的.........每次都要找很久才找到問題原因...心酸...........
程式碼的具體實現請自行查閱原始碼吧!