【05】京东淘宝首页二级联动怎么实现

(1)自定义View中的事件分发流程
(2)嵌套滑动冲突
(3)嵌套滑动冲突解决方案
(4)嵌套滑动及吸顶效果制作
(5)嵌套滑动吸顶效果滑动冲突解决方案
(6)嵌套滑动吸顶效果中的惯性滑动处理
(7)事件的内部拦截与外部拦截

【05】京东淘宝首页二级联动怎么实现

1.概述

1.1Android本质

(1)四大组件
(2)自定义View

1.2自定义View需要了解什么?

(1)自定义View的创建及渲染流程

  • onMeasure 测量
  • onLayout 摆放
  • onDraw 绘制

(2)事件分发

  • dispatch
  • intercept
  • onTouch

(3)滑动冲突

滑动冲突有哪两种解决方案?

  • 内部拦截与外部拦截

(4)嵌套滑动(是滑动冲突的进阶)

嵌套滑动有几个版本?

  • 3个版本。

  • 大厂APP很多在用

2.案例布局分析

在这里插入图片描述

2.1为什么滑动事件会被吃掉?

(1)与事件分发有关系

2.1.1view与ViewGroup的关系

(1)从代码层面来讲,ViewGroup继承了View。
(2)从运行角度来讲,ViewGroup是View的父亲。
(3)事件分发是根据运行角度来的。
(4)事件分发是如何来分发的?

  • 是从Activity开始的。
2.1.2.单点触摸

在这里插入图片描述

2.1.3多点触摸

在这里插入图片描述

2.1.3.1多点手势手指操作流程

(1)手势

在这里插入图片描述

(2)流程

在这里插入图片描述

(3)一个Move事件有几个手指的信息?

  • 有几个手指就有几个.
  • 最多不能超过32个手指信息.

3.事件分发的流程

(1)从Activity开始分发

  • android.app.Activity#dispatchTouchEvent
  • android.view.Window.Callback#dispatchTouchEvent
  • android.view.Window#superDispatchTouchEvent
  • com.android.internal.policy.PhoneWindow#superDispatchTouchEvent
  • com.android.internal.policy.DecorView#superDispatchTouchEvent

(2)经过以上步骤,事件被分发到DecorView,DecorView继续分发事件
android.view.ViewGroup#dispatchTouchEvent

  • DecorView继承自FrameLayout
  • 而FrameLayout继承自ViewGroup

(3)事件是什么时候开始的

  • 从ACTION_DOWN开始(不管是单点触摸还是多点触摸,一个手势的MOVE都是从ACTION_DOWN开始的)

(4)DecorView分发到具体的布局时,会通过一个函数判断是否继续分发事件。
onInterceptTouchEvent

(5)android.view.ViewGroup#dispatchTouchEvent

