最近公司要做的項目中要求實現一個簡單的視頻錄制功能的組件,我簡單設計了一個,主要功能就是開始,暫停,停止和顯示錄制時間長度。首先看一下效果圖:

可以看到是一個非常簡單的動畫效果,為了方便使用,我把他做成了aar并發布到了jCenter,集成方式:
1
|
compile 'com.rangaofei:sakarecordview:0.0.2' |
組件里用到的庫也非常簡單,包括databinding,屬性動畫和layouttransition。通過這個簡單的庫簡單的介紹一下LayoutTransition的用法,其中也會插入一些簡單的databinding和屬性動畫的知識點,遇到困難請自行解決。
使用方法: 在xml文件中添加自定義控件:
1
2
3
4
5
6
7
|
< com.hanlinbode.sakarecordview.RecordView android:id = "@+id/rv_saka" android:layout_width = "wrap_content" android:layout_height = "wrap_content" android:layout_alignParentBottom = "true" android:layout_margin = "30dp" app:record_view_time_string = "HHMMSS" /> |
record_view_time_string
屬性是枚舉類型,用來表示時間表示形式:
HHMMSS 00:00:00
MMSS 00:00
HH_MM_SS 00-00-00
MM_SS 00-00
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
//更新時間 void updateTime( long ) /*設置監聽器, void onInitial(); void onStartRecord(); void onPauseRecord(); void onResumeRecord(); void onStopRecord();*/ void setRecordListener(RecordListener) void setDebug( boolean ) |
LayoutTransition簡介
來源于官方文檔
LayoutTransition能夠在viewgroup的布局發生變化時產生一個動畫效果。可以通過 ViewGroup.setLayoutTransition(LayoutTransition transition)
來設置過度效果。調用這個方法將會使用內置的過渡動畫(alpha值變化,xy位置變化等),開發者可用通過`LayoutTransition.setAnimator(int transitionType,Animator animator)來設置自己的過渡效果。能夠出發動畫的情況有兩種:
- item添加(設置View.VISIBLE也可)
- item移除(設置View.GON也可)
當viewgroup中發生上述兩種行為時,或者由于添加刪除而引起其他item變化,都會觸發動畫。
過渡動畫的觸發種類
這個種類指的是在發生某種行為時(例如item添加或者刪除),共有5種: CHANGE_APPEARING,CHANGE_DISAPPERING,APPEARING,DISAPPEARING,CHANGING
。每種狀態有自己的一個位標記。
CHANGE_APPEARING
指示動畫將會在新的控件添加到viewgroup中的時候引起其他view變化觸發。它的標志位是0x01。也就是當addview或者將非VISIBLE狀態的view設置為VISIBILE狀態時其他的view被影響到時也會觸發。
CHANGE_DISAPPEARING
指示動畫將會在viewgroup刪除控件的時候引起其他view變化觸發,它的標志位是0x02。也就是當removeview或者將VISIBLE狀態的view設置為非VISIBLE狀態時其他的view被影響到也會觸發。
APPEARING
當新的view添加到viewgroup中的時候觸發。它的標志位是0x04。也就是當addview或者將非VISIBLE狀態的view設置為VISIBILE狀態時會觸發。
DISAPPERAING
指示動畫將會在viewgroup刪除控件時觸發,它的標志位是0x08。也就是當removeview或者將VISIBLE狀態的view設置為非VISIBLE狀態時會觸發。
CHANGING
出去前邊的四種,當布局發生變化時會觸發動畫。它的標志位是0x10。這個標志位默認是不激活的,但是可以通過enableTransitonType(int)來激活。
了解了這些,這個庫基本就能實現了。
RecordView分析
左邊的開始和暫停按鈕是一個checkbox實現的,通過一個簡單的selector來切換圖片,并在右側布局出現和消失的時候有一個縮放動畫。我們可以通過設置一個簡單的ObjectAnimator監聽器來實現這個縮放:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
ObjectAnimator animShow = ObjectAnimator.ofFloat( null , "scaleX" , 0 , 1 ); animShow.setInterpolator( new OvershootInterpolator()); animShow.addUpdateListener( new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { if (isDebug()) { Log.e(TAG, "show anim value=" + ( float ) animation.getAnimatedValue()); } recordState.setPlayScale( 1 + ( float ) animation.getAnimatedValue() / 5 ); } }); layoutTransition.setAnimator(LayoutTransition.APPEARING, animShow); ObjectAnimator animHide = ObjectAnimator.ofFloat( null , "alpha" , 1 , 0 ); animHide.addUpdateListener( new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { if (isDebug()) { Log.e(TAG, "hide anim value=" + ( float ) animation.getAnimatedValue()); } recordState.setPlayScale( 1 + ( float ) animation.getAnimatedValue() / 5 ); } }); layoutTransition.addTransitionListener( this ); layoutTransition.setAnimator(LayoutTransition.DISAPPEARING, animHide); binding.rootView.setLayoutTransition(layoutTransition); binding.rootContainer.setLayoutTransition(layoutTransition); |
record是自定一個一個類,用來設置顯示的圖片和時間,并保存縮放的狀態:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
public class RecordState extends BaseObservable implements Parcelable { private boolean recording; private String time = "00:00:00" ; private float playScale = 1 ; @DrawableRes private int playDrawable; @DrawableRes private int stopDrawable; public RecordState( int playDrawable, int stopDrawable) { this .playDrawable = playDrawable; this .stopDrawable = stopDrawable; } @Bindable public boolean isRecording() { return recording; } public void setRecording( boolean recording) { this .recording = recording; notifyPropertyChanged(BR.recording); } //省略其他的getter和setter @Bindable public float getPlayScale() { return playScale; } public void setPlayScale( float playScale) { this .playScale = playScale; notifyPropertyChanged(BR.playScale); } //省略parcelable代碼 } |
這里需要提一個view的局限性,就是只能改變x或者y的縮放,不能同時改變,所以這里做了一個雙向綁定并寫了一個adapter來設置同時更改X和Y的scale值:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
public class CheckboxAttrAdapter { @BindingAdapter ( "checkListener" ) public static void setCheckBoxListener(CheckBox view, CompoundButton.OnCheckedChangeListener listener) { view.setOnCheckedChangeListener(listener); } @BindingAdapter ( "android:button" ) public static void setButton(CheckBox view, @DrawableRes int drawableId) { view.setButtonDrawable(drawableId); } @BindingAdapter ( "recordScale" ) public static void setRecordScale(CheckBox view, float scale) { view.setScaleX(scale); view.setScaleY(scale); } } |
然后在xml文件中可以直接飲用屬性:
1
2
3
4
5
6
7
8
9
10
|
< CheckBox android:id = "@+id/start" android:layout_width = "wrap_content" android:layout_height = "wrap_content" android:layout_centerVertical = "true" android:layout_marginLeft = "30dp" android:button = "@{state.playDrawable}" android:checked = "@{state.recording}" app:checkListener = "@{checkListener}" app:recordScale = "@{state.playScale}" /> |
這樣就基本完成了動畫操作,然后暴露一些接口即可:
1
2
3
4
5
6
7
8
9
10
11
12
|
public interface RecordListener { void onInitial(); void onStartRecord(); void onPauseRecord(); void onResumeRecord(); void onStopRecord(); } |
這樣就完成了一個最簡單的RecordView了。
原理探究
本人水平有限,這里只進行最簡單的一些分析。
LayoutTransition設置了一系列的默認值,這些默認值有默認的animator,animator的duration,動畫開始的延遲時間,動畫的錯開間隔,插值器,等待執行view的動畫map關系,正在顯示或者消失的view動畫的map關系,view和view的onlayoutchangelistenr對應關系等等。
默認的方法和變量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
public LayoutTransition() { if (defaultChangeIn == null ) { PropertyValuesHolder pvhLeft = PropertyValuesHolder.ofInt( "left" , 0 , 1 ); PropertyValuesHolder pvhTop = PropertyValuesHolder.ofInt( "top" , 0 , 1 ); PropertyValuesHolder pvhRight = PropertyValuesHolder.ofInt( "right" , 0 , 1 ); PropertyValuesHolder pvhBottom = PropertyValuesHolder.ofInt( "bottom" , 0 , 1 ); PropertyValuesHolder pvhScrollX = PropertyValuesHolder.ofInt( "scrollX" , 0 , 1 ); PropertyValuesHolder pvhScrollY = PropertyValuesHolder.ofInt( "scrollY" , 0 , 1 ); defaultChangeIn = ObjectAnimator.ofPropertyValuesHolder((Object) null , pvhLeft, pvhTop, pvhRight, pvhBottom, pvhScrollX, pvhScrollY); defaultChangeIn.setDuration(DEFAULT_DURATION); defaultChangeIn.setStartDelay(mChangingAppearingDelay); defaultChangeIn.setInterpolator(mChangingAppearingInterpolator); defaultChangeOut = defaultChangeIn.clone(); defaultChangeOut.setStartDelay(mChangingDisappearingDelay); defaultChangeOut.setInterpolator(mChangingDisappearingInterpolator); defaultChange = defaultChangeIn.clone(); defaultChange.setStartDelay(mChangingDelay); defaultChange.setInterpolator(mChangingInterpolator); defaultFadeIn = ObjectAnimator.ofFloat( null , "alpha" , 0f, 1f); defaultFadeIn.setDuration(DEFAULT_DURATION); defaultFadeIn.setStartDelay(mAppearingDelay); defaultFadeIn.setInterpolator(mAppearingInterpolator); defaultFadeOut = ObjectAnimator.ofFloat( null , "alpha" , 1f, 0f); defaultFadeOut.setDuration(DEFAULT_DURATION); defaultFadeOut.setStartDelay(mDisappearingDelay); defaultFadeOut.setInterpolator(mDisappearingInterpolator); } mChangingAppearingAnim = defaultChangeIn; mChangingDisappearingAnim = defaultChangeOut; mChangingAnim = defaultChange; mAppearingAnim = defaultFadeIn; mDisappearingAnim = defaultFadeOut; } |
可以看到,默認動畫持有的屬性有left、top、right、bottom、scrollY和scrollX,這里注意一下startDelay這個方法,可以看到其實這個啟動的延遲時間是不一樣的,對應的關系為:
1
2
3
4
5
|
private long mAppearingDelay = DEFAULT_DURATION; private long mDisappearingDelay = 0 ; private long mChangingAppearingDelay = 0 ; private long mChangingDisappearingDelay = DEFAULT_DURATION; private long mChangingDelay = 0 ; |
官方文檔中特別說明了:
By default, the DISAPPEARING animation begins immediately, as does the CHANGE_APPEARING animation. The other animations begin after a delay that is set to the default duration of the animations.
DISAPPEARING和CHANGE_APPEARING沒有延遲時間,其他的動畫都會有延遲300ms。這樣做的目的是為了在動畫展示的時候有一個順序展示的視覺效果,看起來更符合邏輯:
當一個item添加到viewgroup的時候,其他阿德item首先要移動來調整出一塊空白區域供新添加的item顯示,然后執行新添加的item的顯示動畫。當移除一個item時,是一個逆向的過程。
看另個一有用的變量
1
2
|
private int mTransitionTypes = FLAG_CHANGE_APPEARING | FLAG_CHANGE_DISAPPEARING | FLAG_APPEARING | FLAG_DISAPPEARING; |
這個mTransitionTypes就是在后邊的執行動畫中必須使用的一個變量,它默認激活了四種種類,只有前邊提到的FLAG_CHAGE未激活.
開發者可控的變量
這里集中講幾個方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
//設置所有的動畫持續時間 public void setDuration( long duration) //設置指定種類的動畫持續時間:CHANGE_APPEARING,CHANGE_DISAPPEARING,APPEARING,DISAPPEARRING,CHANGING public void setDuration( int transitionType, long duration) //獲取指定種類動畫的持續時間 public long getDuration( int transitionType) //設置在CHANGEINGXX狀態下時間的間隔 public void setStagger( int transitionType, long duration) //獲取在CHANGEINGXX狀態下時間的間隔 public long getStagger( int transitionType) //為指定的種類添加動畫插值器 public void setInterpolator( int transitionType, TimeInterpolator interpolator) //獲取指定的種類添加動畫插值器 public TimeInterpolator getInterpolator( int transitionType) //為指定的種類添加動畫 public void setAnimator( int transitionType, Animator animator) //設置viewgroup的屬性是否隨著view的變化而變化,比如viewgroup使用的是wrapcontent,添加view時會有一個擴張動畫 public void setAnimateParentHierarchy( boolean animateParentHierarchy) //是否正在執行引起布局改變動畫 public boolean isChangingLayout() //是否有正在執行的動畫 public boolean isRunning() //添加item public void addChild(ViewGroup parent, View child) //移除item public void removeChild(ViewGroup parent, View child) //顯示item public void showChild(ViewGroup parent, View child, int oldVisibility) //隱藏item public void hideChild(ViewGroup parent, View child, int newVisibility) //添加監聽器 public void addTransitionListener(TransitionListener listener) //移除監聽器 public void removeTransitionListener(TransitionListener listener) //獲取監聽器 public List<TransitionListener> getTransitionListeners() |
這些方法都比較簡單。
執行流程
先看一張簡單的圖:
從上面的方法中可以看到,flag全都沒有激活的話,那就沒有任何顯示或者隱藏的動畫了。 CHANGE_DISAPPEARING
和 CHANGE_APPEARING
控制的是父view和非新添加view的動畫, APPEARING
和 DISAPPEARING
控制的是新添加view的動畫。
mAnimateParentHierarchy這個變量控制的是是否顯示父布局的改變動畫,所以這個必須設置為true后父布局的 CHANGE_DISAPPEARING
和 CHANGE_APPEARING
才能有作用,設置為false后只有父布局沒有動畫,而子控件中非新添加的view還是用動畫效果。
viewgroup中調用
addview()用來為viewroup添加一個沒有父控件的view,這個方法最終調用的是
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
private void addViewInner(View child, int index, LayoutParams params, boolean preventRequestLayout){ //省略代碼 if (mTransition != null ) { // Don't prevent other add transitions from completing, but cancel remove // transitions to let them complete the process before we add to the container mTransition.cancel(LayoutTransition.DISAPPEARING); } //省略代碼 if (mTransition != null ) { mTransition.addChild( this , child); } //省略代碼 //省略代碼 } |
設置view的顯示或者隱藏時會調用以下方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
protected void onChildVisibilityChanged(View child, int oldVisibility, int newVisibility) { if (mTransition != null ) { if (newVisibility == VISIBLE) { mTransition.showChild( this , child, oldVisibility); } else { mTransition.hideChild( this , child, newVisibility); if (mTransitioningViews != null && mTransitioningViews.contains(child)) { // Only track this on disappearing views - appearing views are already visible // and don't need special handling during drawChild() if (mVisibilityChangingChildren == null ) { mVisibilityChangingChildren = new ArrayList<View>(); } mVisibilityChangingChildren.add(child); addDisappearingView(child); } } } // in all cases, for drags if (newVisibility == VISIBLE && mCurrentDragStartEvent != null ) { if (!mChildrenInterestedInDrag.contains(child)) { notifyChildOfDragStart(child); } } } |
可以看到在viewgroup中與上面圖中提到的方法調用是吻合的。
在調用ViewGroup.setLayoutTransition(LayoutTransition transition)的時候為自身設置了一個TransitionListener,這個地方加入的目的是為了緩存正在進行動畫的view,暫不分析。
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持服務器之家。
原文鏈接:https://juejin.im/post/5b231e18f265da59601958bf