Material Design 之 Behavior的使用和自定义Behavior

写在前面

Material 系列文章:
Material Design 之 Toolbar 开发实践总结
Material Design之 AppbarLayout 开发实践总结

前面两篇文章讲了Toolbar 和 AppbarLayout 相关的东西,还没看过的同学可以去看看。前面我们说过,CoordinatorLayout很强大,它可以协调子View的交互动作,那么CoordinatorLayout它是怎么协调子View的呢?其实核心就是Behavior。那么今天讲的就是这个很重要的东西-Behavior,在上面篇文章中,我们其实已经看到过Behavior这个东西了,在AppbarLayout 与NestedScrollView 联动的时候,我们为NestedScrollView设置了一个Behavior, 通过app:layout_behavior="@string/appbar_scrolling_view_behavior",它的值是一个类的全路径,这个Behavior 是Google已经为我们提供的,AppbarLayout的内部类,专门用于处理可滚动View(如:ScrollView、RecyclerView) 与AppbarLayout 联动的。那么这篇文章我们通过介绍Google提供的一些Behavior 的使用场景、使用方式和自定义Behavior 来熟悉和掌握 Behavior。

本文目录:

  • Behavior 介绍
  • BottomSheetBehavior/BottomSheetDialog 的使用
  • SwipeDissmissBehavior 的使用
  • 自定义 Behavior

正文

1,Behavior 介绍

看一下官方的介绍:Interaction behavior plugin for child views of CoordinatorLayout. 作用于CoordinatorLayout的子View的交互行为插件。一个Behavior 实现了用户的一个或者多个交互行为,它们可能包括拖拽、滑动、快滑或者其他一些手势。

Behavior 是一个顶层抽象类,其他的一些具体行为的Behavior 都是继承自这个类。它提供了几个重要的方法:

  • layoutDependsOn
  • onDependentViewChanged
  • onStartNestedScroll
  • onNestedPreScroll
  • onNestedScroll
  • onStopNestedScroll
  • onNestedScrollAccepted
  • onNestedPreFling
  • onStartNestedScroll
  • onLayoutChild

