美文网首页Android 知识Android知识Android进阶
事件分发(4)-玩转自定义behavior之顶部停靠

事件分发(4)-玩转自定义behavior之顶部停靠

作者: ZJ_Rocky | 来源:发表于2017-09-23 10:23 被阅读248次

    主目录见:Android高级进阶知识(这是总目录索引)
    我们前面讲完《全面的CoordinateLayout和Behavior源码分析》的分析了。我们已经基本懂得怎么掌控behavior了。撸起袖子就是干,不多说,上图:

    效果图

    想要源码的请[点击下载]

    一.目标

    自定义behavior提供了一个优雅的方式来协调整体的运动,使整体运动变得更优雅更容易,所以我们有必要来撸一串代码,今天目标如下:
    1.复习《全面的CoordinateLayout和Behavior源码分析》的知识点;
    2.提供一个顶部停靠的参考方案。

    二.源码分享

    1.基础使用

    这个实例分为上中下三个部分,我们看下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:layout_width="match_parent"
        android:layout_height="match_parent"
        >
            <LinearLayout
                android:id="@+id/llHeadView"
                android:layout_width="match_parent"
                android:layout_height="200dp"
                android:background="@color/gray"
                android:orientation="vertical"
                app:layout_behavior="@string/HeaderBehavior"
                android:layout_gravity="center_horizontal"
                android:gravity="center_horizontal">
                <ImageView
                    android:layout_width="100dp"
                    android:layout_height="100dp"
                    android:src="@mipmap/header_icon"
                    android:layout_marginTop="30dp"
                    />
                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="ZJ_Rocky"
                    android:textColor="@color/black"
                    android:textSize="20dp"
                    android:layout_marginTop="20dp"/>
            </LinearLayout>
    
            <android.support.design.widget.TabLayout
                android:id="@+id/toolbar_tab"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                android:background="#ffffff"
                app:layout_behavior="@string/StickyBehavior"
                app:tabIndicatorColor="#0835f8"
                app:tabIndicatorHeight="2.0dp"
                app:tabSelectedTextColor="#0835f8"
                app:tabTextColor="#ced0d3"
                >
                <android.support.design.widget.TabItem
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:text="我的"
                    />
    
                <android.support.design.widget.TabItem
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:text="分享" />
    
                <android.support.design.widget.TabItem
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:text="收藏" />
    
                <android.support.design.widget.TabItem
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:text="关注" />
    
                <android.support.design.widget.TabItem
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:text="关注者" />
            </android.support.design.widget.TabLayout>
    
            <android.support.v4.view.ViewPager
                android:id="@+id/main_vp_container"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                app:layout_behavior="@string/BottomBehavior"
                />
    </android.support.design.widget.CoordinatorLayout>
    

    这个地方要注意app:layout_behavior要放在CoordinateLayout直接的子布局,并且界面要有实现了NestedScrollingChild接口的子类。不然界面自定义behavior是拦截不到滑动事件的。

    2.代码分析

    首先我们看下我们的布局结构及behavior的使用位置:


    结构图
    2.1布局

    因为我们知道CoordinateLayout跟FrameLayout是一样的,如果直接布局则会重叠在一起,需要我们自己去重新布局。在上面我们已经讲过了,behavior里面有onLayoutChild方法用来布局(跟自定义控件中的onLayout是一模一样的,因为这个其实就是在onLayout中调用的),所以我们先看下顶部(HeaderBehavior)的布局代码:

     @Override
        public boolean onLayoutChild(CoordinatorLayout parent, View child, int layoutDirection) {
            parent.onLayoutChild(child, layoutDirection);
            if (childTop < 0){
                child.offsetTopAndBottom(childTop);
            }else{
                child.offsetTopAndBottom(initOffset);
            }
            return true;
        }
    

    因为onLayoutChild可能会被执行多次,所以我们要记录下childTop的高度,不然有可能会滑动一段距离后,ViewPager切换的时候会被重置会initOffset位置。接下来我们看下中间部分(StickyBehavior)的代码:

      @Override
        public boolean onLayoutChild(CoordinatorLayout parent, View child, int layoutDirection) {
            parent.onLayoutChild(child, layoutDirection);
            View headerView = getHeaderView(parent);
            child.offsetTopAndBottom(headerView.getBottom());
            maxOffset = headerView.getHeight();
            return true;
        }
    

    中间部分也就是停靠栏的初始布局是头部的底部位置,且这个地方记录下maxOffset为头部的总高度。最后我们看下底部(BottomBehavior)的代码:

        @Override
        public boolean onLayoutChild(CoordinatorLayout parent, View child, int layoutDirection) {
            parent.onLayoutChild( child, layoutDirection);
            View stickyLayout = getStickyLayout(parent);
            View headerView = getHeaderView(parent);
            child.offsetTopAndBottom(headerView.getBottom() + stickyLayout.getHeight());
            return true;
        }
    

    底部的布局的位置是顶部的底部和中间部分的高度和,这样我们的几个部分布局已经完成了布局。

    2.2依赖关系

    这个地方我们的依赖关系是这样的,头部和底部的布局依赖中间停靠栏的滑动,所以我们这个地方先来看下头部设置的依赖:

    @Override
        public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
            return dependency instanceof TabLayout;
        }
    

    我们看到这个地方我们判断依赖是不是TabLayout,因为我们的停靠栏是TabLayout,当然不是一定要这么判断,你可以判断控件id是不是同一个或者其他。这个地方我们就这么判断了。接着我们看下底部设置的依赖:

      @Override
        public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
            return dependency instanceof TabLayout;
        }
    

    跟头部是一模一样的,依赖关系很容易设置。

    2.3运动协调

    因为头部和底部的布局都是依赖的中间停靠栏的,那么我们这里看下中间停靠栏是怎么运动的:

       @Override
        public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild, View target, int nestedScrollAxes) {
            //判断监听的方向
            return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0 ;
        }
    

    onStartNestedScroll方法是在(NestedScrollingChild实现类)DOWN事件中调用的,也就是说滑动将要开始调用的,这个方法主要用来监听垂直或者左右的事件,我们这个地方就监听垂直滑动的事件就可以了。接着MOVE事件来了,我们看下如下:

        @Override
        public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dx, int dy, int[] consumed) {
            int top = child.getTop();
            if (top >= minOffset){
                if (dy > 0){//往上移动
                    customScrollToTop = true;
                }else{
                    customScrollToTop = false;
                }
                consumed[1] = scroll(child, dy, minOffset, maxOffset);
            }
        }
    

    这个事件是在NestedScrollingChild实现类中的MOVE事件开始处调用的。也就是说onNestedPreScroll是在滑动开始的时候调用的。其中有个重要的参数consumed数组,这个数组很关键,他总共有consumed[0]和consumed[1],consumed[0]用来保存水平滑动时候中间停靠栏想要消费多少,consumed[1]用来保存垂直滑动时候中间停靠栏想要消费多少滑动值,因为我们这个地方主要就是监听垂直方向的滑动,所以我们主要就是消费垂直的滑动值。然后dy和dx就是代表垂直和水平滑动了多少值。代码里面我们首先是记录下customScrollToTop(用来判断是上滑还是下滑等会会有用),然后我们调用scroll来滑动,我们看下怎么实现:

       private int scroll(View child, int dy, int minOffset, int maxOffset) {
            int top = child.getTop();
            int offset = clamp(top - dy, minOffset, maxOffset) - top;
            child.offsetTopAndBottom(offset);
            return -offset;
        }
    

    我们看到主要就是调用clamp方法,这个方法我们一起看下:

        /**
         * 取上下限之间的值
         * */
        private int clamp(int i, int minOffset, int maxOffset) {
            if (i < minOffset) {
                return minOffset;
            } else if (i > maxOffset) {
                return maxOffset;
            } else {
                return i;
            }
        }
    

    这个方法主要是用来判断滑动之后的值是否在minOffset和maxOffset之间,如果超过则取minOffset或者maxOffset,然后scroll方法还做了一件事就是滑动中间的停靠栏,滑动距离就是-dy,最后将滑动距离返回回去给consumed[1],表示我要拦截minOffset和maxOffset之间所有的垂直方向滑动距离。接下来我们看下MOVE事件调用的另外一个方法:

        @Override
        public void onNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
            int top = child.getTop();
            if (top >= minOffset){
                scroll(child, dyUnconsumed, minOffset, maxOffset);
            }
        }
    

    这个方法和onNestedPreScroll方法类似,同样可以在这里做滑动,只是参数有点不同,其实dxUnconsumed和dyUnconsumed是上面一个方法onNestedPreScroll未消费掉的水平和垂直方向滑动的滑动值,我们这里代码跟上一个代码做的逻辑一样,只是把dyUnconsumed再消费掉。最后DOWN事件和MOVE事件都消费了,最后就是UP事件了,对应于onStopNestedScroll方法:

        @Override
        public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target) {
            //如果需要回弹则需要在这里写代码
            if(Math.abs(child.getTop()) < maxOffset/4 && customScrollToTop){
                scrollTo(coordinatorLayout, child, minOffset, 1000);
            }
        }
    

    这里就是做一些回弹操作的,我们这里也一样,如果已经到达 maxOffset/4,然后滑动是上滑的话我们就缓慢靠近顶部,方法如下:

        private void scrollTo(final CoordinatorLayout parent, final View child, final int y, int duration){
            customScrollToTop = false;
            final Scroller scroller = new Scroller(parent.getContext());
            scroller.startScroll(0,child.getTop(),0,y - child.getTop(),duration);
            ViewCompat.postOnAnimation(child, new Runnable() {
                @Override
                public void run() {
                    if (scroller.computeScrollOffset()) {
                        int delta = scroller.getCurrY() - child.getTop();
                        child.offsetTopAndBottom(delta);
                        parent.dispatchDependentViewsChanged(child);
                        ViewCompat.postOnAnimation(child, this);
                    }
                }
            });
        }
    

    我们这里主要用到了Scroller类和ViewCompat.postOnAnimation()方法来实现缓慢靠近顶部。这样如果停靠栏在minOffset和maxOffset之间的滑动也就完成了。因为头部和底部都是依赖于停靠栏运动的,所以我们先来看下头部(HeaderBehavior)跟随着停靠栏怎么运动:

      @Override
        public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
    
            if (dependentViewTop != -1){
                int top = child.getTop();
                int dy = dependency.getTop() - dependentViewTop;
    
                if (dy > -top){
                    dy = -top;
                }
    
                child.offsetTopAndBottom(dy);
            }
    
            dependentViewTop = dependency.getTop();
            childTop = child.getTop();
            return true;
        }
    

    当停靠栏运动的时候,这个依赖也会相应的做出反应,就是在onDependentViewChanged方法中写我们自己的运动情况,其中child就是我们头部,dependency就是依赖的对象停靠栏。我们这里主要是计算dy(也就是两次滑动的滑动差值),然后child即头部进行相应滑动。接着我们来看下底部滑动情况:

      @Override
        public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
            if (dependentViewTop != -1){
                int dy = dependency.getTop() - dependentViewTop;
    
                if (!isInit){
                    child.offsetTopAndBottom(dy);
                }
               isInit = false;
            }
    
            dependentViewTop = dependency.getTop();
            childTop = child.getTop();
            return true;
        }
    

    这个方法也是类似,这里因为onDependentViewChanged会执行多次,所以我们这里用isInit来标示是不是第一次布局,如果是则不需要让child移动了,因为我们onLayoutChild已经布局过了,如果这个地方再移动的话,那么我们初始位置就偏移了。到这里我们的例子就讲解完毕了,希望大家能明白,当然前提是你看了上面一篇的原理分析。
    总结:这个例子主要是使用了上一篇分析的知识,希望大家能在完全弄懂原理的情况下再来看这一篇,不然知识不会全面,最后就enjoy安卓之旅吧。

    enjoy

    相关文章

      网友评论

        本文标题:事件分发(4)-玩转自定义behavior之顶部停靠

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