if (onFilterTouchEventForSecurity(ev)) {
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;

            // Handle an initial down.
    		//判断是否是一个新事件的开始
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                // Throw away all previous state when starting a new touch gesture.
                // The framework may have dropped the up or cancel event for the previous gesture
                // due to an app switch, ANR, or some other state change.
                //如果是一个新的事件,需要清除掉所有的事件相关的东西
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }

            // Check for interception.
    		//用一个局部变量标记是否拦截事件
    		//如果是一个手势,同时还没有分发给其他人
    		//mFirstTouchTarget是一个存储事件的链表,表示有哪几个View来接收事件,有可能是一个手指触摸到
    		//多个View
    		//如果是多个手指放到多个View的时候,在不同的View层次的时候,才会进入到多个View里面去。
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
               /*
                *1.滑动冲突
                *(1)有内部拦截与外部拦截的
                *(2)mGroupFlags用于标识内部拦截,查看disallowIntercept这个标识是否允许拦截
                *(3)子View有权利申请父亲不要拦截事件,即通过disallowIntercept标识申请
                *(4)而disallowIntercept变量的值只能通过android.view.ViewGroup#requestDisallowInterceptTouchEvent方法进行修改。
                *(5)事件分发的时候要去看一下孩子是否告诉我不能够拦截孩子的事件。
                *(6)如果说事件要拦截,就交给onTouchEvent()进行处理
                *(7)如果说不拦截,就会一直分发下去,分发到什么状态呢?
                */
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                
                if (!disallowIntercept) {
                    //如果不允许拦截,就会问onInterceptTouchEvent                   
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // restore action in case it was changed
                } else {
                    intercepted = false;
                }
            } else {
                // There are no touch targets and this action is not an initial down
                // so this view group continues to intercept touches.
                intercepted = true;
            }

            // If intercepted, start normal event dispatch. Also if there is already
            // a view that is handling the gesture, do normal event dispatch.
            if (intercepted || mFirstTouchTarget != null) {
                ev.setTargetAccessibilityFocus(false);
            }

            // Check for cancelation.
            final boolean canceled = resetCancelNextUpFlag(this)
                    || actionMasked == MotionEvent.ACTION_CANCEL;

            // Update list of touch targets for pointer down, if needed.
            final boolean isMouseEvent = ev.getSource() == InputDevice.SOURCE_MOUSE;
            final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0
                    && !isMouseEvent;
            TouchTarget newTouchTarget = null;
            boolean alreadyDispatchedToNewTouchTarget = false;
            //不是cancel与拦截
            if (!canceled && !intercepted) {
                // If the event is targeting accessibility focus we give it to the
                // view that has accessibility focus and if it does not handle it
                // we clear the flag and dispatch the event to all children as usual.
                // We are looking up the accessibility focused host to avoid keeping
                // state since these events are very rare.
                View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
                        ? findChildWithAccessibilityFocus() : null;

                if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                    final int actionIndex = ev.getActionIndex(); // always 0 for down
                    final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                            : TouchTarget.ALL_POINTER_IDS;

                    // Clean up earlier touch targets for this pointer id in case they
                    // have become out of sync.
                    removePointersFromTouchTargets(idBitsToAssign);

                    final int childrenCount = mChildrenCount;
                    if (newTouchTarget == null && childrenCount != 0) {
                        final float x =
                                isMouseEvent ? ev.getXCursorPosition() : ev.getX(actionIndex);
                        final float y =
                                isMouseEvent ? ev.getYCursorPosition() : ev.getY(actionIndex);
                        // Find a child that can receive the event.
                        // Scan children from front to back.
                        final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                        final boolean customOrder = preorderedList == null
                                && isChildrenDrawingOrderEnabled();
                        final View[] children = mChildren;
                        //一个一个去问,
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            final int childIndex = getAndVerifyPreorderedIndex(
                                    childrenCount, i, customOrder);
                            final View child = getAndVerifyPreorderedView(
                                    preorderedList, children, childIndex);
                            if (!child.canReceivePointerEvents()
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                continue;
                            }

                            newTouchTarget = getTouchTarget(child);
                            if (newTouchTarget != null) {
                                // Child is already receiving touch within its bounds.
                                // Give it the new pointer in addition to the ones it is handling.
                                newTouchTarget.pointerIdBits |= idBitsToAssign;
                                break;
                            }

                            resetCancelNextUpFlag(child);
                            //如果遇到一个孩子要处理事件,继续分发,返回为true了,
                            //分发事件是分发给一个View,链表记录的也是一个View
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                // Child wants to receive touch within its bounds.
                                mLastTouchDownTime = ev.getDownTime();
                                if (preorderedList != null) {
                                    // childIndex points into presorted list, find original index
                                    for (int j = 0; j < childrenCount; j++) {
                                        if (children[childIndex] == mChildren[j]) {
                                            mLastTouchDownIndex = j;
                                            break;
                                        }
                                    }
                                } else {
                                    mLastTouchDownIndex = childIndex;
                                }
                                mLastTouchDownX = ev.getX();
                                mLastTouchDownY = ev.getY();
                                //将该孩子添加到touchTarget中去,会改变mFirstTouchTarget,链表发生改变
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            }

                            // The accessibility focus didn't handle the event, so clear
                            // the flag and do a normal dispatch to all children.
                            ev.setTargetAccessibilityFocus(false);
                        }
                        if (preorderedList != null) preorderedList.clear();
                    }

                    if (newTouchTarget == null && mFirstTouchTarget != null) {
                        // Did not find a child to receive the event.
                        // Assign the pointer to the least recently added target.
                        newTouchTarget = mFirstTouchTarget;
                        while (newTouchTarget.next != null) {
                            newTouchTarget = newTouchTarget.next;
                        }
                        newTouchTarget.pointerIdBits |= idBitsToAssign;
                    }
                }
            }

            // Dispatch to touch targets.
            if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary view.
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
                // Dispatch to touch targets, excluding the new touch target if we already
                // dispatched to it.  Cancel touch targets if necessary.
                TouchTarget predecessor = null;
                TouchTarget target = mFirstTouchTarget;
                while (target != null) {
                    final TouchTarget next = target.next;
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        handled = true;
                    } else {
                        final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                || intercepted;
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                            handled = true;
                        }
                        if (cancelChild) {
                            if (predecessor == null) {
                                mFirstTouchTarget = next;
                            } else {
                                predecessor.next = next;
                            }
                            target.recycle();
                            target = next;
                            continue;
                        }
                    }
                    predecessor = target;
                    target = next;
                }
            }

            // Update list of touch targets for pointer up or cancel, if needed.
            if (canceled
                    || actionMasked == MotionEvent.ACTION_UP
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                resetTouchState();
            } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
                final int actionIndex = ev.getActionIndex();
                final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
                removePointersFromTouchTargets(idBitsToRemove);
            }
        }

