1. 程式人生 > >自定義Behavior實現AppBarLayout越界彈性效果

自定義Behavior實現AppBarLayout越界彈性效果

2328703a-f197-39f4-b432-8e7292e0f27e.gif

一、繼承AppBarLayout.Behavior

AppBarLayout有一個預設的Behavior,即AppBarLayout.Behavior,AppBarLayout.Behavior已註解的方式設定給AppBarLayout。

@CoordinatorLayout.DefaultBehavior(AppBarLayout.Behavior.class)
public class AppBarLayout extends LinearLayout {
    ...
}

1.繼承AppBarLayout.Behavior自定義Behavior

我們可以繼承AppBarLayout.Behavior並重新設定給AppBarLayout來修改AppBarLayout的預設滾動行為,實現AppBarLayout的彈性越界效果就可以通過這種方式實現。

繼承AppBarLayout.Behavior需要重寫構造方法

public class AppBarLayoutOverScrollViewBehavior extends AppBarLayout.Behavior {

    public AppBarLayoutOverScrollViewBehavior() {
    }

    public AppBarLayoutOverScrollViewBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

}


2.將自定義的Behavior設定給AppBarLayout

可以通過兩種方式將自定義的Behavior設定給AppBarLayout

在佈局檔案中設定

<android.support.design.widget.AppBarLayout
     ...
     app:layout_behavior="packageName.AppBarLayoutOverScrollViewBehavior">
 </android.support.design.widget.AppBarLayout>

在程式碼中設定

 AppBarLayout appBar = (AppBarLayout) findViewById(R.id.appbar);
 CoordinatorLayout.LayoutParams params = new CoordinatorLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
 params.setBehavior(new AppBarLayoutOverScrollViewBehavior());
 appBar.setLayoutParams(params);

設定完成後,自定義的Behavior就會生效,但是因為沒有重寫任何方法,所以AppBarLayout的滾動行為不會發生變化。

二、Behavior中的回撥方法分析

將自定義的Behavior設定給AppBarLayout後,可以在自定義的Behavior中重寫滾動相關回調方法

public class AppBarLayoutOverScrollViewBehavior extends AppBarLayout.Behavior {

    ...

    /**
     * AppBarLayout佈局時呼叫
     *
     * @param parent 父佈局CoordinatorLayout
     * @param abl 使用此Behavior的AppBarLayout
     * @param layoutDirection 佈局方向
     * @return 返回true表示子View重新佈局,返回false表示請求預設佈局
     */
    @Override
    public boolean onLayoutChild(CoordinatorLayout parent, AppBarLayout abl, int layoutDirection) {
        return super.onLayoutChild(parent, abl, layoutDirection);
    }

    /**
     * 當CoordinatorLayout的子View嘗試發起巢狀滾動時呼叫
     *
     * @param parent 父佈局CoordinatorLayout
     * @param child 使用此Behavior的AppBarLayout
     * @param directTargetChild CoordinatorLayout的子View,或者是包含巢狀滾動操作的目標View
     * @param target 發起巢狀滾動的目標View(即AppBarLayout下面的ScrollView或RecyclerView)
     * @param nestedScrollAxes 巢狀滾動的方向
     * @return 返回true表示接受滾動
     */
    @Override
    public boolean onStartNestedScroll(CoordinatorLayout parent, AppBarLayout child, View directTargetChild, View target, int nestedScrollAxes) {
        return super.onStartNestedScroll(parent, child, directTargetChild, target, nestedScrollAxes);
    }

    /**
     * 當巢狀滾動已由CoordinatorLayout接受時呼叫
     *
     * @param coordinatorLayout 父佈局CoordinatorLayout
     * @param child 使用此Behavior的AppBarLayout
     * @param directTargetChild CoordinatorLayout的子View,或者是包含巢狀滾動操作的目標View
     * @param target 發起巢狀滾動的目標View(即AppBarLayout下面的ScrollView或RecyclerView)
     * @param nestedScrollAxes 巢狀滾動的方向
     */
    @Override
    public void onNestedScrollAccepted(CoordinatorLayout coordinatorLayout, AppBarLayout child, View directTargetChild, View target, int nestedScrollAxes) {
        super.onNestedScrollAccepted(coordinatorLayout, child, directTargetChild, target, nestedScrollAxes);
    }

