Android经验分享

如何实现两个recycleView的平滑滚动

2019-02-18  本文已影响0人  没头脑和挺高兴

目标

目前没有直接可用的布局可以完成我们的需求,我们基于CoordinatorLayout做一些简单的定制来完成我们的需求,如果想完成定制,那么需要我们理解嵌入式滑动的原理,下面我们会从三个方面来进行讲解

最终效果图

small.gif

android的绘制原理

onMeasure

当计划在界面绘制一个View时,我们需要知道,视图的大小,onMeasure会提供给我们一个机会来决定我们绘制view的大小,我们可以直接设定这个大小,也可以设置一个依赖值,由父类根据父类的大小来动态决定子空间大小,我们一旦自己设置了固定的大小,那么需要在这里调用setMeasuredDimension方法,明确告诉父容器我们的设置

依赖值

MATCH_PARENT 表示,我们需要父容器有多大,我们尽可能占据多大
WRAP_CONTENT 表示,只要能够显示出我们的内容,就可以了。其他位置由父容器另外安排

getMeasureHeight 和getHeight的区别

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

onLayout

上面我们一旦知道我们的大小,那么就需要确定我们的位置,在这个回调中,父容器提供一个机会给我们来确定自己容器的位置,我们可以根据父容器提供的上下左右来确定我们的位置,也可以自己设置我们理想中的上下左右位置。

座标系

 @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
    }

onDraw

这个方法调用时,就是根据我们前面通过onMeasure和onLayout完成的对视图大小和位置的计算来完成最终的绘制。我们可以在这块来决定绘制的颜色,也可以修改我们绘制的大小和位置。

Canvas

 @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
    }

事件分发机制

dispatchTouchEvent

这个方法是事件分发的入口,所有的方法都从这个入口进入,然后向子视图或者自己的其他方法传递,在这个方法内的拦截会直接影响性能和后面的回调处理

@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        return super.dispatchTouchEvent(ev);
    }

onInterceptTouchEvent

这个方法只存在可以添加子视图的容器类中,因为这个方法主要是做拦截处理的。如果方法返回true,那么就开始拦截,会把事件转到自己的onTouchEvent中,
而不会向子视图传递,如果返回false,那么不会拦截,会继续传递和处理

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return super.onInterceptTouchEvent(ev);
    }

onTouchEvent

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        return super.onTouchEvent(event);
    }

嵌入式滑动的定制处理

上面的绘制和事件分发逻辑都比较简洁,清晰,随着业务发展,可能需要更多更负责的页面,比如在一个页面滑动时,需要修改其他视图的布局或者滑动。那么Android提供给了我们CoordernateLayout布局来完成,同时提供了ViewBehavior来自定义嵌入式滑动的布局和滑动

目前很多android默认的视图已经实现了NestedScrollingParent和NestedScrollingChild接口来完成了嵌入式滑动的处理,我们可以直接使用,不过如果使用效果无法满足我们的需求,还是需要通过ViewBehavior来定制我们处理。
如果我们用到的视图不支持嵌入式滑动,我们需要自己来实现NestedScrollingParent和NestedScrollingChild接口来完成嵌入式滑动。

原理

容器A支持NestedScrollingParent, 增加了支持NestedScrollingChild接口的视图B和视图C,那么在B滑动时,如何影响视图C的布局和滑动呢

以前的滑动处理

嵌入式滑动

需求解决

我们的目标是容器A中有容器B和容器C,先添加B,再添加C,B为顶部视图,C为底部视图,B和C都是RecycleView,他们是支持滑动的,A我们可以使用CoordernateLayout,B和C使用嵌入式滑动来处理事件
我们需要解决几个问题

布局问题

C在layoutDependsOn回调中设置对B的依赖。那么B绘制完,C会被触发,onLayoutChild回调中我们下移C到B下面。确保B和C按线性排列


@Override
    public boolean onLayoutChild(@NonNull CoordinatorLayout parent, @NonNull View child, int layoutDirection) {

        if(isLayout) {
            return super.onLayoutChild(parent, child, layoutDirection);
        }else {
            isLayout = true;
        }
        int height = parent.getContext().getResources().getDimensionPixelOffset(R.dimen.home_top_container_height);
        Log.e(TAG,"main.method:onLayoutChild,id:"+R.id.main_container+",child.id:"+child.getId()+",height:"+height);
        child.setTranslationY(height);
        return super.onLayoutChild(parent, child, layoutDirection);
    }
@Override
    public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
        boolean flag = (dependency.getId() == R.id.home_top_container);
        Log.e(TAG,"flag:"+flag+",child.id:"+child.getId());
        return flag;
    }

滑动问题

在B中增加滑动处理,当C中有滑动时,首先父容器会查找是否有当前的其他容器会消费这个事件,如果会消费,会让这个容器来处理事件,直到处理完毕,没有其他容器消费,再交给C来处理。

 @Override
    public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View directTargetChild, @NonNull View target, int axes, int type) {
        Log.e(TAG,"top.method:onStartNestedScroll,child.id:"+child.getId()+",target.id:"+target.getId());
        return (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
    }

    @Override
    public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
        Log.e(TAG,"top.method:onNestedPreScroll,child.id:"+child.getId()+",target.id:"+target.getId());
        super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type);
        if (target instanceof RecyclerView) {
            RecyclerView list = (RecyclerView) target;
            // 列表第一个全部可见Item的位置
            int pos = ((LinearLayoutManager) list.getLayoutManager()).findFirstCompletelyVisibleItemPosition();
            if (pos == 0 && pos < lastPosition) {
                downReach = true;
            }
            // 整体可以滑动,否则RecyclerView消费滑动事件
            if (canScroll(child, dy) && pos == 0) {
                float finalY = child.getTranslationY() - dy;
                if (finalY < -child.getHeight()) {
                    finalY = -child.getHeight();
                    upReach = true;
                } else if (finalY > 0) {
                    finalY = 0;
                }
                child.setTranslationY(finalY);
                // 让CoordinatorLayout消费滑动事件
                consumed[1] = dy;
            }
            lastPosition = pos;
        }
    }