在这里插入图片描述

3.1如何解决嵌套滑动冲突?

3.1.1嵌套滑动产生原因

(1)案例中,事件已经给了里层的recycleView,已经将事件给消费掉了,导致无法将事件分发到外层的控件ScrollView,也就导致外部的ScrollView无法滚动。即在不支持嵌套滑动时,无法将事件向外层事件分发。

  • 外层使用的是ScrollView
  • 嵌套滑动,里层的RecycleView属于外层ScrollView的孩子。
  • RecycleView实现了嵌套滑动的孩子,NestedScrollingChild2, NestedScrollingChild3.
  • 嵌套滑动,一定要有两个角色参与,嵌套滑动要有父亲的角色,要有孩子的角色.案例1中一个是父亲(ScrollView),一个是孩子(RecyclerView)。
  • 而RecyclerView实现了孩子的角色,NestedScrollingChild2, NestedScrollingChild3,而ScrollView没有实现父亲的角色。ScrollView作为父控件,没有实现NestedScrollingParent3,即不具备父亲的角色,导致无法嵌套滑动。
public class ScrollView extends FrameLayout {
public class RecyclerView extends ViewGroup implements ScrollingView,
        NestedScrollingChild2, NestedScrollingChild3 {
3.1.2嵌套滑动解决方法
  • 换成NestedScrollView来解决问题,它实现了NestedScrollingParent3,NestedScrollingChild3
public class NestedScrollView extends FrameLayout implements NestedScrollingParent3,
        NestedScrollingChild3, ScrollingView {

3.2吸顶效果

(1)固定位置
(2)事件拦截
(3)备胎

在这里插入图片描述

(4)将tablayout+viewpager的高度设置为屏幕的高度

  • 自定义NestedScrollView实现吸顶效果
    com.gdc.knowledge.highui.jdtb.nestedscroll.c_fixedheight_viewpager_nestedscrollview_recyclerview.NestedScrollLayoutTest
/**
     * 1.当布局加载完成时,获取需要做吸顶效果的View(TabLayout+ViewPager区域部分)
     * 2.在测量的过程中,修改该布局区域的高度为整个屏幕的高度,即可出现吸顶效果
     */
    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        contentView = (ViewGroup) ((ViewGroup) getChildAt(0)).getChildAt(1);
    }

    /**
     * 1.调整contentView的高度为父容器的高度,使之填充(布局)整个屏幕的高度
     * 即产生吸顶效果,避免父容器滚动后出现空白。
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        ViewGroup.LayoutParams lp = contentView.getLayoutParams();
        lp.height = getMeasuredHeight();
        contentView.setLayoutParams(lp);
    }
  • 布局
    activity_nested_view_pager_test3.xml

3.3做了吸顶效果后无法上滑

在这里插入图片描述

(1)希望达到的效果

如果在滑动RecyclerView的时候,可以先判断一下NestedScrollView是否还可以继续滑动,如果可以继续滑动,则先让其向上滑动完成,当他不能往上滑的时候,就让RecyclerView自己滑。

(2)孩子滑动有3个版本,3继承2,2继承1

  • 版本2比版本1多了个嵌套滑动类型的参数
    一种叫TYPE_TOUCH:惯性滑动,是手指已经离开了屏幕,但是还有剩余的力量支撑滑动。即手指还在屏幕上,用的力气很大,手指离开以后还要继续滑。

一种是TYPE_NON_TOUCH:手指滑动

public interface NestedScrollingChild3 extends NestedScrollingChild2 {
public interface NestedScrollingChild2 extends NestedScrollingChild {

(3)父亲与孩子的关系

3.4嵌套滑动流程

在这里插入图片描述

3.3.1ACTION_DOWN
  • 嵌套滑动虽然有父亲与孩子两个角色,但是主动者是孩子,事情是由孩子触发的。

  • 如上图,如果在RecyclerView滑动之前,希望NestedScrollView先滑完.

  • 案例2没有实现的原因

  • 一进入到界面时,需要设置RecyclerVeiw支持嵌套滑动

  • 监测RecyclerView嵌套滑动事件流程
    com.gdc.knowledge.highui.jdtb.common.fragment.NestedLogRecyclerView

  • 滑动事件帮助类
    androidx.core.view.NestedScrollingChildHelper
    androidx.core.view.NestedScrollingChildHelper#startNestedScroll(int, int)

public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) {
    //判断是否有嵌套滑动的父亲,首次没有
    if (hasNestedScrollingParent(type)) {
            // Already in progress
            return true;
        }
   /**
   	1.如果没有嵌套滑动的父亲,就判断是否支持嵌套滑动
   	2.就一直去找嵌套滑动的父亲
    */
        if (isNestedScrollingEnabled()) {
            ViewParent p = mView.getParent();
            View child = mView;
            while (p != null) {
                //3.如果找到了,就判断其是否支持嵌套滑动,执行相应的嵌套滑动方法
                if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)) {
                    setNestedScrollingParentForType(type, p);
                    ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type);
                    return true;
                }
                //4.如果不支持嵌套滑动,就会一直往上去找,找它的父亲
                if (p instanceof View) {
                    child = (View) p;
                }
                p = p.getParent();
            }
        }
        return false;
    }

