Material Design - 自定义Behavior

作者: 红橙Darren | 来源:发表于2017-07-05 17:27 被阅读3413次

    1.概述


    自己做的项目似乎很少用到了 Material Design 的自定义 Behavior,至多就是用了一些 Material Design 的常用控件,关于 Behavior 其实都快忘记得差不多了。这几天在网上找了很多 app 发现知乎很多地方用到了这方面的知识,我们就以知乎的效果为例,效果非常简单,先上张图。

    知乎首页效果.gif

    2.效果实现


    ** 2.1 CoordinatorLayout 和 Behavior 介绍 **

    效果实现的方式其实是多种多样,不一定非得要用 Behavior ,如监听列表的滚动采用位移动画也行,但是这里我们采用自定义 Behavior,至于原理及源码分析先缓缓,到后面再去扯这里我们先实现效果。

    <?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"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context="com.darren.behaviordemo.MainActivity">
    
        <android.support.design.widget.AppBarLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content">
            <android.support.v7.widget.Toolbar
                android:id="@+id/tool_bar"
                app:title="知乎首页"
                app:titleTextColor="#FFFFFF"
                app:layout_scrollFlags="scroll|enterAlways|snap"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"/>
        </android.support.design.widget.AppBarLayout>
    
        <android.support.v7.widget.RecyclerView
            android:id="@+id/recycler_view"
            app:layout_behavior="@string/appbar_scrolling_view_behavior"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
    
    
        <android.support.design.widget.FloatingActionButton
            android:id="@+id/fab"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="bottom|end"
            android:layout_marginBottom="70dp"
            android:layout_marginEnd="16dp"
            android:layout_marginRight="16dp"
            android:src="@drawable/ic_social_share"
            app:layout_behavior="com.darren.behaviordemo.TranslationBehavior"
            app:layout_scrollFlags="scroll|enterAlways|snap" />
    
        <LinearLayout
            android:id="@+id/bottom_tab_layout"
            android:layout_width="match_parent"
            android:layout_height="?actionBarSize"
            android:layout_alignParentBottom="true"
            android:background="@android:color/white"
            app:layout_behavior="@string/bottom_sheet_behavior">
    
            <ImageView
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1"
                android:src="@drawable/ic_history_article" />
    
            <ImageView
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1"
                android:src="@drawable/ic_history_answer"
                />
    
            <ImageView
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:src="@drawable/ic_history_ebook"
                android:layout_weight="1"/>
    
            <ImageView
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:src="@drawable/ic_history_favorites"
                android:layout_weight="1" />
        </LinearLayout>
    
    </android.support.design.widget.CoordinatorLayout>
    
    

    CoordinatorLayout是什么?看一下官方的介绍:Interaction behavior plugin for child views of CoordinatorLayout。作用于 CoordinatorLayout 的子 View 的交互行为插件。一个 Behavior 实现了用户的一个或者多个交互行为,它们可能包括拖拽、滑动、快滑或者其他一些手势。Behavior 是一个顶层抽象类,其他的一些具体行为的 Behavior 都是继承自这个类。解释一下几个方法:

        /**
         * 表示是否给应用了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 的时候调用,可以重写这个方法对子View 进行重新布局
         */
        @Override
        public boolean onLayoutChild(CoordinatorLayout parent, View child, int layoutDirection) {
            return super.onLayoutChild(parent, child, layoutDirection);
        }
    

    2.2 自定义 Behavior 实现 FloatingActionBar 的动画控制

    当列表向上滑动的时候我们需要影藏 FloatingActionBar ,当向下滑动的时候显示 FloatingActionBar ,这其实就是两个位移动画关键就是怎么跟我们的自定义 Behavior 结合起来呢?上面的如果你能看懂其实应该知道怎么办了,如果实在不行但又想了解只能选择视频了,话不多说直接来。

    /**
     * description: FloatingActionButton 位移的自定义 Behavior
     * author: Darren on 2017/7/4 14:36
     * email: 240336124@qq.com
     * version: 1.0
     */
    public class TranslationBehavior extends FloatingActionButton.Behavior {
    
        /**
         * 当前是否是显示状态
         */
        private boolean mIsShow = false;
    
        public TranslationBehavior(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    
        /**
         *  当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, FloatingActionButton child, View directTargetChild, View target, int nestedScrollAxes) {
            // nestedScrollAxes 滑动关联的轴,我们只关心垂直的滑动
            return nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL || super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target, nestedScrollAxes);
        }
    
        /**
         * 进行嵌套滚动时被调用
         * @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, FloatingActionButton child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
            super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed);
            // 根据情况执行动画 一个显示 一个是影藏
            if (dyConsumed > 0) {
                if (!mIsShow) {
                    CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) child.getLayoutParams();
                    child.animate().translationY(params.bottomMargin + child.getMeasuredHeight()).setDuration(400).start();
                }
            }
    
            if (dyConsumed < 0) {
                if (mIsShow) {
                    child.animate().translationY(0).setDuration(400).start();
                    mIsShow = !mIsShow;
                }
            }
        }
    }
    

    2.3 实现底部 Tab 的动画控制

    底部的 Tab 也有位移动画难不成还要写一个自定义 Behavior ,估计应该是行得通但是已经有了一个 Behavior ,而且代码应该是差不多,所以我们可以选择在 TranslationBehavior 做一些修改。

        /**
         * 摆放子 View 的时候调用
         */
        @Override
        public boolean onLayoutChild(CoordinatorLayout parent, FloatingActionButton child, int layoutDirection) {
            mBottomTabView = parent.findViewById(R.id.bottom_tab_layout);
            return super.onLayoutChild(parent, child, layoutDirection);
        }
    
        /**
         * 进行嵌套滚动时被调用
         * @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, FloatingActionButton child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
            super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed);
            // 根据情况执行动画 一个显示 一个是影藏
            if (dyConsumed > 0) {
                if (!mIsShow) {
                    CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) child.getLayoutParams();
                    child.animate().translationY(params.bottomMargin + child.getMeasuredHeight()).setDuration(300).start();
                    // 处理底部位移动画
                    mBottomTabView.animate().translationY(mBottomTabView.getMeasuredHeight()).setDuration(300).start();
                    mIsShow = !mIsShow;
                }
            }
    
            if (dyConsumed < 0) {
                if (mIsShow) {
                    child.animate().translationY(0).setDuration(400).start();
                    // 处理底部位移动画
                    mBottomTabView.animate().translationY(0).setDuration(200).start();
                    mIsShow = !mIsShow;
                }
            }
        }
    

    这只是一个非常简单的效果,用得6可以实现一些非常复杂和炫的效果,今天算是一个简单的入门,至于源码以及原理分析敬请期待。

    所有分享大纲:Android进阶之旅 - 自定义View篇

    视频讲解地址:http://pan.baidu.com/s/1dFgpKCp

    相关文章

      网友评论

      本文标题:Material Design - 自定义Behavior

      本文链接:https://www.haomeiwen.com/subject/koeicxtx.html