回调介绍

package com.p.b.ui.behavior;

import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;

import com.p.b.R;
import com.y.b.tools.Log;

import androidx.annotation.NonNull;
import androidx.coordinatorlayout.widget.CoordinatorLayout;

public class MainViewBehavior extends CoordinatorLayout.Behavior<View>{

    private static final String TAG = "TopViewBehavior";
    private float deltaY;

    public MainViewBehavior() {
    }

    public MainViewBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
        boolean flag = (dependency.getId() == R.id.home_top_container);
        Log.e(TAG,"flag:"+flag+",child.id:"+child.getId());
        return flag;
    }

    @Override
    public boolean onInterceptTouchEvent(@NonNull CoordinatorLayout parent, @NonNull View child, @NonNull MotionEvent ev) {
        return super.onInterceptTouchEvent(parent, child, ev);
    }

    boolean isLayout = false;

    @Override
    public boolean onLayoutChild(@NonNull CoordinatorLayout parent, @NonNull View child, int layoutDirection) {

        if(isLayout) {
            return super.onLayoutChild(parent, child, layoutDirection);
        }else {
            isLayout = true;
        }
        int height = parent.getContext().getResources().getDimensionPixelOffset(R.dimen.home_top_container_height);
        Log.e(TAG,"main.method:onLayoutChild,id:"+R.id.main_container+",child.id:"+child.getId()+",height:"+height);
        child.setTranslationY(height);
        return super.onLayoutChild(parent, child, layoutDirection);
    }



    @Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
        //计算列表y坐标,最小为0
        float y = dependency.getHeight() + dependency.getTranslationY();
        Log.e(TAG,"main.method:onDependentViewChanged,child.id:"+child.getId()+",dependency.id:"+dependency.getId()+",y:"+y+",de.height:"+dependency.getHeight()+",tranY:"+dependency.getTranslationY());
        if (y <= 0) {
            y = 0;
        }
        child.setY(y);
        return true;
    }}


package com.p.b.ui.behavior;

import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;

import com.p.b.R;
import com.y.b.tools.Log;

import androidx.annotation.NonNull;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import androidx.core.view.ViewCompat;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;

public class TopViewBehavior extends CoordinatorLayout.Behavior<View>{

    private static final String TAG = "TopViewBehavior";
    private float deltaY;

    public TopViewBehavior() {
    }

    public TopViewBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
//        boolean flag = (dependency.getId() == R.id.main_container);
//        Log.e(TAG,"flag:"+flag+",child.id:"+child.getId());
        return false;
    }


    @Override
    public boolean onLayoutChild(@NonNull CoordinatorLayout parent, @NonNull View child, int layoutDirection) {
//        Log.e(TAG,"id:"+R.id.home_main_container+",child.id:"+child.getId());
//        child.setTranslationY(600);
        return false;
    }


    // 界面整体向上滑动,达到列表可滑动的临界点
    private boolean upReach;
    // 列表向上滑动后,再向下滑动,达到界面整体可滑动的临界点
    private boolean downReach;
    // 列表上一个全部可见的item位置
    private int lastPosition = -1;


    @Override
    public boolean onInterceptTouchEvent(CoordinatorLayout parent, View child, MotionEvent ev) {
        Log.e(TAG,"top.method:onStartNestedScroll,child.id:"+child.getId()+",ev:"+ev);
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                downReach = false;
                upReach = false;
                break;
        }
        return super.onInterceptTouchEvent(parent, child, ev);
    }

    @Override
    public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View directTargetChild, @NonNull View target, int axes, int type) {
        Log.e(TAG,"top.method:onStartNestedScroll,child.id:"+child.getId()+",target.id:"+target.getId());
        return (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
    }

    @Override
    public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
        Log.e(TAG,"top.method:onNestedPreScroll,child.id:"+child.getId()+",target.id:"+target.getId());
        super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type);
        if (target instanceof RecyclerView) {
            RecyclerView list = (RecyclerView) target;
            // 列表第一个全部可见Item的位置
            int pos = ((LinearLayoutManager) list.getLayoutManager()).findFirstCompletelyVisibleItemPosition();
            if (pos == 0 && pos < lastPosition) {
                downReach = true;
            }
            // 整体可以滑动,否则RecyclerView消费滑动事件
            if (canScroll(child, dy) && pos == 0) {
                float finalY = child.getTranslationY() - dy;
                if (finalY < -child.getHeight()) {
                    finalY = -child.getHeight();
                    upReach = true;
                } else if (finalY > 0) {
                    finalY = 0;
                }
                child.setTranslationY(finalY);
                // 让CoordinatorLayout消费滑动事件
                consumed[1] = dy;
            }
            lastPosition = pos;
        }
    }

    private boolean canScroll(View child, float scrollY) {
        if (scrollY > 0 && child.getTranslationY() == -child.getHeight() && !upReach) {
            return false;
        }

        if (downReach) {
            return false;
        }
        return true;
    }


上一篇 下一篇

猜你喜欢

热点阅读