LayoutTransiton實現簡單的錄製按鈕

saka發表於2019-02-28

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

LayoutTransiton實現簡單的錄製按鈕

可以看到是一個非常簡單的動畫效果,為了方便使用,我把他做成了aar併發布到了jCenter,整合方式:

compile `com.rangaofei:sakarecordview:0.0.2`
複製程式碼

元件裡用到的庫也非常簡單,包括databinding,屬性動畫和layouttransition。通過這個簡單的庫簡單的介紹一下LayoutTransition的用法,其中也會插入一些簡單的databinding和屬性動畫的知識點,遇到困難請自行解決。

使用方法:
在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屬性是列舉型別,用來表示時間表示形式:

HHMMSS      00:00:00
MMSS        00:00
HH_MM_SS    00-00-00
MM_SS       00-00
複製程式碼
//更新時間
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)來設定自己的過渡效果。能夠出發動畫的情況有兩種:

  1. item新增(設定View.VISIBLE也可)
  2. 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分析

LayoutTransiton實現簡單的錄製按鈕

左邊的開始和暫停按鈕是一個checkbox實現的,通過一個簡單的selector來切換圖片,並在右側佈局出現和消失的時候有一個縮放動畫。我們可以通過設定一個簡單的ObjectAnimator監聽器來實現這個縮放:

 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是自定一個一個類,用來設定顯示的圖片和時間,並儲存縮放的狀態:

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值:

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}" />

複製程式碼

這樣就基本完成了動畫操作,然後暴露一些介面即可:


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對應關係等等。

預設的方法和變數

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這個方法,可以看到其實這個啟動的延遲時間是不一樣的,對應的關係為:

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時,是一個逆向的過程。

看另個一有用的變數

private int mTransitionTypes = FLAG_CHANGE_APPEARING | FLAG_CHANGE_DISAPPEARING |
            FLAG_APPEARING | FLAG_DISAPPEARING;
複製程式碼

這個mTransitionTypes就是在後邊的執行動畫中必須使用的一個變數,它預設啟用了四種種類,只有前邊提到的FLAG_CHAGE未啟用.

開發者可控的變數

這裡集中講幾個方法:

//設定所有的動畫持續時間
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()

複製程式碼

這些方法都比較簡單。

執行流程

先看一張簡單的圖:

LayoutTransiton實現簡單的錄製按鈕

從上面的方法中可以看到,flag全都沒有啟用的話,那就沒有任何顯示或者隱藏的動畫了。
CHANGE_DISAPPEARINGCHANGE_APPEARING控制的是父view和非新新增view的動畫,APPEARINGDISAPPEARING控制的是新新增view的動畫。

mAnimateParentHierarchy這個變數控制的是是否顯示父佈局的改變動畫,所以這個必須設定為true後父佈局的CHANGE_DISAPPEARINGCHANGE_APPEARING才能有作用,設定為false後只有父佈局沒有動畫,而子控制元件中非新新增的view還是用動畫效果。

viewgroup中呼叫

addview()用來為viewroup新增一個沒有父控制元件的view,這個方法最終呼叫的是

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的顯示或者隱藏時會呼叫以下方法

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,暫不分析。

相關文章