解释一下上面几个方法和它们的调用时机:

 /**
     * 表示是否给应用了Behavior 的View 指定一个依赖的布局,通常,当依赖的View 布局发生变化时
     * 不管被被依赖View 的顺序怎样,被依赖的View也会重新布局
     * @param parent
     * @param child 绑定behavior 的View
     * @param dependency   依赖的view
     * @return 如果child 是依赖的指定的View 返回true,否则返回false
     */
    @Override
    public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
        return super.layoutDependsOn(parent, child, dependency);
    }

    /**
     * 当被依赖的View 状态(如:位置、大小)发生变化时,这个方法被调用
     * @param parent
     * @param child
     * @param dependency
     * @return
     */
    @Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
        return super.onDependentViewChanged(parent, child, dependency);
    }

    /**
     *  当coordinatorLayout 的子View试图开始嵌套滑动的时候被调用。当返回值为true的时候表明
     *  coordinatorLayout 充当nested scroll parent 处理这次滑动,需要注意的是只有当返回值为true
     *  的时候,Behavior 才能收到后面的一些nested scroll 事件回调(如:onNestedPreScroll、onNestedScroll等)
     *  这个方法有个重要的参数nestedScrollAxes,表明处理的滑动的方向。
     *
     * @param coordinatorLayout 和Behavior 绑定的View的父CoordinatorLayout
     * @param child  和Behavior 绑定的View
     * @param directTargetChild
     * @param target
     * @param nestedScrollAxes 嵌套滑动 应用的滑动方向,看 {@link ViewCompat#SCROLL_AXIS_HORIZONTAL},
     *                         {@link ViewCompat#SCROLL_AXIS_VERTICAL}
     * @return
     */
    @Override
    public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild, View target, int nestedScrollAxes) {
        return super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target, nestedScrollAxes);
    }

    /**
     * 嵌套滚动发生之前被调用
     * 在nested scroll child 消费掉自己的滚动距离之前,嵌套滚动每次被nested scroll child
     * 更新都会调用onNestedPreScroll。注意有个重要的参数consumed,可以修改这个数组表示你消费
     * 了多少距离。假设用户滑动了100px,child 做了90px 的位移,你需要把 consumed[1]的值改成90,
     * 这样coordinatorLayout就能知道只处理剩下的10px的滚动。
     * @param coordinatorLayout
     * @param child
     * @param target
     * @param dx  用户水平方向的滚动距离
     * @param dy  用户竖直方向的滚动距离
     * @param consumed
     */
    @Override
    public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dx, int dy, int[] consumed) {
        super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
    }

    /**
     * 进行嵌套滚动时被调用
     * @param coordinatorLayout
     * @param child
     * @param target
     * @param dxConsumed target 已经消费的x方向的距离
     * @param dyConsumed target 已经消费的y方向的距离
     * @param dxUnconsumed x 方向剩下的滚动距离
     * @param dyUnconsumed y 方向剩下的滚动距离
     */
    @Override
    public void onNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
        super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed);
    }

    /**
     *  嵌套滚动结束时被调用,这是一个清除滚动状态等的好时机。
     * @param coordinatorLayout
     * @param child
     * @param target
     */
    @Override
    public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target) {
        super.onStopNestedScroll(coordinatorLayout, child, target);
    }

    /**
     * onStartNestedScroll返回true才会触发这个方法,接受滚动处理后回调,可以在这个
     * 方法里做一些准备工作,如一些状态的重置等。
     * @param coordinatorLayout
     * @param child
     * @param directTargetChild
     * @param target
     * @param nestedScrollAxes
     */
    @Override
    public void onNestedScrollAccepted(CoordinatorLayout coordinatorLayout, View child, View directTargetChild, View target, int nestedScrollAxes) {
        super.onNestedScrollAccepted(coordinatorLayout, child, directTargetChild, target, nestedScrollAxes);
    }

    /**
     * 用户松开手指并且会发生惯性动作之前调用,参数提供了速度信息,可以根据这些速度信息
     * 决定最终状态,比如滚动Header,是让Header处于展开状态还是折叠状态。返回true 表
     * 示消费了fling.
     *
     * @param coordinatorLayout
     * @param child
     * @param target
     * @param velocityX x 方向的速度
     * @param velocityY y 方向的速度
     * @return
     */
    @Override
    public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, View child, View target, float velocityX, float velocityY) {
        return super.onNestedPreFling(coordinatorLayout, child, target, velocityX, velocityY);
    }

    //可以重写这个方法对子View 进行重新布局
    @Override
    public boolean onLayoutChild(CoordinatorLayout parent, View child, int layoutDirection) {
        return super.onLayoutChild(parent, child, layoutDirection);
    }

以上就是Behavior的一些重要方法,当我们要自定义一个Behavior的时候,就会去重写上面的一些方法。自定义Behavior 会放在文章最后讲。对Behavior 有了一些了解后,接下来我们看一下Google给我提供了一些特殊场景的Behavior。

2,BottomSheetBehavior/BottomSheetDialog 的使用

BottomSheetBehavior 实现的效果在我们的项目中用的比较多,它就是从底部弹出一个布局,在很多的应用中,分享功能都有这样一个交互。在以前我们通常都是用PopupWindow来搞定,前面也写了一篇文章了,关于PupupWindow的使用和封装,通用PopupWindow,几行代码搞定PopupWindow弹窗,有了BottomSheetBehavior 实现起来就简单一点了。请看效果图:

bottomSheetBehavior.gif

看看怎么用BottomSheetBehavior:
1,在xml布局文件中为需要从底部弹出的布局绑定BottomSheetBehavior,代码如下:

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
   <TextView
       android:id="@+id/btn_show_bottom_sheet"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:text="显示/隐藏 BottomSheet"
       android:background="@android:color/darker_gray"
       android:textColor="@color/black"
       android:padding="10dp"
       />
 <FrameLayout
     android:id="@+id/share_view"
     app:layout_behavior="@string/bottom_sheet_behavior"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
     android:background="@android:color/white"
     android:orientation="vertical"
     app:behavior_peekHeight="0dp"
     >
     <include layout="@layout/bottom_sheet_share_dialog"/>
 </FrameLayout>

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

注意上面这行代码: app:behavior_peekHeight="0dp",peekHeight 属性是设置bottomSheet 折叠时的高度,我们设置为0表示折叠的时候完全隐藏,默认情况时显示布局的高度,布局会显示在界面,所以,如果要一开始布局不显示在界面上的话,需要将peekHeight 设置为0。也可以在代码中设置, 通过sheetBehavior.setPeekHeight(0)。

