2024年Android最全RecyclerView 事件分发原理实战分析(1),安卓中高级最新面试题

文末

对于很多初中级Android工程师而言,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。 整理的这些架构技术希望对Android开发的朋友们有所参考以及少走弯路,本文的重点是你有没有收获与成长,其余的都不重要,希望读者们能谨记这一点。

最后想要拿高薪实现技术提升薪水得到质的飞跃。最快捷的方式,就是有人可以带着你一起分析,这样学习起来最为高效,所以为了大家能够顺利进阶中高级、架构师,我特地为大家准备了一套高手学习的源码和框架视频等精品Android架构师教程,保证你学了以后保证薪资上升一个台阶。

当你有了学习线路,学习哪些内容,也知道以后的路怎么走了,理论看多了总要实践的。

进阶学习视频

附上:我们之前因为秋招收集的二十套一二线互联网公司Android面试真题 (含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

在一个视频通话界面中,放置一个发言方列表,这个列表支持横向滑动,称为小窗列表, 处于背景的窗口称之大窗,当用户想将小窗列表中的某一个 item 切换到大窗时,可以使用手指触摸想要切换的 item, 并向上方滑动,即可将选定的小窗切换至大窗位置,而且上滑需要支持垂直向上和斜向上的方向。

原始解决方案


解决方案

原始解决方案是为 item view 设置 OnTouchListener 方法, 在其 onTouch() 方法中的 ACTION_MOVE 事件中判断 dy (Y 坐标偏移量) 是否大于某个阈值。

遇到的问题

遇到的问题是当在 item 斜向上滑动时,item view 收到的 ACTION_MOVE 事件的 dy 总是特别小,即使你确定已经滑动了很多时

问题定位 & 怀疑


  • 该问题定位为 在横向滑动时,RecyclerView 与 item 发生了嵌套滑动冲突

  • 怀疑是 RecyclerView 消费了部分滑动事件,导致 item view 收到的滑动距离特别小。

尝试新的解决方案


通过翻阅源码发现,RecyclerView 内部提供了 OnItemTouchListener, 介绍如下:

/**

  • An OnItemTouchListener allows the application to intercept touch events in progress at the

  • view hierarchy level of the RecyclerView before those touch events are considered for

  • RecyclerView’s own scrolling behavior.

  • This can be useful for applications that wish to implement various forms of gestural

  • manipulation of item views within the RecyclerView. OnItemTouchListeners may intercept

  • a touch interaction already in progress even if the RecyclerView is already handling that

  • gesture stream itself for the purposes of scrolling.

  • @see SimpleOnItemTouchListener

*/

public static interface OnItemTouchListener{

/**

  • Silently observe and/or take over touch events sent to the RecyclerView

  • before they are handled by either the RecyclerView itself or its child views.

  • The onInterceptTouchEvent methods of each attached OnItemTouchListener will be run

  • in the order in which each listener was added, before any other touch processing

  • by the RecyclerView itself or child views occurs.

  • @param e MotionEvent describing the touch event. All coordinates are in

  •      the RecyclerView's coordinate system.
    
  • @return true if this OnItemTouchListener wishes to begin intercepting touch events, false

  •     to continue with the current behavior and continue observing future events in
    
  •     the gesture.
    

*/

public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e);

}

OnItemTouchListener 的作用主要有两个:

  1. 在 RecyclerView 对事件消费之前,给予开发者自定义事件分发算法的权利。

  2. 当 RecyclerView 已经在对事件消费过程中时,可以通过本类对 RecylerView 正在处理的事件序列进行拦截。

本文提到的问题看似可以解决,思路就是为 RecyclerView 添加 OnItemTouchListener, 在其 onInterceptTouchEvent(RecyclerView rv, MotionEvent e) 调用时判断,如果 Y 轴的偏移量大于某一阈值,表明当前用户想触发窗口置换操作,那么就在 onInterceptTouchEvent() 中返回 false, 我们期望 RecyclerView 完全不消费事件,使事件下沉到 RecyclerView 的 item view 中,那么 item 就可以正常获取到 MOVE 事件,部分代码如下:

/**

  • 纵坐标偏移量阈值

*/

private final int Y_AXIS_MOVE_THRESHOLD = 15;

private int downY = 0;

@Override

public boolean onInterceptTouchEvent(MotionEvent e) {

if (e.getAction() == MotionEvent.ACTION_DOWN) {

downY = (int) e.getRawY();

} else if (e.getAction() == MotionEvent.ACTION_MOVE) {

int realtimeY = (int) e.getRawY();

int dy = Math.abs(downY - realtimeY);

if (dy > Y_AXIS_MOVE_THRESHOLD) {

return false;

}

}

return true;

}

