首页 > 其他分享 >自定义Behavior实现AppBarLayout越界弹性效果

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

时间:2023-05-01 14:32:40浏览次数:51  
标签:AppBarLayout target 自定义 param Behavior child View



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



一、继承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);
        }
    }
}





可以从这里获取代码

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

  • 大小: 939.3 KB
  • 查看图片附件

标签:AppBarLayout,target,自定义,param,Behavior,child,View
From: https://blog.51cto.com/u_5454003/6238906

相关文章

  • 自定义PopupWindow动画效果
    publicclassRollActivityextendsActivity{ privateViewview; privateButtonbtn; privatePopupWindowmPopupWindow; privateView[]btns;/**Calledwhentheactivityisfirstcreated.*/@OverridepublicvoidonCreate(BundlesavedInstan......
  • Android提高第十八篇之自定义PopupWindow实现的Menu(TabMenu)
    用过UCWEB-Android版的人都应该对其特殊的menu有印象,把menu做成Tab-Menu(支持分页的Menu),可以容纳比Android传统的menu更丰富的内容(Android的menu超过6项则缩略在[更多]里),本文参考网上的例子(作者:CoffeeCole,email:[email protected]),对例子进行简化以及封装,使其作为一个复......
  • 【web 开发基础】PHP自定义回调函数之call_user_func_array()
    前言从上一篇文章中我们了解到,回调函数是将一个函数作为参数传递到调用的函数中。如果在函数的格式说明中出现callback类型的参数,则该函数就是回调函数。虽然可以使用变量函数去声明自己的回调函数,不过我们通常大多还是会通过借助 call_user_func_array() 函数去实现。通过借助......
  • 自定义快捷键
    问题:复制粘贴的快捷键是CtrlC和CtrlV,在现实中粘贴值到可见单元格的用处更大,如何将这一功能自定义成快捷键?解决:【文件】》【选项】》【自定义功能区】 输入命令“粘贴值”,点击【请按新快捷键】,依次按下指定的快捷键(假设为Ctrl+Shift+V),点击【指定】 据此法,可以自定义任意命......
  • Excel 使用VBA 自定义函数
     启用Excel开发工具    打开Excel的VBA(ALT+F11)   新键VBA工程模块写入自定义函数FunctionHexIPAddr(strIPAddrAsString,isAscAsBoolean)AsStringDimarry,bit0AsString,bit1AsString,bit2AsString,bit3As......
  • Typora自定义图片图床服务器
    0x01启用picgo文件-偏好设置-图像-上传服务设定-PicGo-core(commandline)0x02安装插件打开路径C:\Users\你的用户名\.picgo(其他环境自己百度吧,我这是Windows),然后输入命令(得确保PC已有Node环境,不然npm报没有命令):npminstallpicgo-plugin-web-uploader0x02服务器返回接......
  • chipyard——自定义配置生成和前仿
    一,生成配置前面用rocket-chip仓库做了生成和前仿,为了方便扩展外设,这里转到chipyard仓库。首先我们生成一个之前用的配置: 为删SimDTM(我的测试框架不需要),先在rocket的subsystem/config下创建一个class: 然后在chipyard顶层创建config: makeCONFIG=MyConfig创建设计 发......
  • nginx自定义指定加载配置
    进入 /usr/local/nginx/conf/include目录,创建 nginx.node.conf文件,在里面输入如下代码:upstreamnodejs{server127.0.0.1:3000;#server127.0.0.1:3001;keepalive64;}server{listen80;server_namewww.penguu.compenguu.com;access_lo......
  • 如何自定义starter
    背景使用过SpringBoot的小伙伴都应该知道,一个SpringBoot项目就是由一个一个starter组成的,一个starter代表该项目的SpringBoot启动依赖,除了官方已有的starter,我们可以根据自己的需要自定义新的starter。我们经常会看到或者使用到各种***-starter。比如下面几种:spring-boo......
  • Spring 实现自定义 bean 的扩展
    Springmvc提供了扩展xml的机制,用来编写自定义的xmlbean,例如dubbo框架,就利用这个机制实现了好多的dubbobean,比如 <dubbo:application>、<dubbo:registry> 等等,只要安装这个标准的扩展方式实现配置即可。扩展自定义bean的意义何在假设我们要使用一个开源框架或者一套......