    /**
     * 當準備開始巢狀滾動時呼叫
     *
     * @param coordinatorLayout 父佈局CoordinatorLayout
     * @param child 使用此Behavior的AppBarLayout
     * @param target 發起巢狀滾動的目標View(即AppBarLayout下面的ScrollView或RecyclerView)
     * @param dx 使用者在水平方向上滑動的畫素數
     * @param dy 使用者在垂直方向上滑動的畫素數
     * @param consumed 輸出引數,consumed[0]為水平方向應該消耗的距離,consumed[1]為垂直方向應該消耗的距離
     */
    @Override
    public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, int dx, int dy, int[] consumed) {
        super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
    }

    /**
     * 巢狀滾動時呼叫
     *
     * @param coordinatorLayout 父佈局CoordinatorLayout
     * @param child 使用此Behavior的AppBarLayout
     * @param target 發起巢狀滾動的目標View(即AppBarLayout下面的ScrollView或RecyclerView)
     * @param dxConsumed 由目標View滾動操作消耗的水平畫素數
     * @param dyConsumed 由目標View滾動操作消耗的垂直畫素數
     * @param dxUnconsumed 由使用者請求但是目標View滾動操作未消耗的水平畫素數
     * @param dyUnconsumed 由使用者請求但是目標View滾動操作未消耗的垂直畫素數
     */
    @Override
    public void onNestedScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
        super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed);
    }

    /**
     * 當巢狀滾動的子View準備快速滾動時呼叫
     *
     * @param coordinatorLayout 父佈局CoordinatorLayout
     * @param child 使用此Behavior的AppBarLayout
     * @param target 發起巢狀滾動的目標View(即AppBarLayout下面的ScrollView或RecyclerView)
     * @param velocityX 水平方向的速度
     * @param velocityY 垂直方向的速度
     * @return 如果Behavior消耗了快速滾動返回true
     */
    @Override
    public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, float velocityX, float velocityY) {
        return super.onNestedPreFling(coordinatorLayout, child, target, velocityX, velocityY);
    }

    /**
     * 當巢狀滾動的子View快速滾動時呼叫
     *
     * @param coordinatorLayout 父佈局CoordinatorLayout
     * @param child 使用此Behavior的AppBarLayout
     * @param target 發起巢狀滾動的目標View(即AppBarLayout下面的ScrollView或RecyclerView)
     * @param velocityX 水平方向的速度
     * @param velocityY 垂直方向的速度
     * @param consumed 如果巢狀的子View消耗了快速滾動則為true
     * @return 如果Behavior消耗了快速滾動返回true
     */
    @Override
    public boolean onNestedFling(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, float velocityX, float velocityY, boolean consumed) {
        return super.onNestedFling(coordinatorLayout, child, target, velocityX, velocityY, consumed);
    }

    /**
     * 當定製滾動時呼叫
     *
     * @param coordinatorLayout 父佈局CoordinatorLayout
     * @param abl 使用此Behavior的AppBarLayout
     * @param target 發起巢狀滾動的目標View(即AppBarLayout下面的ScrollView或RecyclerView)
     */
    @Override
    public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, AppBarLayout abl, View target) {
        super.onStopNestedScroll(coordinatorLayout, abl, target);
    }
}

可以通過列印log來觀察AppBarLayout在滾動時Behavior中回撥方法的呼叫情況。

通過觀察可以發現:

上滑時
當AppBarLayout由展開到收起時,會依次呼叫onStartNestedScroll()->onNestedScrollAccepted()->onNestedPreScroll()->onStopNestedScroll()
當AppBarLayout收起後繼續向上滑動時,會依次呼叫onStartNestedScroll()->onNestedScrollAccepted()->onNestedPreScroll()->onNestedScroll()->onStopNestedScroll()
下滑時
當AppBarLayout全部展開時(即未到頂部時),會依次呼叫onStartNestedScroll()->onNestedScrollAccepted()->onNestedPreScroll()->onNestedScroll()->onStopNestedScroll()
當AppBarLayout全部展開時(即到頂部時),繼續向下滑動螢幕,會依次呼叫onStartNestedScroll()->onNestedScrollAccepted()->onNestedPreScroll()->onNestedScroll()->onStopNestedScroll()
當有快速滑動時會在onStopNestedScroll()前依次呼叫onNestedPreFling()->onNestedFling()
所以要修改AppBarLayout的越界行為可以重寫onNestedPreScroll()或onNestedScroll(),因為AppBarLayout收起時不會呼叫onNestedScroll(),所以只能選擇重寫onNestedPreScroll(),具體原因下面會有說明。

三、重寫Behavior的相關方法

1.獲取越界時需要改變尺寸的View

佈局時會呼叫onLayoutChild(),所以在該方法中可獲取需要改變尺寸的View,可以使用View的findViewWithTag方法獲取指定的View,並初始化屬性。

public class AppBarLayoutOverScrollViewBehavior extends AppBarLayout.Behavior {
    private static final String TAG = "overScroll";
    private View mTargetView;       // 目標View
    private int mParentHeight;      // AppBarLayout的初始高度
    private int mTargetViewHeight;  // 目標View的高度

