Android LayoutTransiton實(shí)現(xiàn)簡(jiǎn)單的錄制按鈕
最近公司要做的項(xiàng)目中要求實(shí)現(xiàn)一個(gè)簡(jiǎn)單的視頻錄制功能的組件,我簡(jiǎn)單設(shè)計(jì)了一個(gè),主要功能就是開始,暫停,停止和顯示錄制時(shí)間長(zhǎng)度。首先看一下效果圖:
可以看到是一個(gè)非常簡(jiǎn)單的動(dòng)畫效果,為了方便使用,我把他做成了aar并發(fā)布到了jCenter,集成方式:
compile 'com.rangaofei:sakarecordview:0.0.2'
組件里用到的庫(kù)也非常簡(jiǎn)單,包括databinding,屬性動(dòng)畫和layouttransition。通過這個(gè)簡(jiǎn)單的庫(kù)簡(jiǎn)單的介紹一下LayoutTransition的用法,其中也會(huì)插入一些簡(jiǎn)單的databinding和屬性動(dòng)畫的知識(shí)點(diǎn),遇到困難請(qǐng)自行解決。
使用方法: 在xml文件中添加自定義控件:
<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
屬性是枚舉類型,用來表示時(shí)間表示形式:
HHMMSS 00:00:00
MMSS 00:00
HH_MM_SS 00-00-00
MM_SS 00-00
//更新時(shí)間 void updateTime(long) /*設(shè)置監(jiān)聽器, void onInitial(); void onStartRecord(); void onPauseRecord(); void onResumeRecord(); void onStopRecord();*/ void setRecordListener(RecordListener) void setDebug(boolean)
LayoutTransition簡(jiǎn)介
來源于官方文檔
LayoutTransition能夠在viewgroup的布局發(fā)生變化時(shí)產(chǎn)生一個(gè)動(dòng)畫效果??梢酝ㄟ^ ViewGroup.setLayoutTransition(LayoutTransition transition)
來設(shè)置過度效果。調(diào)用這個(gè)方法將會(huì)使用內(nèi)置的過渡動(dòng)畫(alpha值變化,xy位置變化等),開發(fā)者可用通過`LayoutTransition.setAnimator(int transitionType,Animator animator)來設(shè)置自己的過渡效果。能夠出發(fā)動(dòng)畫的情況有兩種:
- item添加(設(shè)置View.VISIBLE也可)
- item移除(設(shè)置View.GON也可)
當(dāng)viewgroup中發(fā)生上述兩種行為時(shí),或者由于添加刪除而引起其他item變化,都會(huì)觸發(fā)動(dòng)畫。
過渡動(dòng)畫的觸發(fā)種類
這個(gè)種類指的是在發(fā)生某種行為時(shí)(例如item添加或者刪除),共有5種: CHANGE_APPEARING,CHANGE_DISAPPERING,APPEARING,DISAPPEARING,CHANGING
。每種狀態(tài)有自己的一個(gè)位標(biāo)記。
CHANGE_APPEARING
指示動(dòng)畫將會(huì)在新的控件添加到viewgroup中的時(shí)候引起其他view變化觸發(fā)。它的標(biāo)志位是0x01。也就是當(dāng)addview或者將非VISIBLE狀態(tài)的view設(shè)置為VISIBILE狀態(tài)時(shí)其他的view被影響到時(shí)也會(huì)觸發(fā)。
CHANGE_DISAPPEARING
指示動(dòng)畫將會(huì)在viewgroup刪除控件的時(shí)候引起其他view變化觸發(fā),它的標(biāo)志位是0x02。也就是當(dāng)removeview或者將VISIBLE狀態(tài)的view設(shè)置為非VISIBLE狀態(tài)時(shí)其他的view被影響到也會(huì)觸發(fā)。
APPEARING
當(dāng)新的view添加到viewgroup中的時(shí)候觸發(fā)。它的標(biāo)志位是0x04。也就是當(dāng)addview或者將非VISIBLE狀態(tài)的view設(shè)置為VISIBILE狀態(tài)時(shí)會(huì)觸發(fā)。
DISAPPERAING
指示動(dòng)畫將會(huì)在viewgroup刪除控件時(shí)觸發(fā),它的標(biāo)志位是0x08。也就是當(dāng)removeview或者將VISIBLE狀態(tài)的view設(shè)置為非VISIBLE狀態(tài)時(shí)會(huì)觸發(fā)。
CHANGING
出去前邊的四種,當(dāng)布局發(fā)生變化時(shí)會(huì)觸發(fā)動(dòng)畫。它的標(biāo)志位是0x10。這個(gè)標(biāo)志位默認(rèn)是不激活的,但是可以通過enableTransitonType(int)來激活。
了解了這些,這個(gè)庫(kù)基本就能實(shí)現(xiàn)了。
RecordView分析
左邊的開始和暫停按鈕是一個(gè)checkbox實(shí)現(xiàn)的,通過一個(gè)簡(jiǎn)單的selector來切換圖片,并在右側(cè)布局出現(xiàn)和消失的時(shí)候有一個(gè)縮放動(dòng)畫。我們可以通過設(shè)置一個(gè)簡(jiǎn)單的ObjectAnimator監(jiān)聽器來實(shí)現(xiàn)這個(gè)縮放:
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是自定一個(gè)一個(gè)類,用來設(shè)置顯示的圖片和時(shí)間,并保存縮放的狀態(tài):
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代碼 }
這里需要提一個(gè)view的局限性,就是只能改變x或者y的縮放,不能同時(shí)改變,所以這里做了一個(gè)雙向綁定并寫了一個(gè)adapter來設(shè)置同時(shí)更改X和Y的scale值:
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文件中可以直接飲用屬性:
<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}" />
這樣就基本完成了動(dòng)畫操作,然后暴露一些接口即可:
public interface RecordListener { void onInitial(); void onStartRecord(); void onPauseRecord(); void onResumeRecord(); void onStopRecord(); }
這樣就完成了一個(gè)最簡(jiǎn)單的RecordView了。
原理探究
本人水平有限,這里只進(jìn)行最簡(jiǎn)單的一些分析。
LayoutTransition設(shè)置了一系列的默認(rèn)值,這些默認(rèn)值有默認(rèn)的animator,animator的duration,動(dòng)畫開始的延遲時(shí)間,動(dòng)畫的錯(cuò)開間隔,插值器,等待執(zhí)行view的動(dòng)畫map關(guān)系,正在顯示或者消失的view動(dòng)畫的map關(guān)系,view和view的onlayoutchangelistenr對(duì)應(yīng)關(guān)系等等。
默認(rèn)的方法和變量
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; }
可以看到,默認(rèn)動(dòng)畫持有的屬性有l(wèi)eft、top、right、bottom、scrollY和scrollX,這里注意一下startDelay這個(gè)方法,可以看到其實(shí)這個(gè)啟動(dòng)的延遲時(shí)間是不一樣的,對(duì)應(yīng)的關(guān)系為:
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沒有延遲時(shí)間,其他的動(dòng)畫都會(huì)有延遲300ms。這樣做的目的是為了在動(dòng)畫展示的時(shí)候有一個(gè)順序展示的視覺效果,看起來更符合邏輯:
當(dāng)一個(gè)item添加到viewgroup的時(shí)候,其他阿德item首先要移動(dòng)來調(diào)整出一塊空白區(qū)域供新添加的item顯示,然后執(zhí)行新添加的item的顯示動(dòng)畫。當(dāng)移除一個(gè)item時(shí),是一個(gè)逆向的過程。
看另個(gè)一有用的變量
private int mTransitionTypes = FLAG_CHANGE_APPEARING | FLAG_CHANGE_DISAPPEARING | FLAG_APPEARING | FLAG_DISAPPEARING;
這個(gè)mTransitionTypes就是在后邊的執(zhí)行動(dòng)畫中必須使用的一個(gè)變量,它默認(rèn)激活了四種種類,只有前邊提到的FLAG_CHAGE未激活.
開發(fā)者可控的變量
這里集中講幾個(gè)方法:
//設(shè)置所有的動(dòng)畫持續(xù)時(shí)間 public void setDuration(long duration) //設(shè)置指定種類的動(dòng)畫持續(xù)時(shí)間:CHANGE_APPEARING,CHANGE_DISAPPEARING,APPEARING,DISAPPEARRING,CHANGING public void setDuration(int transitionType, long duration) //獲取指定種類動(dòng)畫的持續(xù)時(shí)間 public long getDuration(int transitionType) //設(shè)置在CHANGEINGXX狀態(tài)下時(shí)間的間隔 public void setStagger(int transitionType, long duration) //獲取在CHANGEINGXX狀態(tài)下時(shí)間的間隔 public long getStagger(int transitionType) //為指定的種類添加動(dòng)畫插值器 public void setInterpolator(int transitionType, TimeInterpolator interpolator) //獲取指定的種類添加動(dòng)畫插值器 public TimeInterpolator getInterpolator(int transitionType) //為指定的種類添加動(dòng)畫 public void setAnimator(int transitionType, Animator animator) //設(shè)置viewgroup的屬性是否隨著view的變化而變化,比如viewgroup使用的是wrapcontent,添加view時(shí)會(huì)有一個(gè)擴(kuò)張動(dòng)畫 public void setAnimateParentHierarchy(boolean animateParentHierarchy) //是否正在執(zhí)行引起布局改變動(dòng)畫 public boolean isChangingLayout() //是否有正在執(zhí)行的動(dòng)畫 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) //添加監(jiān)聽器 public void addTransitionListener(TransitionListener listener) //移除監(jiān)聽器 public void removeTransitionListener(TransitionListener listener) //獲取監(jiān)聽器 public List<TransitionListener> getTransitionListeners()
這些方法都比較簡(jiǎn)單。
執(zhí)行流程
先看一張簡(jiǎn)單的圖:
從上面的方法中可以看到,flag全都沒有激活的話,那就沒有任何顯示或者隱藏的動(dòng)畫了。 CHANGE_DISAPPEARING
和 CHANGE_APPEARING
控制的是父view和非新添加view的動(dòng)畫, APPEARING
和 DISAPPEARING
控制的是新添加view的動(dòng)畫。
mAnimateParentHierarchy這個(gè)變量控制的是是否顯示父布局的改變動(dòng)畫,所以這個(gè)必須設(shè)置為true后父布局的 CHANGE_DISAPPEARING
和 CHANGE_APPEARING
才能有作用,設(shè)置為false后只有父布局沒有動(dòng)畫,而子控件中非新添加的view還是用動(dòng)畫效果。
viewgroup中調(diào)用
addview()用來為viewroup添加一個(gè)沒有父控件的view,這個(gè)方法最終調(diào)用的是
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); } //省略代碼 //省略代碼 }
設(shè)置view的顯示或者隱藏時(shí)會(huì)調(diào)用以下方法
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中與上面圖中提到的方法調(diào)用是吻合的。
在調(diào)用ViewGroup.setLayoutTransition(LayoutTransition transition)的時(shí)候?yàn)樽陨碓O(shè)置了一個(gè)TransitionListener,這個(gè)地方加入的目的是為了緩存正在進(jìn)行動(dòng)畫的view,暫不分析。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Android仿京東頂部搜索框滑動(dòng)伸縮動(dòng)畫效果
這篇文章主要為大家詳細(xì)介紹了Android仿京東頂部搜索框滑動(dòng)伸縮動(dòng)畫效果,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-07-07android獲取屏幕的長(zhǎng)與寬實(shí)現(xiàn)代碼(手寫)
android中獲取屏幕的長(zhǎng)于寬,參考了網(wǎng)上有很多代碼,但結(jié)果與實(shí)際不符,如我的手機(jī)是i9000,屏幕大小是480*800px,得到的結(jié)果卻為320*533,于此問題很是疑惑,于是自己寫了幾行代碼,親測(cè)一下,效果還不錯(cuò),需要了解的朋友可以參考下2012-12-12解析Android中string-array數(shù)據(jù)源的簡(jiǎn)單使用
本篇文章是對(duì)Android中string-array數(shù)據(jù)源的使用進(jìn)行了詳細(xì)的分析介紹,需要的朋友參考下2013-06-06Android實(shí)現(xiàn)千變?nèi)f化的ViewPager切換動(dòng)畫
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)千變?nèi)f化的ViewPager切換動(dòng)畫,自定義PageTransformer實(shí)現(xiàn)個(gè)性的切換動(dòng)畫,感興趣的小伙伴們可以參考一下2016-05-05Android中如何優(yōu)雅的處理重復(fù)點(diǎn)擊實(shí)例代碼
這篇文章主要給大家介紹了關(guān)于Android中如何優(yōu)雅的處理重復(fù)點(diǎn)擊的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)各位Android開發(fā)者們具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2018-09-09Android仿天貓橫向滑動(dòng)指示器功能的實(shí)現(xiàn)
這篇文章主要介紹了Android仿天貓橫向滑動(dòng)指示器,Android開發(fā)中會(huì)有很多很新奇的交互,比如天貓商城的首頁(yè)頭部的分類,使用的是GridLayoutManager+橫向指示器實(shí)現(xiàn)的,需要的朋友可以參考下2022-08-08Android按鈕按下的時(shí)候改變顏色實(shí)現(xiàn)方法
這篇文章主要介紹了Android按鈕按下的時(shí)候改變顏色實(shí)現(xiàn)方法,有需要的朋友可以參考一下2014-01-01設(shè)備APP開發(fā)環(huán)境配置細(xì)節(jié)介紹
隨著工業(yè)自動(dòng)化的不斷發(fā)展,設(shè)備APP也越來越重要,本文就設(shè)備APP開發(fā)軟件配置細(xì)節(jié)做一個(gè)深入詳解2022-09-09flutter實(shí)現(xiàn)頁(yè)面多個(gè)webview的方案詳解
這篇文章主要為大家詳細(xì)介紹了flutter如何實(shí)現(xiàn)頁(yè)面多個(gè)webview的效果,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起了解下2023-09-09