(4)找到支持嵌套滑动的父亲之后,执行相应的嵌套滑动方法

@Override
    public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes,
            int type) {
        return (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
    }

NestedScrollView只支持垂直方向的滑动,横向的不支持。

3.3.2ACTION_MOVE

在滑动之前会执行dispatchNestedPreScroll方法
androidx.core.view.NestedScrollingChildHelper#dispatchNestedPreScroll(int, int, int[], int[], int),NestedScrollView是先交给了它的父亲滑动,它即是孩子又是父亲,有控件询问自己是否可以滑.

/**
 * @author XiongJie
 * @version appVer
 * @Package com.gdc.knowledge.highui.jdtb.nestedscroll.e_prefect_nestedscroll
 * @file
 * @Description:
 * 1.解决嵌套滑动吸顶效果滑动冲突问题
 * (1)当滑动内层RecycleView时,判断外层NestedScrollLayout自定义View是否还可以继续滑动
 * (2)如果可以继续滑动,则先让其滑动完
 * (3)如果父级NestedScrollLayout不能滑动了,则让
 * @date 2021-6-30 16:10
 * @since appVer
 */

public class NestedScrollLayout extends NestedScrollView {

    //布局
    private View topView;
    private ViewGroup contentView;
    private static final String TAG = "NestedScrollLayout";
    /**
     * 惯性滑动时使用到的工具类
     */
    private FlingHelper mFlingHelper;
    /**
     *在RecyclerView fling(惯性滑动)情况下,记录当前RecyclerView在y轴的偏移
     */
    int totalDy = 0;
    /**
     * 用于判断RecyclerView是否在fling惯性滑动
     */
    boolean isStartFling = false;
    /**
     * 记录当前滑动的y轴加速度
     */
    private int velocityY = 0;

    public NestedScrollLayout(@NonNull Context context) {
        super(context);
        init();
    }

    public NestedScrollLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public NestedScrollLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        mFlingHelper = new FlingHelper(getContext());
        //1.为了记录惯性滑动距离
        setOnScrollChangeListener(new View.OnScrollChangeListener(){
            @Override
            public void onScrollChange(View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) {
                if (isStartFling) {
                    totalDy = 0;
                    isStartFling = false;
                }
                if (scrollY == 0) {
                    Log.i(TAG, "TOP SCROLL");
                    // refreshLayout.setEnabled(true);
                }
                if (scrollY == (getChildAt(0).getMeasuredHeight() - v.getMeasuredHeight())) {
                    Log.i(TAG, "BOTTOM SCROLL");
                    dispatchChildFling();
                }
                //在RecyclerView fling情况下,记录当前RecyclerView在y轴的偏移
                totalDy += scrollY - oldScrollY;
            }
        });
    }

    /**
     * 孩子ReccycleView可以滑动了
     */
    private void dispatchChildFling() {
       if(velocityY != 0){
           //1.把父亲滑动的速度转换成距离
           Double splineFlingDistance =
                   mFlingHelper.getSplineFlingDistance(velocityY);

           //2.孩子滑动的距离=速度转换后的距离-父亲自己滑动的距离

           //3.再将孩子滑动的距离转换成速度,是因为fling惯性滑动方法参数只支持速度
           if(splineFlingDistance > totalDy){
               childFling(mFlingHelper.getVelocityByDistance(splineFlingDistance - Double.valueOf(totalDy)));
           }
           totalDy = 0;
           velocityY = 0;
       }
    }

    /**
     * 1.孩子的惯性滑动
     * (1)孩子应该滑动的距离,又要转换成速度,因为fling方法只支持速度
     * @param velY
     */
    private void childFling(int velY) {
        RecyclerView childRecyclerView = getChildRecyclerView(contentView);
        if (childRecyclerView != null) {
            childRecyclerView.fling(0, velY);
        }
    }

    /**
     * 1.惯性滑动
     * (1)是父亲带着孩子滑
     * (2)会传进来一个速度,需要将速度记录下来.
     * (3)惯性滑动是父亲先滑,自己滑完之后,还有余力,孩子再滑。
     * (4)惯性滑动有速度
     * (5)速度与距离之前的换算由FlingHelper惯性滑动工具处理,速度与距离是可以转换的。
     * (6)计算自己滑动的距离
     * (7)孩子滑动的距离=速度转换后的距离-我自己的滑动的距离
     * @param velocityY
     */
    @Override
    public void fling(int velocityY) {
        super.fling(velocityY);
        //加速度<= 0
        if(velocityY <= 0){
            this.velocityY = 0;
        }else{
            isStartFling = true;
            this.velocityY = velocityY;
        }
    }

    /**
     * 获取子RecyclerView
     * @param viewGroup
     * @return
     */
    private RecyclerView getChildRecyclerView(ViewGroup viewGroup) {
        for (int i = 0; i < viewGroup.getChildCount(); i++) {
            View view = viewGroup.getChildAt(i);
            if (view instanceof RecyclerView && view.getClass() == NestedLogRecyclerView.class) {
                return (RecyclerView) viewGroup.getChildAt(i);
            } else if (viewGroup.getChildAt(i) instanceof ViewGroup) {
                ViewGroup childRecyclerView = getChildRecyclerView((ViewGroup) viewGroup.getChildAt(i));
                if (childRecyclerView instanceof RecyclerView) {
                    return (RecyclerView) childRecyclerView;
                }
            }
            continue;
        }
        return null;
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        topView = ((ViewGroup) getChildAt(0)).getChildAt(0);
        contentView = (ViewGroup) ((ViewGroup) getChildAt(0)).getChildAt(1);
    }

    /**
     * 1.吸顶效果
     * 调整contentView的高度为父容器高度,使之填充布局,避免父容器滚动后出现空白
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        ViewGroup.LayoutParams lp = contentView.getLayoutParams();
        lp.height = getMeasuredHeight();
        contentView.setLayoutParams(lp);
    }

    /**
     * 1.嵌套滑动之前
     *
     * (1)向上滑动。
     * (2)若当前topview可见,需要将topview滑动至不可见
     * (3)int[] consumed:问父亲是否可以滑动得到的返回值
     * (4)父亲先滑动,子控件再滑动。父亲滑动之后还有剩余的部分,先让其滑完,然后自己再滑动。
     *
     * @param target
     * @param dx
     * @param dy
     * @param consumed
     */
    @Override
    public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
        Log.i(TAG, getScrollY()+"::onNestedPreScroll::"+topView.getMeasuredHeight());
        //1.判断顶部是否可见
        boolean hideTop = dy > 0 && getScrollY() < topView.getMeasuredHeight();
        if (hideTop) {
            scrollBy(0, dy);
            //以下语句是确保先让父亲滑动完成,然后子控件再滑,如果不加此句,会导致父控件与子控件同时都滑动,滑动顺序不可控了。
            consumed[1] = dy;
        }
    }
}

