(1)自定义View中的事件分发流程
(2)嵌套滑动冲突
(3)嵌套滑动冲突解决方案
(4)嵌套滑动及吸顶效果制作
(5)嵌套滑动吸顶效果滑动冲突解决方案
(6)嵌套滑动吸顶效果中的惯性滑动处理
(7)事件的内部拦截与外部拦截
(1)四大组件
(2)自定义View
(1)自定义View的创建及渲染流程
(2)事件分发
(3)滑动冲突
滑动冲突有哪两种解决方案?
(4)嵌套滑动(是滑动冲突的进阶)
嵌套滑动有几个版本?
3个版本。
大厂APP很多在用
(1)与事件分发有关系
(1)从代码层面来讲,ViewGroup继承了View。
(2)从运行角度来讲,ViewGroup是View的父亲。
(3)事件分发是根据运行角度来的。
(4)事件分发是如何来分发的?
(1)手势
(2)流程
(3)一个Move事件有几个手指的信息?
(1)从Activity开始分发
(2)经过以上步骤,事件被分发到DecorView,DecorView继续分发事件
android.view.ViewGroup#dispatchTouchEvent
(3)事件是什么时候开始的
(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);
}
}
(1)案例中,事件已经给了里层的recycleView,已经将事件给消费掉了,导致无法将事件分发到外层的控件ScrollView,也就导致外部的ScrollView无法滚动。即在不支持嵌套滑动时,无法将事件向外层事件分发。
public class ScrollView extends FrameLayout {
public class RecyclerView extends ViewGroup implements ScrollingView,
NestedScrollingChild2, NestedScrollingChild3 {
public class NestedScrollView extends FrameLayout implements NestedScrollingParent3,
NestedScrollingChild3, ScrollingView {
(1)固定位置
(2)事件拦截
(3)备胎
(4)将tablayout+viewpager的高度设置为屏幕的高度
/**
* 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);
}
(1)希望达到的效果
如果在滑动RecyclerView的时候,可以先判断一下NestedScrollView是否还可以继续滑动,如果可以继续滑动,则先让其向上滑动完成,当他不能往上滑的时候,就让RecyclerView自己滑。
(2)孩子滑动有3个版本,3继承2,2继承1
一种是TYPE_NON_TOUCH:手指滑动
public interface NestedScrollingChild3 extends NestedScrollingChild2 {
public interface NestedScrollingChild2 extends NestedScrollingChild {
(3)父亲与孩子的关系
嵌套滑动虽然有父亲与孩子两个角色,但是主动者是孩子,事情是由孩子触发的。
如上图,如果在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只支持垂直方向的滑动,横向的不支持。
在滑动之前会执行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;
}
}
}
(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);
}
}
//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;
}
});
(1)孩子会接收到一堆事件,在ACTION_DOWN的时候,告诉父亲,我要这个事件,不要拦截我的事件。
androidx.recyclerview.widget.RecyclerView#requestDisallowInterceptTouchEvent
(2)需要满足一定条件时才会去请求。
(3)外部拦截,除非是自己写一个View才会使用。没必要使用外部拦截,即自己判断是否需要,如果需要,则不给孩子事件。
感谢您的细心阅读,您的鼓励是我写作的不竭动力!!!