但其实这样是无法实现需求的,因为如果按照我们目前的实现方案,是期望在 dy 大于阈值时,RecyclerView 可以完全对 MOVE 事件放手,将事件下沉到 item view 中去处理,根据事件分发规则,这就需要 RecyclerView 的 onInterceptTouchEvent() return false,然后子 View 即 item view 的 onTouchEvent() 会被调用。进而实现窗口置换,下面我们来通过源码分析为什么这种方案不能实现。

RecyclerView 事件分发代码分析


@Override

public boolean onInterceptTouchEvent(MotionEvent e) {

if (mLayoutFrozen) {

// When layout is frozen, RV does not intercept the motion event.

// A child view e.g. a button may still get the click.

return false;

}

if (dispatchOnItemTouchIntercept(e)) {

cancelTouch();

return true;

}

if (mLayout == null) {

return false;

}

final boolean canScrollHorizontally = mLayout.canScrollHorizontally();

final boolean canScrollVertically = mLayout.canScrollVertically();

if (mVelocityTracker == null) {

mVelocityTracker = VelocityTracker.obtain();

}

mVelocityTracker.addMovement(e);

final int action = MotionEventCompat.getActionMasked(e);

final int actionIndex = MotionEventCompat.getActionIndex(e);

switch (action) {

case MotionEvent.ACTION_DOWN:

if (mIgnoreMotionEventTillDown) {

mIgnoreMotionEventTillDown = false;

}

mScrollPointerId = e.getPointerId(0);

mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f);

mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f);

if (mScrollState == SCROLL_STATE_SETTLING) {

getParent().requestDisallowInterceptTouchEvent(true);

setScrollState(SCROLL_STATE_DRAGGING);

}

// Clear the nested offsets

mNestedOffsets[0] = mNestedOffsets[1] = 0;

int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;

if (canScrollHorizontally) {

nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;

}

if (canScrollVertically) {

nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;

}

startNestedScroll(nestedScrollAxis);

break;

case MotionEventCompat.ACTION_POINTER_DOWN:

mScrollPointerId = e.getPointerId(actionIndex);

mInitialTouchX = mLastTouchX = (int) (e.getX(actionIndex) + 0.5f);

mInitialTouchY = mLastTouchY = (int) (e.getY(actionIndex) + 0.5f);

break;

case MotionEvent.ACTION_MOVE: {

final int index = e.findPointerIndex(mScrollPointerId);

if (index < 0) {

Log.e(TAG, "Error processing scroll; pointer index for id " +

mScrollPointerId + " not found. Did any MotionEvents get skipped?");

return false;

}

final int x = (int) (e.getX(index) + 0.5f);

final int y = (int) (e.getY(index) + 0.5f);

if (mScrollState != SCROLL_STATE_DRAGGING) {

final int dx = x - mInitialTouchX;

final int dy = y - mInitialTouchY;

boolean startScroll = false;

if (canScrollHorizontally && Math.abs(dx) > mTouchSlop) {

mLastTouchX = mInitialTouchX + mTouchSlop * (dx < 0 ? -1 : 1);

startScroll = true;

}

if (canScrollVertically && Math.abs(dy) > mTouchSlop) {

mLastTouchY = mInitialTouchY + mTouchSlop * (dy < 0 ? -1 : 1);

startScroll = true;

}

if (startScroll) {

setScrollState(SCROLL_STATE_DRAGGING);

}

}

} break;

case MotionEventCompat.ACTION_POINTER_UP: {

onPointerUp(e);

} break;

case MotionEvent.ACTION_UP: {

mVelocityTracker.clear();

stopNestedScroll();

} break;

case MotionEvent.ACTION_CANCEL: {

cancelTouch();

架构师筑基包括哪些内容

我花了将近半个月时间将:深入 Java 泛型.、注解深入浅出、并发编程.、数据传输与序列化、Java 虚拟机原理、反射与类加载、高效 IO、Kotlin项目实战等等Android架构师筑基必备技能整合成了一套系统知识笔记PDF,相信看完这份文档,你将会对这些Android架构师筑基必备技能有着更深入、更系统的理解。

由于文档内容过多,为了避免影响到大家的阅读体验,在此只以截图展示部分内容

注:资料与上面思维导图一起看会更容易学习哦!每个点每个细节分支,都有对应的目录内容与知识点!



这份资料就包含了所有Android初级架构师所需的所有知识!

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

,在此只以截图展示部分内容**

注:资料与上面思维导图一起看会更容易学习哦!每个点每个细节分支,都有对应的目录内容与知识点!

[外链图片转存中…(img-JHOuvPVK-1715623858019)]
[外链图片转存中…(img-RAkAt4bo-1715623858019)]
这份资料就包含了所有Android初级架构师所需的所有知识!

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 29
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值