2,在代码中获取到与布局相关联的BottomSheetBehavior,设置展开与折叠的状态就可以了,BottomSheetBehavior有5种状态:

1, STATE_EXPANDED 展开状态,显示完整布局。
2,STATE_COLLAPSED 折叠状态,显示peekHeigth 的高度,如果peekHeight为0,则全部隐藏,与STATE_HIDDEN效果一样。
3,STATE_DRAGGING 拖拽时的状态
4,STATE_HIDDEN 隐藏时的状态
5,STATE_SETTLING 释放时的状态

看代码:

 View shareView = findViewById(R.id.share_view);
        //获取BottomSheetBehavior
        final BottomSheetBehavior sheetBehavior = BottomSheetBehavior.from(shareView);
        
        //设置折叠时的高度
        //sheetBehavior.setPeekHeight(BottomSheetBehavior.PEEK_HEIGHT_AUTO);
        
        //监听BottomSheetBehavior 状态的变化
        sheetBehavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
            @Override
            public void onStateChanged(@NonNull View bottomSheet, int newState) {

            }

            @Override
            public void onSlide(@NonNull View bottomSheet, float slideOffset) {

            }
        });
        //下滑的时候是否可以隐藏
        sheetBehavior.setHideable(true);
        findViewById(R.id.btn_show_bottom_sheet).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if(sheetBehavior.getState() != BottomSheetBehavior.STATE_EXPANDED){
                    sheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
                }else {
                    sheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
                }

            }
        });

代码很简单,重要的就是通过方法 sheetBehavior.setState()来改变状态,是显示还是隐藏。其他的几个方法都添加了注释,不用多讲。

2.1, BottomSheetDialog

上面说了BottomSheetBehavior, 接下来看一下BottomSheetDialog, 一看名字就知道,它就是一个Dialog,使用方法和Dialog 一样,它是对BootomSheetBehavior 进行了包装,从底部弹出一个Dialog。BottomSheetDialog 使用起来比BottomSheetBahvior更方便,效果更佳。看一下它的源码也非常简单,就是Dialog 显示的布局绑定了BottomSheeBehavior,源码如下:

 private View wrapInBottomSheet(int layoutResId, View view, ViewGroup.LayoutParams params) {
        final CoordinatorLayout coordinator = (CoordinatorLayout) View.inflate(getContext(),
                R.layout.design_bottom_sheet_dialog, null);
        if (layoutResId != 0 && view == null) {
            view = getLayoutInflater().inflate(layoutResId, coordinator, false);
        }
        FrameLayout bottomSheet = (FrameLayout) coordinator.findViewById(R.id.design_bottom_sheet);
        mBehavior = BottomSheetBehavior.from(bottomSheet);
        mBehavior.setBottomSheetCallback(mBottomSheetCallback);
        mBehavior.setHideable(mCancelable);
        if (params == null) {
            bottomSheet.addView(view);
        } else {
            bottomSheet.addView(view, params);
        }
        // We treat the CoordinatorLayout as outside the dialog though it is technically inside
        coordinator.findViewById(R.id.touch_outside).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                if (mCancelable && isShowing() && shouldWindowCloseOnTouchOutside()) {
                    cancel();
                }
            }
        });
        return coordinator;
    }

就这样一个方法,获取到Behavior,设置了一个监听状态的回调,设置了下滑可以隐藏。然后将Dialog 显示的布局添加到了绑定了BottomSheetBehavior 的ViewGroup 里。这个方法在setContent()方法被调用:

 @Override
    public void setContentView(View view) {
        super.setContentView(wrapInBottomSheet(0, view, null));
    }

    @Override
    public void setContentView(View view, ViewGroup.LayoutParams params) {
        super.setContentView(wrapInBottomSheet(0, view, params));
    }

接下来看一下使用方法,非常简单,以网易云音乐的歌单和分享UI为例:
网易云音乐歌单UI效果 如下:

网易云音乐歌单.png

来张gif图效果更清楚:

网易云音乐效果图.gif

本文通过BottomSheetDialog 实现的效果图如下:

bottomSheetDialog.gif

歌单代码如下:

