自定义Behavior实现AppBarLayout越界弹性效果

来源:互联网 发布:js显示时间日期代码 编辑:程序博客网 时间:2024/04/20 01:31


一、继承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值@Overridepublic 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()方法的原因

@Overridepublic 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);    }}

与上滑的逻辑基本一直,所以可写为一个方法

@Overridepublic 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()方法

@Overridepublic 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;  //是否有动画@Overridepublic boolean onStartNestedScroll(CoordinatorLayout parent, AppBarLayout child, View directTargetChild, View target, int nestedScrollAxes) {    // 开始滑动时,启用动画    isAnimate = true;    return super.onStartNestedScroll(parent, child, directTargetChild, target, nestedScrollAxes);}@Overridepublic 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);        }    }}



可以从这里获取代码
  • 大小: 939.3 KB
  • 查看图片附件
0 0
原创粉丝点击