中交建设集团网站分公司,成都市温江区建设局网站,老鹰网站建设,软件定制开发优势文章目录为什么要理解Android事件分发机制#xff1f;滑动冲突类问题我们以什么开始#xff1f;代码如下#xff1a;activity xml 代码#xff1a;Activity代码#xff1a;item_user.xml代码修改后代码如下#xff1a;Activity xmlactivity代码item_gift.xml问题出现了An…
文章目录为什么要理解Android事件分发机制滑动冲突类问题我们以什么开始代码如下activity xml 代码Activity代码item_user.xml代码修改后代码如下Activity xmlactivity代码item_gift.xml问题出现了Android 事件分发机制实现我们的功能再看一下我们面临的问题尝试解决1、 从OnTouchListener入手2、重写RecyclerView的OnTouch方法3、重写RecyclerView的OnTouch方法并调用其onTouch方法。尝试其他方案1、 在onInterceptTouchEvent中直接返回false2、直接在dispatchTouchEvent中处理可以吗3、在OnTouch中依然直接返回false同时自己重置scrollState可以吗为什么要理解Android事件分发机制
我最开始去了解Android事件分发机制是在遇到滑动冲突的时候相信很多朋友也是在遇到滑动冲突时候去深入了解该机制的但是实际上除了解决滑动冲突它还能处理很多复杂的问题。以下将列出一系列相关问题后面会根据举几个笔者最近遇到的问题以及解决的思路。
滑动冲突类问题
RecyclerView 与 ScrollView嵌套使用同方向上的滑动冲突ViewPager 与 水平滚动相关View的联合使用 事件传递时机类问题如何让RecyclerView无法滚动如何让RecyclerView的子项无法点击如何在RecyclerView重叠使用时设置了点击事件的View都可以响应 ……
我们以什么开始
这里我先设定一个需求背景 我有一个用户头像列表上边会展示用户的基本信息用户头像头像装饰用户名点击用户头像可以弹出用户信息这里用Toast代替。目前实现的UI如下
代码如下
activity xml 代码
?xml version1.0 encodingutf-8?
FrameLayout xmlns:androidhttp://schemas.android.com/apk/res/androidxmlns:apphttp://schemas.android.com/apk/res-autoxmlns:toolshttp://schemas.android.com/toolsandroid:layout_widthmatch_parentandroid:layout_heightmatch_parenttools:context.event.EventDispatchActivityandroidx.recyclerview.widget.RecyclerViewandroid:layout_marginTop20dpandroid:idid/rv_usersandroid:layout_widthmatch_parentandroid:layout_heightmatch_parent//FrameLayoutActivity代码
class EventDispatchActivity : AppCompatActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_event_dispatch)val itemDecoration object : ItemDecoration(){override fun getItemOffsets(outRect: Rect,view: View,parent: RecyclerView,state: RecyclerView.State) {super.getItemOffsets(outRect, view, parent, state)outRect.bottom 40}}findViewByIdRecyclerView(R.id.rv_users)?.let {it.adapter UserAdapter()it.layoutManager GridLayoutManager(it.context, 4)it.addItemDecoration(itemDecoration)}}class UserAdapter : RecyclerView.AdapterUserAdapter.ViewHolder() {class ViewHolder(view: View): RecyclerView.ViewHolder(view) {val ivUser itemView.findViewByIdImageView(R.id.iv_user)val tvUser itemView.findViewByIdTextView(R.id.tv_name)}override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {return ViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_user, parent, false))}override fun onBindViewHolder(holder: ViewHolder, position: Int) {val name user $positionholder.ivUser.setOnClickListener {Toast.makeText(holder.ivUser.context, name, Toast.LENGTH_SHORT).show()}holder.tvUser.text name}override fun getItemCount(): Int {return 16}}
}item_user.xml代码
?xml version1.0 encodingutf-8?
LinearLayout xmlns:androidhttp://schemas.android.com/apk/res/androidxmlns:apphttp://schemas.android.com/apk/res-autoandroid:layout_widthmatch_parentandroid:layout_heightwrap_contentandroid:orientationverticalandroid:gravitycenter_horizontalImageViewandroid:layout_width48dpandroid:layout_height48dpandroid:idid/iv_userandroid:srcdrawable/userapp:tintcolor/purple_500 /TextViewandroid:layout_marginTop10dpandroid:layout_widthwrap_contentandroid:layout_height20dpandroid:idid/tv_nameandroid:text用户名android:singleLinetrue//LinearLayout现在有一个新的需求需要在每两个头像之间加上红包展示如果点击红包可以弹出红包弹窗此处用Toast代替。方案当然可以选择在原来的Item上加中间的红包但是此处我选择的方案是新创建一个RecyclerView专门展示红包。不要考虑方案的优劣只是当时情况需要选择此方案新的UI展示如下 修改后代码如下
Activity xml
?xml version1.0 encodingutf-8?
FrameLayout xmlns:androidhttp://schemas.android.com/apk/res/androidxmlns:apphttp://schemas.android.com/apk/res-autoxmlns:toolshttp://schemas.android.com/toolsandroid:layout_widthmatch_parentandroid:layout_heightmatch_parenttools:context.event.EventDispatchActivityandroidx.recyclerview.widget.RecyclerViewandroid:layout_marginTop20dpandroid:idid/rv_usersandroid:layout_widthmatch_parentandroid:layout_heightmatch_parent/androidx.recyclerview.widget.RecyclerViewandroid:idid/rv_user_giftandroid:layout_marginTop20dpandroid:layout_widthmatch_parentandroid:layout_heightmatch_parent//FrameLayoutactivity代码
class EventDispatchActivity : AppCompatActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_event_dispatch)val itemDecoration object : ItemDecoration(){override fun getItemOffsets(outRect: Rect,view: View,parent: RecyclerView,state: RecyclerView.State) {super.getItemOffsets(outRect, view, parent, state)outRect.bottom 40}}findViewByIdRecyclerView(R.id.rv_users)?.let {it.adapter UserAdapter()it.layoutManager GridLayoutManager(it.context, 4)it.addItemDecoration(itemDecoration)}findViewByIdRecyclerView(R.id.rv_user_gift)?.let {it.adapter GiftAdapter()it.layoutManager GridLayoutManager(it.context, 2)it.addItemDecoration(itemDecoration)}}class UserAdapter : RecyclerView.AdapterUserAdapter.ViewHolder() {class ViewHolder(view: View): RecyclerView.ViewHolder(view) {val ivUser itemView.findViewByIdImageView(R.id.iv_user)val tvUser itemView.findViewByIdTextView(R.id.tv_name)}override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {return ViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_user, parent, false))}override fun onBindViewHolder(holder: ViewHolder, position: Int) {val name user $positionholder.ivUser.setOnClickListener {Toast.makeText(holder.ivUser.context, name, Toast.LENGTH_SHORT).show()}holder.tvUser.text name}override fun getItemCount(): Int {return 16}}class GiftAdapter : RecyclerView.AdapterGiftAdapter.ViewHolder() {class ViewHolder(view: View): RecyclerView.ViewHolder(view) {val giftView itemView.findViewByIdImageView(R.id.iv_gift)}override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {return ViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_gift, parent, false))}override fun onBindViewHolder(holder: ViewHolder, position: Int) {val gift gift $positionholder.giftView.setOnClickListener {Toast.makeText(holder.giftView.context, gift, Toast.LENGTH_SHORT).show()}}override fun getItemCount(): Int {return 8}}}item_gift.xml
?xml version1.0 encodingutf-8?
LinearLayout xmlns:androidhttp://schemas.android.com/apk/res/androidandroid:layout_widthmatch_parentandroid:layout_height78dpandroid:orientationverticalandroid:gravitycenter_horizontalImageViewandroid:idid/iv_giftandroid:layout_width36dpandroid:layout_height36dpandroid:layout_marginTop6dpandroid:srcdrawable/gift//LinearLayout问题出现了
按照上面的实现展示上是没问题的但是用户操作上出现了问题。 首先说一下我想要的结果
用户头像可点击点击后可出用户信息弹窗红包可点击点击后可出红包弹窗
现在的问题是 红包可点击符合需求但是用户头像不可点击(不符合需求)了。
问题原因推测
只有用户列表时用户信息是可以点击的加上新的列表后用户信息点不了应该是点击事件被顶层的红包层拦截了。目标方案顶层只有红包区域可点击其他位置要正常传递给下面的头像。
但是我应该从哪开始改起呢带着上面的问题我们开始研究分发机制达到最终上面的效果。
Android 事件分发机制
Android 的触摸事件是从屏幕硬件触发最终到达目前正在展示的Activity的至于这期间如何传递的此处不做讨论我们讨论的起点从Activity接收到触摸事件开始。 流程图 参考文档 Android 事件传递相关流程图 源码分析 参考文档 Android 事件分发源码解析基于API31
实现我们的功能
事件传递相关的流程与 源码分析在上面的文档中已经看到了接下来我们来实现一下文章开头提到的实现目标。
根据问题的前提我们知道这是两个RecyclerView重叠摆放的问题导致的当然我们可以自定义ViewGroup重新定义两个View的摆放规则但是那不在我们本次讨论范围内我们只考虑当前的实现方案如何最终实现这个功能。
再看一下我们面临的问题
最上层的RecyclerView会拦截所有的事件分发所以最上层的RecyclerView 中的View会被分发到事件而下边的RecyclerView不能接收到事件。 那么有没有什么办法可以让上边的RecyclerView 只在有View的地方响应点击事件
尝试解决
1、 从OnTouchListener入手
首先肯定是不会考虑重写一个View的那么最简单的方案是设置一个OnTouchListener但是这种方案可不可以呢不可以。 原因如果我在OnTouchListener的onTouch方法返回true那么礼物的点击事件无法执行了如果返回false和没加一样。
2、重写RecyclerView的OnTouch方法
既然没法子通过OnTouchListener处理那么修改onTouchEvent方法直接返回false也就是不处理点击事件但是让RecyclerView只分发不处理点击事件这可行不可行呢
class DispatchRecyclerView : RecyclerView {constructor(context: Context) : super(context)constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context,attrs,defStyleAttr)override fun onTouchEvent(e: MotionEvent?): Boolean {return false}
}尝试修改了一下是可行的。 但是出现了一个新的问题多次极端滑动(一直往下滑但实际上它下面没有内容了)之后不再执行到RecyclerView的onTouch方法了因为它的滑动状态一直没有被重置成IDLE一直是DRAGING状态。DRAGING状态时它的OnTouch里边返回false也没用因为在拦截那一步已经被处理成拦截了。
也就是在某一状态下RecyclerView的scrollState 变成了 DRAGGING且onInterceptTouchEvent反回了true。而RecyclerView 没有重写dispatchTouchEvent那么哦我们知道如果“onInterceptTouchEvent”返回了true那么它将不会进行事件分发。
原因是在onInterceptToucEvent中RecyclerView把scrollState置成了DRAGGING在ACTION_UP的时候是在onTouch里边重置的滚动状态而我们直接在onTouch中返回了false导致重置scrollState步骤没有执行到。
3、重写RecyclerView的OnTouch方法并调用其onTouch方法。
class DispatchRecyclerView : RecyclerView {constructor(context: Context) : super(context)constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context,attrs,defStyleAttr)override fun onTouchEvent(e: MotionEvent?): Boolean {super.onTouchEvent(e) // 执行操作return false // 同时让父布局继续往下分发}
}当我们添加了该方法发现问题解决了~
尝试其他方案
1、 在onInterceptTouchEvent中直接返回false
在这个方法中返回false不行因为我们知道即使不拦截如果RecyclerView发现没有可以处理事件的子View最后仍然会回调自己的onTouchEvent。
2、直接在dispatchTouchEvent中处理可以吗
其实是可以的这是所有事件分发的入口但是如果我们重写整个方法需要把整个找子View的过程都写一遍这得不偿失。
3、在OnTouch中依然直接返回false同时自己重置scrollState可以吗
这种方式是可以的但需要写的代码可能会更多。