private void showBottomSheetDialog(){
        BottomSheetDialog dialog = new BottomSheetDialog(this);
        View view = LayoutInflater.from(this).inflate(R.layout.bottom_sheet_dialog,null);

        handleList(view);

        dialog.setContentView(view);
        dialog.setCancelable(true);
        dialog.setCanceledOnTouchOutside(true);
        dialog.show();
    }

    private void handleList(View contentView){
        RecyclerView recyclerView = (RecyclerView) contentView.findViewById(R.id.recyclerView);
        LinearLayoutManager manager = new LinearLayoutManager(this);
        manager.setOrientation(LinearLayoutManager.VERTICAL);
        recyclerView.setLayoutManager(manager);
        MusicAdapter adapter = new MusicAdapter();
        recyclerView.setAdapter(adapter);
        adapter.setData(mockData());
        adapter.notifyDataSetChanged();
    }

分享代码如下:

/**
     * share Dialog
     */
    private void showShareDialog(){
        if(mBottomSheetDialog == null){
            mBottomSheetDialog = new BottomSheetDialog(this);
            View view = LayoutInflater.from(this).inflate(R.layout.bottom_sheet_share_dialog,null);
            mBottomSheetDialog.setContentView(view);
            mBottomSheetDialog.setCancelable(true);
            mBottomSheetDialog.setCanceledOnTouchOutside(true);
            // 解决下滑隐藏dialog 后,再次调用show 方法显示时,不能弹出Dialog
            View view1 = mBottomSheetDialog.getDelegate().findViewById(android.support.design.R.id.design_bottom_sheet);
            final BottomSheetBehavior bottomSheetBehavior = BottomSheetBehavior.from(view1);
            bottomSheetBehavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
                @Override
                public void onStateChanged(@NonNull View bottomSheet, int newState) {
                    if (newState == BottomSheetBehavior.STATE_HIDDEN) {
                        Log.i("BottomSheet","onStateChanged");
                        mBottomSheetDialog.dismiss();
                        bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
                    }
                }

                @Override
                public void onSlide(@NonNull View bottomSheet, float slideOffset) {

                }
            });
        }else{
            mBottomSheetDialog.show();
        }

    }

代码很简单,和其他普通Dialog的用法一样。值的主意的一点是这里有个bug ,那就是当你下滑隐藏了Dialog 之后,下次直接调用show方法来显示Dialog时(没有重新new 的情况下),Dialog不能显示,原因是因为BottomSheetDialog 源码中,关闭的Dialog 是依赖BottomSheetBehavior 的,当下滑隐藏的时候,BottomSheet的状态也为STATE_HIDDEN,并且同时dismiss Dialog,下次show 的时候,是没有办法显示一个状态为STATE_HIDDEN 的布局的。 网上搜了一下,有很多人都碰到过,解决方法来自这篇文章Material之Behavior实现支付宝密码弹窗 仿淘宝/天猫商品属性选择, 解决思路:获取到BottomSheetDialog 的布局,然后拿到绑定的BottomSheetBehavior,重新设置监听,在调用dismiss 方法时,我们重新设置一些Behavior 的状态。代码如下:

            // 解决下滑隐藏dialog 后,再次调用show 方法显示时,不能弹出Dialog
            View view1 = mBottomSheetDialog.getDelegate().findViewById(android.support.design.R.id.design_bottom_sheet);
            final BottomSheetBehavior bottomSheetBehavior = BottomSheetBehavior.from(view1);
            bottomSheetBehavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
                @Override
                public void onStateChanged(@NonNull View bottomSheet, int newState) {
                    if (newState == BottomSheetBehavior.STATE_HIDDEN) {
                        Log.i("BottomSheet","onStateChanged");
                        mBottomSheetDialog.dismiss();
                        bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
                    }
                }

                @Override
                public void onSlide(@NonNull View bottomSheet, float slideOffset) {

                }
            });

以上就是BottomSheetBehavior 和BottomSheetDialog 的用法。

3,SwipeDissmissBehavior 的使用

上面讲了BottomSheetBehavior 和BottomSheetDialog 的用法,接下来看另一种场景的Behavior-SwipeDissmissBehavior,叫滑动消失或者滑动关闭,这个Behavior 在我们项目中用得可能就不是很多了。有个场景就是Snackbar的使用了,Android 5.0 以上 ,增加了Snackbar提示消息,Snackbar 的Behavior 的就是 SwipeDissmissBehavior 的应用,当滑动Snackbar 的时候,Snackbar 消失,效果如下:

