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

目标

  • 2个recycleview同时放在一个页面内,可以完成平滑滚动
  • 顶部recycleview有固定高度,为gridLayout布局
  • 底部recycleview占用剩余高度,为LinearLayoutManager布局

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

  • 绘制原理
  • 事件分发机制
  • 嵌入式滑动处理

最终效果图

small.gif

android的绘制原理

  • 跟绘制相关的三个核心方法
    • onMeasure
    • onLayout
    • onDraw

onMeasure

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

依赖值

  • ViewGroup.MATCH_PARENT
  • ViewGroup.WRAP_CONTENT

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

getMeasureHeight 和getHeight的区别

  • getMeasureHeight是计算出来的高度
  • getHeight是最后绘制的高度
  • 有可能不同,因为后面还有动画,或者直接设置来改变
  • 在onMeasure后。getMeasureHeight是有值的
  • 在onDraw后,getHeight才有值
 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

onLayout

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

座标系

  • 原点左上角
  • 宽是x轴
  • 高是y轴
  • 视图的位置由左上角的点(x1,y1)和右下角的点(x2,y2)的位置来决定
    • 上 y1
    • 左 x1
    • 右 x2
    • 下 y2
    • 高度 y2-y1
    • 宽度 x2-x1
 @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

  • Canvas是无限大的
  • 屏幕只是画布的可见区域
  • 我们可以绘制在屏幕外部
  • 如果需要看到屏幕外部的内容,我们需要滑动屏幕来完成,不过为了优化。我们常常是在屏幕外不会去做绘制的
 @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
    }

事件分发机制

  • 事件分发的三个方法
    • dispatchTouchEvent
    • onInterceptTouchEvent
    • onTouchEvent

dispatchTouchEvent

  • View
  • ViewGroup
  • Activity

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

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

onInterceptTouchEvent

  • ViewGroup
  • Activity

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

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

onTouchEvent

  • ViewGroup
  • View
  • Activity
    这个方法是事件的处理方法,可以在这里写具体的处理逻辑。返回true说明自己会处理,返回false,说明自己不会处理
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        return super.onTouchEvent(event);
    }

嵌入式滑动的定制处理

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

  • ViewBehavior
  • NestedScrollingParent
  • NestedScrollingChild

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

原理

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

以前的滑动处理

  • 如果滑动发生在B,那么事件分发由A开始
  • 如果A要拦截,那么A就会处理事件
  • 如果A不拦截,那么就交给B来处理自己的事件

嵌入式滑动

  • 如果滑动发生在B,同时B支持NestedScrollingChild,事件分发还在是A开始
  • 如果容器A内的子视图有包含NestedScrollingChild或者有ViewBehavior。那么就要分发touch事件给这个视图,比如A中的另外一个视图C
  • 视图C会根据定制的ViewBehavior来确定是否要响应这个滑动。

需求解决

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

  • 布局问题,需要B和C平铺在A中,默认是覆盖,后面的覆盖前面的
  • 滑动问题,在C上滑动时,整体布局上移动,知道B移除屏幕,C开始处理自己的滑动

布局问题

  • layoutDependsOn
  • onLayoutChild

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来处理。

  • onStartNestedScroll 来确定消费的方向
  • onNestedPreScroll 会来确定是否消费,消费多少,同时返回消费剩余内容
 @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;
        }
    }

回调介绍

  • onLayoutChild
  • layoutDependsOn
  • onDependentViewChanged
  • onNestedPreScroll
  • onStartNestedScroll
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;
    }


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