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京东淘宝首页二级联动怎么实现的主要内容,如果未能解决你的问题,请参考以下文章
vue mint-ui 实现省市区街道4级联动(仿淘宝京东收货地址4级联动)
仿京东淘宝首页,通过两层嵌套的RecyclerView实现tab的吸顶效果