基于ClipPathLayout轉(zhuǎn)場動(dòng)畫布局的實(shí)現(xiàn)
在上篇Android中不規(guī)則形狀View的布局實(shí)現(xiàn)中講解了ClipPathLayout的使用及核心原理實(shí)現(xiàn),這篇將講解基于ClipPathLayout擴(kuò)展出來的轉(zhuǎn)場動(dòng)畫布局的實(shí)現(xiàn).
擴(kuò)展的轉(zhuǎn)場動(dòng)畫布局目前暫且有兩種,一種是針對View的切換的,一種是針對Fragment切換的.
依賴
轉(zhuǎn)場動(dòng)畫的布局存在于ClipPathLayout中,所以添加如下依賴即可
implementation 'com.yxf:clippathlayout:1.0.+'
TransitionFrameLayout
這是一個(gè)用于View轉(zhuǎn)場切換的一個(gè)布局,其繼承關(guān)系如下
TransitionFrameLayout -> ClipPathFrameLayout -> FrameLayout
其中ClipPathFrameLayout具備完全的FrameLayout的功能,并且增加了對不規(guī)則圖形的布局支持.
TransitionFrameLayout,首先這個(gè)ViewGroup的設(shè)定就是用于做場景切換的,那么其實(shí)他只需要顯示一個(gè)子View,所以在TransitionFrameLayout中修改了顯示邏輯,添加的子View只有最后一個(gè)View會獲得顯示,其他View都是GONE隱藏狀態(tài).然后既然設(shè)定如此,那么addView和setVisibility將不建議使用,這兩個(gè)方法會破壞這個(gè)ViewGroup的場景設(shè)定.然后子View的大小也要求是和TransitionFrameLayout一致的,即使用match_parent的方式,不然可能會導(dǎo)致出現(xiàn)一些不和諧的切換效果.
使用
TransitionFrameLayout 的使用非常簡單,如果需要添加或者將隱藏的View顯示出來只需要調(diào)用TransitionFrameLayout.switchView即可,這個(gè)方法將會把switchView的View顯示出來,然后將原來顯示的View隱藏.
具體的使用以簡單的兩個(gè)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="藍(lán)色界面"
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)在綠色界面在上面顯示,藍(lán)色隱藏.
如果需要將藍(lán)色界面切換出來,可以調(diào)用如下代碼.
TransitionAdapter adapter = mLayout.switchView(mBlueView);
switchView有兩個(gè)方法
@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表示動(dòng)畫擴(kuò)張,為true表示收縮.
在switchView后獲得一個(gè)adapter對象,此時(shí)藍(lán)色界面還沒有展示出來.
可以通過adapter獲得一個(gè)ValueAnimator對象或者一個(gè)Controller對象.
可以直接調(diào)用
adapter.animate();
來啟動(dòng)場景切換動(dòng)畫效果.
也可以通過
adapter.getAnimator();
獲得一個(gè)屬性動(dòng)畫,自己控制動(dòng)畫過程.
還可以獲得一個(gè)Controller對象
mController = adapter.getController();
然后通過
mController.setProgress
來控制動(dòng)畫的實(shí)現(xiàn)進(jìn)度.當(dāng)?shù)竭_(dá)1時(shí)(進(jìn)度范圍0~1),即動(dòng)畫結(jié)束時(shí),調(diào)用
adapter.finish();
來通知轉(zhuǎn)場結(jié)束了.
直接使用adapter.animate()的效果如下

也可以通過自己通過Controller控制進(jìn)度,比如關(guān)聯(lián)滑動(dòng),可以獲得如下效果