    @Override
    public boolean onLayoutChild(CoordinatorLayout parent, AppBarLayout abl, int layoutDirection) {
        boolean handled = super.onLayoutChild(parent, abl, layoutDirection);
        // 需要在呼叫過super.onLayoutChild()方法之後獲取
        if (mTargetView == null) {
            mTargetView = parent.findViewWithTag(TAG);
            if (mTargetView != null) {
                initial(abl);
            }
        }
        return handled;
    }

    private void initial(AppBarLayout abl) {
        // 必須設定ClipChildren為false,這樣目標View在放大時才能超出佈局的範圍
        abl.setClipChildren(false);
        mParentHeight = abl.getHeight();
        mTargetViewHeight = mTargetView.getHeight();
    }

    ...

}

需要在佈局檔案或程式碼中給目標View指定tag,如下:

<android.support.design.widget.CoordinatorLayout 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:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true">

    <android.support.design.widget.AppBarLayout
        android:id="@+id/appbar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:fitsSystemWindows="true"
        android:theme="@style/AppTheme.AppBarOverlay"
        android:transitionName="picture"
        app:layout_behavior="com.zly.exifviewer.widget.behavior.AppBarLayoutOverScrollViewBehavior"
        tools:targetApi="lollipop">

        <android.support.design.widget.CollapsingToolbarLayout
            android:id="@+id/collapsingToolbarLayout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:contentScrim="@color/colorPrimary"
            app:layout_scrollFlags="scroll|enterAlways|enterAlwaysCollapsed"
            app:statusBarScrim="@color/colorPrimaryDark">

            <ImageView
                android:id="@+id/siv_picture"
                android:layout_width="match_parent"
                android:layout_height="200dp"
                android:fitsSystemWindows="true"
                android:foreground="@drawable/shape_fg_picture"
                android:scaleType="centerCrop"
                android:tag="overScroll"
                app:layout_collapseMode="parallax"
                tools:src="@android:drawable/sym_def_app_icon" />

            <android.support.v7.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                app:contentInsetEnd="64dp"
                app:layout_collapseMode="pin"
                app:popupTheme="@style/AppTheme.PopupOverlay" />
        </android.support.design.widget.CollapsingToolbarLayout>
    </android.support.design.widget.AppBarLayout>

    <android.support.v4.widget.NestedScrollView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">

        ...

    </android.support.v4.widget.NestedScrollView>

</android.support.design.widget.CoordinatorLayout>

2.下滑處理

重寫onNestedPreScroll()修改AppBarLayou滑動的頂部後的行為

private static final float TARGET_HEIGHT = 500; // 最大滑動距離
private float mTotalDy;     // 總滑動的畫素數
private float mLastScale;   // 最終放大比例
private int mLastBottom;    // AppBarLayout的最終Bottom值

@Override
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, int dx, int dy, int[] consumed) {
    // 1.mTargetView不為null
    // 2.是向下滑動,dy<0表示向下滑動
    // 3.AppBarLayout已經完全展開,child.getBottom() >= mParentHeight
    if (mTargetView != null && dy < 0 && child.getBottom() >= mParentHeight) {
        // 累加垂直方向上滑動的畫素數
        mTotalDy += -dy;
        // 不能大於最大滑動距離
        mTotalDy = Math.min(mTotalDy, TARGET_HEIGHT);
        // 計算目標View縮放比例,不能小於1
        mLastScale = Math.max(1f, 1f + mTotalDy / TARGET_HEIGHT);
        // 縮放目標View
        ViewCompat.setScaleX(mTargetView, mLastScale);
        ViewCompat.setScaleY(mTargetView, mLastScale);
        // 計算目標View放大後增加的高度
        mLastBottom = mParentHeight + (int) (mTargetViewHeight / 2 * (mLastScale - 1));
        // 修改AppBarLayout的高度
        child.setBottom(mLastBottom);
    } else {
        super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
    }
}

此時可以實現下滑越界時目標View放大,AppBarLayout變高的效果。

3.上滑處理

下滑時目標View放大,AppBarLayout變高,如果此時使用者不鬆開手指,直接上滑,需要目標View縮小,並且AppBarLayout變高。

預設情況下AppBarLayout的滑動是通過修改top和bottom實現的,所以上滑時,AppBarLayout為整體向上移動,高度不會發生改變,並且AppBarLayout下面的ScrollView也會向上滾動;而我們需要的是在AppBarLayout的高度大於原始高度時,減小AppBarLayout的高度,top不發生改變,並且AppBarLayout下面的ScrollView不會向上滾動。

AppBarLayout上滑時不會呼叫onNestedScroll(),所以只能在onNestedPreScroll()方法中修改,這也是為什麼選擇onNestedPreScroll()方法的原因