3.4惯性滑动

(1)没有连续滑动的原因

  • 是因为不属于嵌套滑动的范畴,因为嵌套滑动是由孩子触发的。
  • 而父控件滑动没有导致子控件惯性滑动,是因为其不属于嵌套滑动范围。(父亲带着孩子滑动)
3.4.1父亲带着孩子滑

这种滑动属于惯性滑动。

/**
     * 孩子ReccycleView可以滑动了
     */
    private void dispatchChildFling() {
       if(velocityY != 0){
           //1.把父亲滑动的速度转换成距离
           Double splineFlingDistance =
                   mFlingHelper.getSplineFlingDistance(velocityY);

           //2.孩子滑动的距离=速度转换后的距离-父亲自己滑动的距离

           //3.再将孩子滑动的距离转换成速度,是因为fling惯性滑动方法参数只支持速度
           if(splineFlingDistance > totalDy){
               childFling(mFlingHelper.getVelocityByDistance(splineFlingDistance - Double.valueOf(totalDy)));
           }
           totalDy = 0;
           velocityY = 0;
       }
    }

    /**
     * 1.孩子的惯性滑动
     * (1)孩子应该滑动的距离,又要转换成速度,因为fling方法只支持速度
     * @param velY
     */
    private void childFling(int velY) {
        RecyclerView childRecyclerView = getChildRecyclerView(contentView);
        if (childRecyclerView != null) {
            childRecyclerView.fling(0, velY);
        }
    }