具體實(shí)現(xiàn)請自行查閱源碼.
原理
現(xiàn)在是實(shí)現(xiàn)原理時(shí)間,這個(gè)實(shí)現(xiàn)是基于ClipPathLayout實(shí)現(xiàn)的,由于ClipPathLayout具備裁剪子View實(shí)現(xiàn)任意形狀View的能力.通過這一點(diǎn),其實(shí)可以讓子View的Path信息發(fā)生變化,進(jìn)而讓子View的繪制形狀發(fā)生變化.嗯,原理就是這么簡單.
當(dāng)前做的靜態(tài)的Path形態(tài)的變化效果,如果需要做動(dòng)態(tài)的,對于Path生成器的實(shí)現(xiàn)會比較復(fù)雜,當(dāng)然動(dòng)態(tài)的Path可以增加一些貝塞爾曲線之類的,實(shí)現(xiàn)更加炫酷的效果,有興趣的同學(xué)可以自己嘗試去實(shí)現(xiàn).
實(shí)現(xiàn)這個(gè)動(dòng)畫需要解決幾個(gè)問題:
- 動(dòng)畫的擴(kuò)散點(diǎn)(收縮點(diǎn))定在哪里?
不同的擴(kuò)散點(diǎn),Path是不一樣的,但是也不能每次都重新寫一個(gè)Path吧,這樣太麻煩了,那把擴(kuò)散點(diǎn)以參數(shù)的方式傳給Path生成器嗎?這樣做,增加了Path生成器的工作,并不合適.最終想到的方式是通過矩陣變幻的方式,通過Path.transform方法生成一個(gè)新的Path,這樣就可以實(shí)現(xiàn)Path的平移和縮放效果了.
- 還有一個(gè)問題是動(dòng)畫何時(shí)停止?
擴(kuò)散肯定有個(gè)度,需要知道多大的Scale可以讓Path內(nèi)的區(qū)域覆蓋整個(gè)View.這是一個(gè)非常難的問題,如果是一個(gè)完全閉合而且里面完全填充的Path,還可以通過類似二分查找的方式找到合適的位置,但如果Path里面有鏤空的怎么辦?就像半個(gè)陰陽魚或者一個(gè)圓環(huán),在這種情況下沒有很好的辦法可以找到一個(gè)非常合適的Scale去讓擴(kuò)大后的Path覆蓋掉整個(gè)View.這里默認(rèn)將使用二分法的方式找出一個(gè)合適的區(qū)域,不過基于以上問題的存在,如果Path比較特殊可以實(shí)現(xiàn)TransitionPathGenerator接口,這個(gè)接口比普通的Path生成器多了一個(gè)方法(maxContainSimilarRange),用于確定限定范圍內(nèi)的Path可以包含的最大矩形區(qū)域,這個(gè)區(qū)域當(dāng)然最好是和外面?zhèn)鬟M(jìn)來的區(qū)域相似的,然后獲得這個(gè)區(qū)域后就可以通過計(jì)算來獲得一個(gè)合適的Scale,讓經(jīng)過變幻后的Path可以剛好覆蓋整個(gè)View.
由于直接由Path生成器獲得的Path是不能直接使用的,需要轉(zhuǎn)換,所以也有了一個(gè)適配器(TransitionAdapter),這個(gè)適配器負(fù)責(zé)將原始的Path進(jìn)行變幻,然后再交給ClipPathLayout處理.
嗯,原理大概是這樣吧!具體詳情參見源碼,還是寫的很簡單的.
TransitionFragmentContainer
這個(gè)是Fragment的容器,用于Fragment的場景切換,其繼承關(guān)系如下
TransitionFragmentContainer -> TransitionFrameLayout -> ClipPathFrameLayout -> FrameLayout
沒錯(cuò),這個(gè)是TransitionFrameLayout的子View.
使用
這個(gè)的使用就巨簡單了,將常用的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>
由于這個(gè)View是繼承于TransitionFrameLayout的,所以這個(gè)View支持替換掉默認(rèn)的適配器,可以設(shè)置默認(rèn)的動(dòng)畫時(shí)間,插值器,擴(kuò)散中心等信息.
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));
但是這個(gè)動(dòng)畫是自動(dòng)的,不支持主動(dòng)控制,所以不應(yīng)該直接獲得其Animator或者Controller對象.
切換效果如下

原理
這個(gè)大部分的實(shí)現(xiàn)還是基于TransitionFrameLayout實(shí)現(xiàn)的,TransitionFragmentContainer需要做處理的是Fragment的添加和刪除過程.
Fragment的添加和刪除過程在容器中的表現(xiàn)就是可見性的控制和View的增加刪除.
所以TransitionFragmentContainer重寫了Fragment添加和刪除所會用到的addView和removeView和removeViewAt方法.
直接的添加過程還是很簡單的,直接在addView中調(diào)用switchView即可,但是Fragment的replace過程有點(diǎn)讓人頭疼.Fragment的replace會先調(diào)用刪除然后再添加,這樣的話就有個(gè)問題,如何判斷他是replace,而不是remove或者add呢?這里使用的方法是remove的時(shí)候使用一個(gè)屬性動(dòng)畫,然后在動(dòng)畫結(jié)束才會真正的把View刪掉,如果是替換的話,還會調(diào)用到addView方法,然后在addView中取消之前remove的動(dòng)畫,并且繼承其需要remove的View,在新的addView動(dòng)畫結(jié)束時(shí)將需要remove的View刪除,寫這部分邏輯的時(shí)候栽坑里好多次,都是時(shí)序問題導(dǎo)致的.........每次都要找很久才找到問題原因..............
代碼的具體實(shí)現(xiàn)請自行查閱源碼吧!