snackbar的behavir.gif

使用也非常简单,在代码中只接new 一个SwipeDismissBehavior,设置一些属性后,添加到CoordinatorLayout.LayoutParams,代码如下:

        mSwipeLayout = findViewById(R.id.swipe_layout);
        SwipeDismissBehavior swipe = new SwipeDismissBehavior();

        /**
         * //设置滑动的方向,有3个值
         *
         * 1,SWIPE_DIRECTION_ANY 表示向左像右滑动都可以,
         * 2,SWIPE_DIRECTION_START_TO_END,只能从左向右滑
         * 3,SWIPE_DIRECTION_END_TO_START,只能从右向左滑
         */
        swipe.setSwipeDirection(SwipeDismissBehavior.SWIPE_DIRECTION_START_TO_END);

        swipe.setStartAlphaSwipeDistance(0f);

        swipe.setSensitivity(0.2f);

        swipe.setListener(new SwipeDismissBehavior.OnDismissListener() {
            @Override
            public void onDismiss(View view) {
                Log.e(TAG,"------>onDissmiss");
            }

            @Override
            public void onDragStateChanged(int state) {
                Log.e(TAG,"------>onDragStateChanged");
            }
        });

        CoordinatorLayout.LayoutParams layoutParams = (CoordinatorLayout.LayoutParams) mSwipeLayout.getLayoutParams();
        if(layoutParams!=null){
            layoutParams.setBehavior(swipe);
        }

有两个重要的方法,wipe.setSwipeDirection设置滑动方向,有三个取值,上面已经注释,不过多解释,还有就是swipe.setListener可以监听dissmiss 和状态改变,在这些回调里面可以做一些自己的逻辑。最后效果图:

swipeDissmissBehavir.gif
4,自定义Behavior

上面讲了Google 为我们提供的一些场景使用的Behavior,当然还有一些Google 提供的一些组件使用的Behavior,AppbarLayout内部的Behavior,如专门协调 AppbarLayout 与可滚动View(NestedScrollView,RecyclerView )的, FloatActionButton内部的Behavior ,协调和Snackbar 的关系,保证Snackbar 弹出的时候不被FAB 遮挡。还有就是上面说的Snackbar内部的Behavior 等等。但是有时候,要实现多个View之间的的交互时,我们可以自定义Behavior ,下面就说说怎么自定义一个Behavior。

自定义Behavior 最关键的就是文章第一部分介绍的Behavior 提供的那一些方法,忘了的请到回去看一下第一部分的方法注释。自定义Behavior 分为两种:

  • 第一种是通过监听一个View的状态,如位置、大小的变化,来改变其他View的行为,这种只需要重写2个方法就可以了,分别是layoutDependsOnonDependentViewChanged, layoutDependsOn方法判断是指定依赖的View时,返回true,然后在onDependentViewChanged 里,被依赖的View做需要的行为动作。

  • 第二种就是重写onStartNestedScrollonNestedPreScrollonNestedScroll等一系列方法,前面第一步分已经讲过。

上面两种方法相比,第一种很简单,第二种复杂一些,但是第二种实现的效果也要复杂。下面就以开眼首页的滑动Header效果为例,来实现一个自定义的Behavior。开眼首页滑动header效果如下:


开眼首页效果.gif

效果如上:就是列表滑动的时候是覆盖Header(不是Header缩小,Header没动),然后就是Header有一个alpha 的变化。

1,首先是整个布局,Header 固定在顶部,列表在Header 的下方,CoordinatorLayout 是一个FrameLayout,不能提供这样的布局,我们需要重写onLayoutChild 来让列表位于Header下面:


    @Override
    public boolean onLayoutChild(CoordinatorLayout parent, View child, int layoutDirection) {
        Log.i(TAG,"onLayoutChild.....");
        CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) child.getLayoutParams();
        if(params!=null && params.height == CoordinatorLayout.LayoutParams.MATCH_PARENT){
            child.layout(0,0,parent.getWidth(),parent.getHeight());
            child.setTranslationY(getHeaderHeight());
            return true;
        }

        return super.onLayoutChild(parent, child, layoutDirection);
    }

我们需要知道Header的高度,将Header的高度写在dimens文件中,getHeaderHeight()方法如下:

