如何實(shí)現(xiàn)兩個(gè)recycleView的平滑滾動(dòng)

目標(biāo)

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

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

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

最終效果圖

small.gif

android的繪制原理

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

onMeasure

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

依賴值

  • ViewGroup.MATCH_PARENT
  • ViewGroup.WRAP_CONTENT

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

getMeasureHeight 和getHeight的區(qū)別

  • getMeasureHeight是計(jì)算出來的高度
  • getHeight是最后繪制的高度
  • 有可能不同,因?yàn)楹竺孢€有動(dòng)畫,或者直接設(shè)置來改變
  • 在onMeasure后。getMeasureHeight是有值的
  • 在onDraw后,getHeight才有值
 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

onLayout

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

座標(biāo)系

  • 原點(diǎn)左上角
  • 寬是x軸
  • 高是y軸
  • 視圖的位置由左上角的點(diǎn)(x1,y1)和右下角的點(diǎn)(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

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

Canvas

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

事件分發(fā)機(jī)制

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

dispatchTouchEvent

  • View
  • ViewGroup
  • Activity

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

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

onInterceptTouchEvent

  • ViewGroup
  • Activity

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

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

onTouchEvent

  • ViewGroup
  • View
  • Activity
    這個(gè)方法是事件的處理方法,可以在這里寫具體的處理邏輯。返回true說明自己會(huì)處理,返回false,說明自己不會(huì)處理
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        return super.onTouchEvent(event);
    }

嵌入式滑動(dòng)的定制處理

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

  • ViewBehavior
  • NestedScrollingParent
  • NestedScrollingChild

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

原理

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

以前的滑動(dòng)處理

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

嵌入式滑動(dòng)

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

需求解決

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

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

布局問題

  • layoutDependsOn
  • onLayoutChild

C在layoutDependsOn回調(diào)中設(shè)置對B的依賴。那么B繪制完,C會(huì)被觸發(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;
    }

滑動(dòng)問題

在B中增加滑動(dòng)處理,當(dāng)C中有滑動(dòng)時(shí),首先父容器會(huì)查找是否有當(dāng)前的其他容器會(huì)消費(fèi)這個(gè)事件,如果會(huì)消費(fèi),會(huì)讓這個(gè)容器來處理事件,直到處理完畢,沒有其他容器消費(fèi),再交給C來處理。

  • onStartNestedScroll 來確定消費(fèi)的方向
  • onNestedPreScroll 會(huì)來確定是否消費(fèi),消費(fèi)多少,同時(shí)返回消費(fèi)剩余內(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;
            // 列表第一個(gè)全部可見Item的位置
            int pos = ((LinearLayoutManager) list.getLayoutManager()).findFirstCompletelyVisibleItemPosition();
            if (pos == 0 && pos < lastPosition) {
                downReach = true;
            }
            // 整體可以滑動(dòng),否則RecyclerView消費(fèi)滑動(dòng)事件
            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消費(fèi)滑動(dòng)事件
                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) {
        //計(jì)算列表y坐標(biāo),最小為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;
    }


    // 界面整體向上滑動(dòng),達(dá)到列表可滑動(dòng)的臨界點(diǎn)
    private boolean upReach;
    // 列表向上滑動(dòng)后,再向下滑動(dòng),達(dá)到界面整體可滑動(dòng)的臨界點(diǎn)
    private boolean downReach;
    // 列表上一個(gè)全部可見的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;
            // 列表第一個(gè)全部可見Item的位置
            int pos = ((LinearLayoutManager) list.getLayoutManager()).findFirstCompletelyVisibleItemPosition();
            if (pos == 0 && pos < lastPosition) {
                downReach = true;
            }
            // 整體可以滑動(dòng),否則RecyclerView消費(fèi)滑動(dòng)事件
            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消費(fèi)滑動(dòng)事件
                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輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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