@Override
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, int dx, int dy, int[] consumed) {
    if (mTargetView != null && dy < 0 && child.getBottom() >= mParentHeight) {
        ...
    } else 
    // 1.mTargetView不為null
    // 2.是向上滑動,dy>0表示向下滑動
    // 3.AppBarLayout尚未恢復到原始高度child.getBottom() > mParentHeight
    if (mTargetView != null && dy > 0 && child.getBottom() > mParentHeight) {
        // 累減垂直方向上滑動的畫素數
        mTotalDy -= dy;
        // 計算目標View縮放比例,不能小於1
        mLastScale = Math.max(1f, 1f + mTotalDy / TARGET_HEIGHT);
        // 縮放目標View
        ViewCompat.setScaleX(mTargetView, mLastScale);
        ViewCompat.setScaleY(mTargetView, mLastScale);
        // 計算目標View縮小後減少的高度
        mLastBottom = mParentHeight + (int) (mTargetViewHeight / 2 * (mLastScale - 1));
        // 修改AppBarLayout的高度
        child.setBottom(mLastBottom);
        // 保持target不滑動
        target.setScrollY(0);
    } else {
        super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
    }
}

與上滑的邏輯基本一直,所以可寫為一個方法

@Override
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, int dx, int dy, int[] consumed) {
    if (mTargetView != null && ((dy < 0 && child.getBottom() >= mParentHeight) || (dy > 0 && child.getBottom() > mParentHeight))) {
        scale(child, target, dy);
    } else {
        super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
    }
}

private void scale(AppBarLayout abl, View target, int dy) {
    mTotalDy += -dy;
    mTotalDy = Math.min(mTotalDy, TARGET_HEIGHT);
    mLastScale = Math.max(1f, 1f + mTotalDy / TARGET_HEIGHT);
    ViewCompat.setScaleX(mTargetView, mLastScale);
    ViewCompat.setScaleY(mTargetView, mLastScale);
    mLastBottom = mParentHeight + (int) (mTargetViewHeight / 2 * (mLastScale - 1));
    abl.setBottom(mLastBottom);
    target.setScrollY(0);
}

4.還原

當AppBarLayout處於越界時,如果使用者鬆開手指,此時應該讓目標View和AppBarLayout都還原到原始狀態,重寫onStopNestedScroll()方法

@Override
public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, AppBarLayout abl, View target) {
    recovery(abl);
    super.onStopNestedScroll(coordinatorLayout, abl, target);
}

private void recovery(final AppBarLayout abl) {
    if (mTotalDy > 0) {
        mTotalDy = 0;
        // 使用屬性動畫還原
        ValueAnimator anim = ValueAnimator.ofFloat(mLastScale, 1f).setDuration(200);
        anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float value = (float) animation.getAnimatedValue();
                ViewCompat.setScaleX(mTargetView, value);
                ViewCompat.setScaleY(mTargetView, value);
                abl.setBottom((int) (mLastBottom - (mLastBottom - mParentHeight) * animation.getAnimatedFraction()));
            }
        });
        anim.start();
    }
}

5.優化

由於使用者在滑動時有可能觸發快速滑動,會導致在AppBarLayout收起後觸發還原動畫,重新修改AppBarLayout的Bottom,從而顯示錯誤,所以當發生快速滑動時需要禁止還原動畫,直接還原到初始狀態

private boolean isAnimate;  //是否有動畫

@Override
public boolean onStartNestedScroll(CoordinatorLayout parent, AppBarLayout child, View directTargetChild, View target, int nestedScrollAxes) {
    // 開始滑動時,啟用動畫
    isAnimate = true;
    return super.onStartNestedScroll(parent, child, directTargetChild, target, nestedScrollAxes);
}

@Override
public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, float velocityX, float velocityY) {
    // 如果觸發了快速滾動且垂直方向上速度大於100,則禁用動畫
    if (velocityY > 100) {
        isAnimate = false;
    }
    return super.onNestedPreFling(coordinatorLayout, child, target, velocityX, velocityY);
}

private void recovery(final AppBarLayout abl) {
    if (mTotalDy > 0) {
        mTotalDy = 0;
        if (isAnimate) {
            ValueAnimator anim = ValueAnimator.ofFloat(mLastScale, 1f).setDuration(200);
            anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    float value = (float) animation.getAnimatedValue();
                    ViewCompat.setScaleX(mTargetView, value);
                    ViewCompat.setScaleY(mTargetView, value);
                    abl.setBottom((int) (mLastBottom - (mLastBottom - mParentHeight) * animation.getAnimatedFraction()));
                }
            });
            anim.start();
        } else {
            ViewCompat.setScaleX(mTargetView, 1f);
            ViewCompat.setScaleY(mTargetView, 1f);
            abl.setBottom(mParentHeight);
        }
    }
}



可以從這裡獲取程式碼
  • 2328703a-f197-39f4-b432-8e7292e0f27e-thumb.gif
  • 大小: 939.3 KB