3.4.2记录惯性滑动距离
//1.为了记录惯性滑动距离
        setOnScrollChangeListener(new View.OnScrollChangeListener(){
            @Override
            public void onScrollChange(View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) {
                if (isStartFling) {
                    totalDy = 0;
                    isStartFling = false;
                }
                if (scrollY == 0) {
                    Log.i(TAG, "TOP SCROLL");
                    // refreshLayout.setEnabled(true);
                }
                if (scrollY == (getChildAt(0).getMeasuredHeight() - v.getMeasuredHeight())) {
                    Log.i(TAG, "BOTTOM SCROLL");
                    dispatchChildFling();
                }
                //在RecyclerView fling情况下,记录当前RecyclerView在y轴的偏移
                totalDy += scrollY - oldScrollY;
            }
        });

4.事件的内部拦截与外部拦截

在这里插入图片描述

(1)孩子会接收到一堆事件,在ACTION_DOWN的时候,告诉父亲,我要这个事件,不要拦截我的事件。
androidx.recyclerview.widget.RecyclerView#requestDisallowInterceptTouchEvent
(2)需要满足一定条件时才会去请求。

(3)外部拦截,除非是自己写一个View才会使用。没必要使用外部拦截,即自己判断是否需要,如果需要,则不给孩子事件。

5.打赏鼓励

感谢您的细心阅读,您的鼓励是我写作的不竭动力!!!

5.1微信打赏

在这里插入图片描述

5.2支付宝打赏

在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值