基于ClipPathLayout轉(zhuǎn)場動畫布局的實現(xiàn)
在上篇Android中不規(guī)則形狀View的布局實現(xiàn)中講解了ClipPathLayout的使用及核心原理實現(xiàn),這篇將講解基于ClipPathLayout擴展出來的轉(zhuǎn)場動畫布局的實現(xiàn).
擴展的轉(zhuǎn)場動畫布局目前暫且有兩種,一種是針對View的切換的,一種是針對Fragment切換的.
依賴
轉(zhuǎn)場動畫的布局存在于ClipPathLayout中,所以添加如下依賴即可
implementation 'com.yxf:clippathlayout:1.0.+'
TransitionFrameLayout
這是一個用于View轉(zhuǎn)場切換的一個布局,其繼承關(guān)系如下
TransitionFrameLayout -> ClipPathFrameLayout -> FrameLayout
其中ClipPathFrameLayout具備完全的FrameLayout的功能,并且增加了對不規(guī)則圖形的布局支持.
TransitionFrameLayout,首先這個ViewGroup的設(shè)定就是用于做場景切換的,那么其實他只需要顯示一個子View,所以在TransitionFrameLayout中修改了顯示邏輯,添加的子View只有最后一個View會獲得顯示,其他View都是GONE隱藏狀態(tài).然后既然設(shè)定如此,那么addView和setVisibility將不建議使用,這兩個方法會破壞這個ViewGroup的場景設(shè)定.然后子View的大小也要求是和TransitionFrameLayout一致的,即使用match_parent的方式,不然可能會導(dǎo)致出現(xiàn)一些不和諧的切換效果.
使用
TransitionFrameLayout 的使用非常簡單,如果需要添加或者將隱藏的View顯示出來只需要調(diào)用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);
現(xiàn)在綠色界面在上面顯示,藍色隱藏.
如果需要將藍色界面切換出來,可以調(diào)用如下代碼.
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對象.
可以直接調(diào)用
adapter.animate();
來啟動場景切換動畫效果.
也可以通過
adapter.getAnimator();
獲得一個屬性動畫,自己控制動畫過程.
還可以獲得一個Controller對象
mController = adapter.getController();
然后通過
mController.setProgress
來控制動畫的實現(xiàn)進度.當(dāng)?shù)竭_1時(進度范圍0~1),即動畫結(jié)束時,調(diào)用
adapter.finish();
來通知轉(zhuǎn)場結(jié)束了.
直接使用adapter.animate()的效果如下
也可以通過自己通過Controller控制進度,比如關(guān)聯(lián)滑動,可以獲得如下效果
具體實現(xiàn)請自行查閱源碼.
原理
現(xiàn)在是實現(xiàn)原理時間,這個實現(xiàn)是基于ClipPathLayout實現(xiàn)的,由于ClipPathLayout具備裁剪子View實現(xiàn)任意形狀View的能力.通過這一點,其實可以讓子View的Path信息發(fā)生變化,進而讓子View的繪制形狀發(fā)生變化.嗯,原理就是這么簡單.
當(dāng)前做的靜態(tài)的Path形態(tài)的變化效果,如果需要做動態(tài)的,對于Path生成器的實現(xiàn)會比較復(fù)雜,當(dāng)然動態(tài)的Path可以增加一些貝塞爾曲線之類的,實現(xiàn)更加炫酷的效果,有興趣的同學(xué)可以自己嘗試去實現(xiàn).
實現(xiàn)這個動畫需要解決幾個問題:
- 動畫的擴散點(收縮點)定在哪里?
不同的擴散點,Path是不一樣的,但是也不能每次都重新寫一個Path吧,這樣太麻煩了,那把擴散點以參數(shù)的方式傳給Path生成器嗎?這樣做,增加了Path生成器的工作,并不合適.最終想到的方式是通過矩陣變幻的方式,通過Path.transform方法生成一個新的Path,這樣就可以實現(xiàn)Path的平移和縮放效果了.
- 還有一個問題是動畫何時停止?
擴散肯定有個度,需要知道多大的Scale可以讓Path內(nèi)的區(qū)域覆蓋整個View.這是一個非常難的問題,如果是一個完全閉合而且里面完全填充的Path,還可以通過類似二分查找的方式找到合適的位置,但如果Path里面有鏤空的怎么辦?就像半個陰陽魚或者一個圓環(huán),在這種情況下沒有很好的辦法可以找到一個非常合適的Scale去讓擴大后的Path覆蓋掉整個View.這里默認將使用二分法的方式找出一個合適的區(qū)域,不過基于以上問題的存在,如果Path比較特殊可以實現(xiàn)TransitionPathGenerator接口,這個接口比普通的Path生成器多了一個方法(maxContainSimilarRange),用于確定限定范圍內(nèi)的Path可以包含的最大矩形區(qū)域,這個區(qū)域當(dāng)然最好是和外面?zhèn)鬟M來的區(qū)域相似的,然后獲得這個區(qū)域后就可以通過計算來獲得一個合適的Scale,讓經(jīng)過變幻后的Path可以剛好覆蓋整個View.
由于直接由Path生成器獲得的Path是不能直接使用的,需要轉(zhuǎn)換,所以也有了一個適配器(TransitionAdapter),這個適配器負責(zé)將原始的Path進行變幻,然后再交給ClipPathLayout處理.
嗯,原理大概是這樣吧!具體詳情參見源碼,還是寫的很簡單的.
TransitionFragmentContainer
這個是Fragment的容器,用于Fragment的場景切換,其繼承關(guān)系如下
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支持替換掉默認的適配器,可以設(shè)置默認的動畫時間,插值器,擴散中心等信息.
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));
但是這個動畫是自動的,不支持主動控制,所以不應(yīng)該直接獲得其Animator或者Controller對象.
切換效果如下
原理
這個大部分的實現(xiàn)還是基于TransitionFrameLayout實現(xiàn)的,TransitionFragmentContainer需要做處理的是Fragment的添加和刪除過程.
Fragment的添加和刪除過程在容器中的表現(xiàn)就是可見性的控制和View的增加刪除.
所以TransitionFragmentContainer重寫了Fragment添加和刪除所會用到的addView和removeView和removeViewAt方法.
直接的添加過程還是很簡單的,直接在addView中調(diào)用switchView即可,但是Fragment的replace過程有點讓人頭疼.Fragment的replace會先調(diào)用刪除然后再添加,這樣的話就有個問題,如何判斷他是replace,而不是remove或者add呢?這里使用的方法是remove的時候使用一個屬性動畫,然后在動畫結(jié)束才會真正的把View刪掉,如果是替換的話,還會調(diào)用到addView方法,然后在addView中取消之前remove的動畫,并且繼承其需要remove的View,在新的addView動畫結(jié)束時將需要remove的View刪除,寫這部分邏輯的時候栽坑里好多次,都是時序問題導(dǎo)致的.........每次都要找很久才找到問題原因..............
代碼的具體實現(xiàn)請自行查閱源碼吧!