赞
踩
最近想学习一下自定义View,发现其中有个麻烦的滑动冲突处理问题,为了应对滑动冲突,决定先研究一下Android的事件传递机制。
首先开局一张图,借鉴一篇神文中的图Android事件分发机制详解:史上最全面、最易懂
如果能将上面这张图脱稿画出,基本上事件传递机制也掌握地差不多了。
上图中,将事件传递分为三个模块,一般情况下也就主要是这几个模块的组合,无非是
所以只需要将第一种最经典的事件传递机制搞清楚了,其他的组合也都类似的处理即可。
首先分析一下各个模块中的关键方法:
针对上面分析的方法,再回过头去看图,应该会有更清晰的认识。但程序中所有的认识都是基于实操而加深印象的,任何没有代码的分析都是扯淡,所以要想更深入地了解事件传递机制,只有一个办法,代码验证,虽然通过打log验证的方案很low,但对于理解事件分发流程是没有任何阻碍的,所以这里也就不多废话了,直接上用来测试的代码,大家可以根据测试代码自行验证。
很简单的自定义Button,只是重写了dispatchTouchEvent和onTouchEvent方法而已。
import android.content.Context; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import androidx.appcompat.widget.AppCompatButton; public class CustomButton extends AppCompatButton { public CustomButton(Context context) { super(context); } public CustomButton(Context context, AttributeSet attrs) { super(context, attrs); } public CustomButton(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } /** * 当ViewGroup返回super时调用 * */ @Override public boolean dispatchTouchEvent(MotionEvent event) { Log.e("CustomButton", "CustonButton-----------dispatchTouchEvent"); /** * 默认处理 * 事件扔给onTouchEvent处理 * */ return super.dispatchTouchEvent(event); /** * return true * 表示事件在这里被消费掉了,后续不会出现任何与该事件相关的处理 * */ // return true; /** * return false * 表示事件不再分发,扔给viewGroup的onTouchEvent处理 * */ // return false; } /** * 当dispatchTouchEvent返回super时调用 * */ @Override public boolean onTouchEvent(MotionEvent event) { Log.e("CustomButton", "CustonButton-----------onTouchEvent"); /** * 默认处理 * 调用performClick处理 * */ return super.onTouchEvent(event); /** * return true * 表示事件在这里被消费掉了,后续不会出现任何与该事件相关的处理 * */ // return true; /** * return false * 表示事件在这里不做处理,不会调用click事件 * 而是将事件向上抛,扔给viewGroup的onTouchEvent处理 * */ // return false; } }
很简单的自定义布局,继承自线性布局,重写了dispatchTouchEvent,onInterceptTouchEvent和onTouchEvent方法。
import android.content.Context; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.widget.LinearLayout; public class CustomLayout extends LinearLayout { public CustomLayout(Context context) { super(context); } public CustomLayout(Context context, AttributeSet attrs) { super(context, attrs); } public CustomLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } /** * 当Activity的dispatchTouchEvent返回super时调用 * */ @Override public boolean dispatchTouchEvent(MotionEvent ev) { Log.e("CustomLayout", "CustomLayout------------------dispatchTouchEvent"); /** * 默认处理 * 因为是viewGroup,拥有众多子View * 所以调用onInterceptTouchEvent方法判断是否拦截该事件 * */ return super.dispatchTouchEvent(ev); /** * return true * 表示事件在这里被消费掉了,后续不会出现任何与该事件相关的处理 * */ // return true; /** * return false * 表示事件不再分发,扔给Activity的onTouchEvent处理 * */ // return false; } /** * 只做事件拦截,不消费事件 * 但dispatchTouchEvent返回super时调用 * */ @Override public boolean onInterceptTouchEvent(MotionEvent ev) { Log.e("CustomLayout", "CustomLayout------------------onInterceptTouchEvent"); /** * 默认处理 * 默认不拦截,事件下发到View的dispatchTouchEvent中处理 * */ return super.onInterceptTouchEvent(ev); /** * return true * 拦截事件,事件在onTouchEvent中处理 * */ // return true; /** * return false * 不拦截,事件下发到View的dispatchTouchEvent中处理 * */ // return false; } /** * 当onInterceptTouchEvent返回true时调用 * */ @Override public boolean onTouchEvent(MotionEvent event) { Log.e("CustomLayout", "CustomLayout------------------onTouchEvent"); /** * 默认处理 * 调用performClick处理 * */ return super.onTouchEvent(event); /** * return true * 表示事件在这里被消费掉了,后续不会出现任何与该事件相关的处理 * */ // return true; /** * return false * 表示事件在这里不做处理,不会调用click事件 * 而是将事件向上抛,扔给Activity的onTouchEvent处理 * */ // return false; } }
最基本的Activity,重写了dispatchTouchEvent和onTouchEvent方法,并对控件设置了点击监听。
import androidx.appcompat.app.AppCompatActivity; import androidx.constraintlayout.widget.ConstraintLayout; import android.os.Bundle; import android.util.Log; import android.view.MotionEvent; import android.view.View; public class MainActivity extends AppCompatActivity implements View.OnClickListener{ CustomButton customButton; CustomLayout customLayout; ConstraintLayout activityView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); customButton = findViewById(R.id.custom_button); customButton.setOnClickListener(this); customLayout = findViewById(R.id.custom_layout); customLayout.setOnClickListener(this); activityView = findViewById(R.id.activity_view); activityView.setOnClickListener(this); } /** * 事件分发的入口,Touch事件的分发就是从这开始的 * 一旦这里返回true,事件就被消费了,后续将再也收不到任何事件 * */ @Override public boolean dispatchTouchEvent(MotionEvent ev) { Log.e("MainActivity", "MainActivity----------dispatchTouchEvent"); /** * 默认处理 * 将事件下发给ViewGroup的dispatchTouchEvent处理 * */ return super.dispatchTouchEvent(ev); /** * return true * 表示事件在这里被消费掉了,后续不会出现任何与该事件相关的处理 * */ // return true; /** * return false * 理论上应该交给DecorView的onTouchEvent处理 * 一般也就代表被消费了,后续不会出现任何与该事件相关的处理 * */ // return false; } /** * 当view和viewGroup的onTouchEvent都返回false,即他们都不消费事件时 * 事件被抛到最上层Activity处理 * */ @Override public boolean onTouchEvent(MotionEvent event) { Log.e("MainActivity", "MainActivity----------onTouchEvent"); /** * 默认处理 * 调用performClick处理 * */ return super.onTouchEvent(event); /** * return true * 表示事件在这里被消费掉了,后续不会出现任何与该事件相关的处理 * */ // return true; /** * return false * 表示事件在这里不做处理 * 而是将事件向上抛,但因为上面都不处理 * 所以无论这里返回什么,事件分发到此都结束了 * 实践证明,即使这里返回false,还是会调用Activity的click方法(这里还需研究一下) * */ // return false; } @Override public void onClick(View view) { switch (view.getId()) { case R.id.custom_button: Log.e("MainActivity", "Button------onClick: button clicked!"); break; case R.id.custom_layout: Log.e("MainActivity", "Layout------onClick: layout clicked!"); break; case R.id.activity_view: Log.e("MainActivity", "Activity------onClick: activity clicked!"); break; } } }
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/activity_view" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <com.skydianshi.toucheventtest.CustomLayout android:id="@+id/custom_layout" android:layout_width="match_parent" android:layout_height="500dp" android:background="@color/colorAccent" app:layout_constraintTop_toTopOf="parent"> <com.skydianshi.toucheventtest.CustomButton android:id="@+id/custom_button" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="center" android:text="Button"/> </com.skydianshi.toucheventtest.CustomLayout> </androidx.constraintlayout.widget.ConstraintLayout>
针对不同的返回值在代码中都有对应的解释,读者可以放开对应的返回值调试,体验不同返回值下的事件传递机制,体验完了再去看一下最上面的图,就更明白其中所述了。
最终效果就是能默写出事件传递图就基本差不多啦,想要深入到源码探究的,可以点进开头的那篇神文中,其中做了最详尽的解释。
下面通过一个滑动冲突的解决demo来实践一下事件传递,相信通过这个案例,应该可以更好地理解上文所述。
上图中A为横向ScrollView,B为纵向ScrollView,A在B中,需要实现的效果很简单:
前两个效果很简单,只要正常添加好滚动布局就行了,系统自动就是这种效果。但要实现第三个效果,就要解决A和B的横向纵向滑动冲突问题。
先分析一下怎么实现,到底需要修改哪个布局的哪个方法?肉眼就能看出是A布局,通俗点说,就是在A布局中纵向滑动时不处理事件,把事件扔给B处理不就行了嘛。
对,就是这个意思,老规矩,直接看代码:
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".TouchTestActivity"> <ScrollView android:layout_width="match_parent" android:layout_height="match_parent"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:background="#999999" android:orientation="vertical"> <com.skydianshi.toucheventtest.CustomHorizontalScrollView android:id="@+id/scroll_horizon" android:layout_width="match_parent" android:layout_height="wrap_content" app:layout_constraintTop_toTopOf="parent"> <LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="#ffffff" android:orientation="horizontal"> <TextView android:layout_width="3000dp" android:layout_height="400dp" android:layout_gravity="center" android:gravity="center" android:text="hello world" android:textSize="50sp" android:textColor="#000000"/> </LinearLayout> </com.skydianshi.toucheventtest.CustomHorizontalScrollView> <TextView android:layout_width="match_parent" android:layout_height="800dp" android:layout_gravity="center" android:gravity="center" android:textSize="50sp" android:textColor="#000000" android:text="HELLO WORLD"/> </LinearLayout> </ScrollView> </androidx.constraintlayout.widget.ConstraintLayout>
布局文件很简单,就是两个滚动布局的叠加,并在其中添加了一些控件。
import android.content.Context; import android.util.AttributeSet; import android.view.MotionEvent; import android.widget.HorizontalScrollView; public class CustomHorizontalScrollView extends HorizontalScrollView { private float x1 = 0.0f; private float x2 = 0.0f; private float y1 = 0.0f; private float y2 = 0.0f; public CustomHorizontalScrollView(Context context) { super(context); } public CustomHorizontalScrollView(Context context, AttributeSet attrs) { super(context, attrs); } public CustomHorizontalScrollView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @Override public boolean onTouchEvent(MotionEvent ev) { boolean isHandleEvent = true; switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: x1 = ev.getX(); y1 = ev.getY(); break; case MotionEvent.ACTION_MOVE: x2 = ev.getX(); y2 = ev.getY(); isHandleEvent = judgeHorizonMove(x1, x2, y1, y2); x1 = ev.getX(); y1 = ev.getY(); break; case MotionEvent.ACTION_UP: break; } if (isHandleEvent) { // 如果横向移动,就自己处理 return super.onTouchEvent(ev); } else { // 如果纵向移动,就不处理,扔给父布局处理 return false; } } @Override public boolean dispatchTouchEvent(MotionEvent ev) { // 也可以把上面的判断处理放到这里,如果横向移动,则分发事件,否则拒绝分发,抛给父布局处理。 return super.dispatchTouchEvent(ev); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { /** * 也可以把上面的判断处理放到这里,如果横向移动,则拦截事件自己处理,否则就分发下去 * 但由于分发下去也没人处理,因此还是会将事件向上抛 * */ return super.onInterceptTouchEvent(ev); } /** * 判断是否横向移动 * */ private boolean judgeHorizonMove(float x3, float x4, float y3, float y4) { if (Math.abs(x4 - x3) > Math.abs(y4 - y3)) { return true; } return false; } }
关键的处理就是在这个自定义横向滚动布局中,在这里重写了onTouchEvent方法,当然也可以重写dispatchTouchEvent或者onInterceptTouchEvent方法,都可以实现效果,代码比较简单,关键地方也做了解释,就不啰嗦了。感兴趣的同学可以自己写一下,感受一下事件传递的乐趣,还挺有意思的。
本来想传一下效果图的,拍了一下发现太大,传不了,就不传了,各位可以自行尝试!
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。