/**
     * 获取Header 高度
     * @return
     */
    public int getHeaderHeight(){
        return MaterialDesignSimpleApplication.getAppContext().getResources().getDimensionPixelOffset(R.dimen.header_height);
    }

2,当开始滑动的时候,利用setTranslationY 来移动列表,知道完全盖住header ,这是时候,列表就不移动了,只是列表的滑动了。当下滑到顶端的时候,又将列表向下滑动,直到header 完全显示,思路就是这样。开眼的首页向上滑洞的时候,Header 有一个alpha的变化,本例子没有实现,其实也很简单,只要重写onDependentViewChanged方法,在里面根据滑动距离算出alpha 变化的值就可以了。自定义Behavior 完整代码如下:

/**
 *
 *   自定义Behavior :实现RecyclerView(或者其他可滑动View,如:NestedScrollView) 滑动覆盖header 的效果
 * Created by zhouwei on 16/12/19.
 */

public class CoverHeaderScrollBehavior extends CoordinatorLayout.Behavior<View> {
    public static final String TAG = "CoverHeaderScroll";

    public CoverHeaderScrollBehavior(Context context, AttributeSet attributeSet){
        super(context,attributeSet);
    }


    @Override
    public boolean onLayoutChild(CoordinatorLayout parent, View child, int layoutDirection) {
        Log.i(TAG,"onLayoutChild.....");
        CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) child.getLayoutParams();
        if(params!=null && params.height == CoordinatorLayout.LayoutParams.MATCH_PARENT){
            child.layout(0,0,parent.getWidth(),parent.getHeight());
            child.setTranslationY(getHeaderHeight());
            return true;
        }

        return super.onLayoutChild(parent, child, layoutDirection);
    }

    @Override
    public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild, View target, int nestedScrollAxes) {
        return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
    }

    @Override
    public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dx, int dy, int[] consumed) {
        super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
        // 在这个方法里面只处理向上滑动
        if(dy < 0){
            return;
        }

        float transY =  child.getTranslationY() - dy;
        Log.i(TAG,"transY:"+transY+"++++child.getTranslationY():"+child.getTranslationY()+"---->dy:"+dy);
        if(transY > 0){
            child.setTranslationY(transY);
            consumed[1]= dy;
        }
    }

    @Override
    public void onNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
        super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed);
        // 在这个方法里只处理向下滑动
        if(dyUnconsumed >0){
            return;
        }

        float transY = child.getTranslationY() - dyUnconsumed;
        Log.i(TAG,"------>transY:"+transY+"****** child.getTranslationY():"+child.getTranslationY()+"--->dyUnconsumed"+dxUnconsumed);
        if(transY > 0 && transY < getHeaderHeight()){
            child.setTranslationY(transY);
        }
    }

    /**
     * 获取Header 高度
     * @return
     */
    public int getHeaderHeight(){
        return MaterialDesignSimpleApplication.getAppContext().getResources().getDimensionPixelOffset(R.dimen.header_height);
    }

}

xml 的代码如下:

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
              xmlns:app="http://schemas.android.com/apk/res-auto"
              android:orientation="vertical"
              android:layout_width="match_parent"
              android:layout_height="match_parent">
   <ImageView
       android:layout_width="match_parent"
       android:layout_height="@dimen/header_height"
       android:scaleType="centerCrop"
       android:src="@drawable/meizhi"
       />
    <android.support.v4.widget.NestedScrollView
        android:id="@+id/nested_scroll_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@android:color/white"
        app:layout_behavior="@string/cover_header_behavior"
        >
        <TextView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:text="@string/large_text"
            />
    </android.support.v4.widget.NestedScrollView>
</android.support.design.widget.CoordinatorLayout>

最后实现的效果如下:

仿开眼首页效果.gif

最后

以上就是关于Behavior 的全部内容,自定义Behavior 这一块,特别是处理滑动嵌套对于刚接触的同学来说还是挺难的,不过当掌握了之后,我们能做出很多炫酷的效果。所以,再困难也值得花时间去学习。本文到此结束,如有问题,欢迎交流。所有关于Material Design 的使用示例都在这里:MaterialDesignSamples

参考资料:
1,自定义Behavior的艺术探索-仿UC浏览器主页
2,使用 CoordinatorLayout 实现复杂联动效果
3,Material之Behavior实现支付宝密码弹窗 仿淘宝/天猫商品属性选择

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容