如何實現(xiàn)兩個recycleView的平滑滾動

目標

  • 2個recycleview同時放在一個頁面內(nèi),可以完成平滑滾動
  • 頂部recycleview有固定高度,為gridLayout布局
  • 底部recycleview占用剩余高度,為LinearLayoutManager布局

目前沒有直接可用的布局可以完成我們的需求,我們基于CoordinatorLayout做一些簡單的定制來完成我們的需求,如果想完成定制,那么需要我們理解嵌入式滑動的原理,下面我們會從三個方面來進行講解

  • 繪制原理
  • 事件分發(fā)機制
  • 嵌入式滑動處理

最終效果圖

small.gif

android的繪制原理

  • 跟繪制相關(guān)的三個核心方法
    • onMeasure
    • onLayout
    • onDraw

onMeasure

當計劃在界面繪制一個View時,我們需要知道,視圖的大小,onMeasure會提供給我們一個機會來決定我們繪制view的大小,我們可以直接設(shè)定這個大小,也可以設(shè)置一個依賴值,由父類根據(jù)父類的大小來動態(tài)決定子空間大小,我們一旦自己設(shè)置了固定的大小,那么需要在這里調(diào)用setMeasuredDimension方法,明確告訴父容器我們的設(shè)置

依賴值

  • ViewGroup.MATCH_PARENT
  • ViewGroup.WRAP_CONTENT

MATCH_PARENT 表示,我們需要父容器有多大,我們盡可能占據(jù)多大
WRAP_CONTENT 表示,只要能夠顯示出我們的內(nèi)容,就可以了。其他位置由父容器另外安排

getMeasureHeight 和getHeight的區(qū)別

  • getMeasureHeight是計算出來的高度
  • getHeight是最后繪制的高度
  • 有可能不同,因為后面還有動畫,或者直接設(shè)置來改變
  • 在onMeasure后。getMeasureHeight是有值的
  • 在onDraw后,getHeight才有值
 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

onLayout

上面我們一旦知道我們的大小,那么就需要確定我們的位置,在這個回調(diào)中,父容器提供一個機會給我們來確定自己容器的位置,我們可以根據(jù)父容器提供的上下左右來確定我們的位置,也可以自己設(shè)置我們理想中的上下左右位置。

座標系

  • 原點左上角
  • 寬是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

這個方法調(diào)用時,就是根據(jù)我們前面通過onMeasure和onLayout完成的對視圖大小和位置的計算來完成最終的繪制。我們可以在這塊來決定繪制的顏色,也可以修改我們繪制的大小和位置。

Canvas

  • Canvas是無限大的
  • 屏幕只是畫布的可見區(qū)域
  • 我們可以繪制在屏幕外部
  • 如果需要看到屏幕外部的內(nèi)容,我們需要滑動屏幕來完成,不過為了優(yōu)化。我們常常是在屏幕外不會去做繪制的
 @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
    }

事件分發(fā)機制

  • 事件分發(fā)的三個方法
    • dispatchTouchEvent
    • onInterceptTouchEvent
    • onTouchEvent

dispatchTouchEvent

  • View
  • ViewGroup
  • Activity

這個方法是事件分發(fā)的入口,所有的方法都從這個入口進入,然后向子視圖或者自己的其他方法傳遞,在這個方法內(nèi)的攔截會直接影響性能和后面的回調(diào)處理

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

onInterceptTouchEvent

  • ViewGroup
  • Activity

這個方法只存在可以添加子視圖的容器類中,因為這個方法主要是做攔截處理的。如果方法返回true,那么就開始攔截,會把事件轉(zhuǎn)到自己的onTouchEvent中,
而不會向子視圖傳遞,如果返回false,那么不會攔截,會繼續(xù)傳遞和處理

    @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);
    }

嵌入式滑動的定制處理

上面的繪制和事件分發(fā)邏輯都比較簡潔,清晰,隨著業(yè)務(wù)發(fā)展,可能需要更多更負責的頁面,比如在一個頁面滑動時,需要修改其他視圖的布局或者滑動。那么Android提供給了我們CoordernateLayout布局來完成,同時提供了ViewBehavior來自定義嵌入式滑動的布局和滑動

  • ViewBehavior
  • NestedScrollingParent
  • NestedScrollingChild

目前很多android默認的視圖已經(jīng)實現(xiàn)了NestedScrollingParent和NestedScrollingChild接口來完成了嵌入式滑動的處理,我們可以直接使用,不過如果使用效果無法滿足我們的需求,還是需要通過ViewBehavior來定制我們處理。
如果我們用到的視圖不支持嵌入式滑動,我們需要自己來實現(xiàn)NestedScrollingParent和NestedScrollingChild接口來完成嵌入式滑動。

原理

容器A支持NestedScrollingParent, 增加了支持NestedScrollingChild接口的視圖B和視圖C,那么在B滑動時,如何影響視圖C的布局和滑動呢

以前的滑動處理

  • 如果滑動發(fā)生在B,那么事件分發(fā)由A開始
  • 如果A要攔截,那么A就會處理事件
  • 如果A不攔截,那么就交給B來處理自己的事件

嵌入式滑動

  • 如果滑動發(fā)生在B,同時B支持NestedScrollingChild,事件分發(fā)還在是A開始
  • 如果容器A內(nèi)的子視圖有包含NestedScrollingChild或者有ViewBehavior。那么就要分發(fā)touch事件給這個視圖,比如A中的另外一個視圖C
  • 視圖C會根據(jù)定制的ViewBehavior來確定是否要響應(yīng)這個滑動。

需求解決

我們的目標是容器A中有容器B和容器C,先添加B,再添加C,B為頂部視圖,C為底部視圖,B和C都是RecycleView,他們是支持滑動的,A我們可以使用CoordernateLayout,B和C使用嵌入式滑動來處理事件
我們需要解決幾個問題

  • 布局問題,需要B和C平鋪在A中,默認是覆蓋,后面的覆蓋前面的
  • 滑動問題,在C上滑動時,整體布局上移動,知道B移除屏幕,C開始處理自己的滑動

布局問題

  • layoutDependsOn
  • onLayoutChild

C在layoutDependsOn回調(diào)中設(shè)置對B的依賴。那么B繪制完,C會被觸發(fā),onLayoutChild回調(diào)中我們下移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 會來確定是否消費,消費多少,同時返回消費剩余內(nèi)容
 @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;
        }
    }

回調(diào)介紹

  • 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;
    }


最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容