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

Posted 清风百草

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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以上是关于05京东淘宝首页二级联动怎么实现的主要内容,如果未能解决你的问题,请参考以下文章

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

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

vue mint-ui 实现省市区街道4级联动(仿淘宝京东收货地址4级联动)

仿京东淘宝首页,通过两层嵌套的RecyclerView实现tab的吸顶效果

仿京东淘宝首页,通过两层嵌套的RecyclerView实现tab的吸顶效果

react-native仿京东首页搜索栏联动效果