在 ViewGroup
中处理轻触事件时需要特别小心,因为 ViewGroup
通常会有子视图,而这些子视图是除 ViewGroup
本身以外的其他轻触事件的目标。为了确保每个视图都能够正确地收到专门发给它的轻触事件,请替换 onInterceptTouchEvent()
方法。
在 ViewGroup 中拦截轻触事件
只要在 ViewGroup
的 surface(包括其子视图的 surface)上检测到轻触事件,系统就会调用 onInterceptTouchEvent()
方法。如果 onInterceptTouchEvent()
返回 true
,系统会拦截 MotionEvent
,这意味着系统不会将其传递给子视图,而是传递给父视图的 onTouchEvent()
方法。
onInterceptTouchEvent()
方法可以让父视图先于子视图检测到任何轻触事件。如果您从 onInterceptTouchEvent()
返回 true
,则之前处理轻触事件的子视图会收到 ACTION_CANCEL
,并且从此时起,系统会将事件发送到父视图的 onTouchEvent()
方法以进行常规处理。onInterceptTouchEvent()
也可能会返回 false
,并在事件按照视图层次结构传递至常规目标的过程中进行监视,而这些目标会使用自己的 onTouchEvent()
处理事件。
在以下代码段中,类 MyViewGroup
扩展了 ViewGroup
。MyViewGroup
包含多个子视图。如果您在子视图上横向拖动手指,则子视图应该不再获取轻触事件,而 MyViewGroup
应该通过滚动内容来处理轻触事件。不过,如果您按子视图中的按钮,或纵向滚动子视图,则父视图不应该拦截这些轻触事件,因为子视图是预期目标。在这些情况下,onInterceptTouchEvent()
应该返回 false
,并且系统不会调用 MyViewGroup
的 onTouchEvent()
。
Kotlin
class MyViewGroup @JvmOverloads constructor( context: Context, private val mTouchSlop: Int = ViewConfiguration.get(context).scaledTouchSlop ) : ViewGroup(context) { ... override fun onInterceptTouchEvent(ev: MotionEvent): Boolean { /* * This method JUST determines whether we want to intercept the motion. * If we return true, onTouchEvent will be called and we do the actual * scrolling there. */ return when (ev.actionMasked) { // Always handle the case of the touch gesture being complete. MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> { // Release the scroll. mIsScrolling = false false // Do not intercept touch event, let the child handle it } MotionEvent.ACTION_MOVE -> { if (mIsScrolling) { // We're currently scrolling, so yes, intercept the // touch event! true } else { // If the user has dragged her finger horizontally more than // the touch slop, start the scroll // left as an exercise for the reader val xDiff: Int = calculateDistanceX(ev) // Touch slop should be calculated using ViewConfiguration // constants. if (xDiff > mTouchSlop) { // Start scrolling! mIsScrolling = true true } else { false } } } ... else -> { // In general, we don't want to intercept touch events. They should be // handled by the child view. false } } } override fun onTouchEvent(event: MotionEvent): Boolean { // Here we actually handle the touch event (e.g. if the action is ACTION_MOVE, // scroll this container). // This method will only be called if the touch event was intercepted in // onInterceptTouchEvent ... } }
Java
public class MyViewGroup extends ViewGroup { private int mTouchSlop; ... ViewConfiguration vc = ViewConfiguration.get(view.getContext()); mTouchSlop = vc.getScaledTouchSlop(); ... @Override public boolean onInterceptTouchEvent(MotionEvent ev) { /* * This method JUST determines whether we want to intercept the motion. * If we return true, onTouchEvent will be called and we do the actual * scrolling there. */ final int action = MotionEventCompat.getActionMasked(ev); // Always handle the case of the touch gesture being complete. if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { // Release the scroll. mIsScrolling = false; return false; // Do not intercept touch event, let the child handle it } switch (action) { case MotionEvent.ACTION_MOVE: { if (mIsScrolling) { // We're currently scrolling, so yes, intercept the // touch event! return true; } // If the user has dragged her finger horizontally more than // the touch slop, start the scroll // left as an exercise for the reader final int xDiff = calculateDistanceX(ev); // Touch slop should be calculated using ViewConfiguration // constants. if (xDiff > mTouchSlop) { // Start scrolling! mIsScrolling = true; return true; } break; } ... } // In general, we don't want to intercept touch events. They should be // handled by the child view. return false; } @Override public boolean onTouchEvent(MotionEvent ev) { // Here we actually handle the touch event (e.g. if the action is ACTION_MOVE, // scroll this container). // This method will only be called if the touch event was intercepted in // onInterceptTouchEvent ... } }
请注意,ViewGroup
还提供了 requestDisallowInterceptTouchEvent()
方法。当子视图不想让父视图及其祖先视图使用 onInterceptTouchEvent()
拦截轻触事件时,ViewGroup
便会调用此方法。
处理 ACTION_OUTSIDE 事件
如果 ViewGroup
收到带有 ACTION_OUTSIDE
的 MotionEvent
,则默认情况下系统不会将该事件发送给其子视图。如需处理带有 ACTION_OUTSIDE
的 MotionEvent
,可以替换 dispatchTouchEvent(MotionEvent event)
以将其发送给相应的 View
,或在相关 Window.Callback
(例如 Activity
)中对其进行处理。
使用 ViewConfiguration 常量
上面的代码段使用当前 ViewConfiguration
初始化名为 mTouchSlop
的变量。您可以使用 ViewConfiguration
类来获取 Android 系统常用的距离、速度和时间。
“Touch slop”是指在系统将手势解读为滚动之前,用户的轻触手势可以滑动的距离(以像素为单位)。Touch slop 通常用于防止在用户执行某些其他轻触操作(例如轻触屏幕上的元素)时发生意外滚动。
另外两个常用的 ViewConfiguration
方法是 getScaledMinimumFlingVelocity()
和 getScaledMaximumFlingVelocity()
。这两个方法会分别返回发起滑动的最小速度和最大速度,以像素/秒为单位。例如:
Kotlin
private val vc: ViewConfiguration = ViewConfiguration.get(context) private val mSlop: Int = vc.scaledTouchSlop private val mMinFlingVelocity: Int = vc.scaledMinimumFlingVelocity private val mMaxFlingVelocity: Int = vc.scaledMaximumFlingVelocity ... MotionEvent.ACTION_MOVE -> { ... val deltaX: Float = motionEvent.rawX - mDownX if (Math.abs(deltaX) > mSlop) { // A swipe occurred, do something } return false } ... MotionEvent.ACTION_UP -> { ... if (velocityX in mMinFlingVelocity..mMaxFlingVelocity && velocityY < velocityX) { // The criteria have been satisfied, do something } }
Java
ViewConfiguration vc = ViewConfiguration.get(view.getContext()); private int mSlop = vc.getScaledTouchSlop(); private int mMinFlingVelocity = vc.getScaledMinimumFlingVelocity(); private int mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity(); ... case MotionEvent.ACTION_MOVE: { ... float deltaX = motionEvent.getRawX() - mDownX; if (Math.abs(deltaX) > mSlop) { // A swipe occurred, do something } ... case MotionEvent.ACTION_UP: { ... } if (mMinFlingVelocity <= velocityX && velocityX <= mMaxFlingVelocity && velocityY < velocityX) { // The criteria have been satisfied, do something } }
扩展子视图的可轻触区域
Android 提供了 TouchDelegate
类,让父视图能够将子视图的可轻触区域扩展到子视图的边界之外。当子视图必须较小,同时又应该具有较大的轻触区域时,此方法很有用。您也可以在需要时使用此方法缩小子视图的轻触区域。
在以下示例中,ImageButton
是“代理视图”(即,父视图将扩展其轻触区域的子视图)。其布局文件如下:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/parent_layout" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity" > <ImageButton android:id="@+id/button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@null" android:src="@drawable/icon" /> </RelativeLayout>
以下代码段可实现以下操作:
- 获取父视图,并在界面线程上发布
Runnable
。这样可以确保父视图先对子视图进行布局,然后再调用getHitRect()
方法。getHitRect()
方法以父视图坐标的形式获取子视图点击矩形(可轻触区域)。 - 查找
ImageButton
子视图并调用getHitRect()
以获取子视图可轻触区域的边界。 - 扩展
ImageButton
点击矩形的边界。 - 实例化
TouchDelegate
,将扩展的点击矩形和ImageButton
子视图作为参数传入。 - 在父视图上设置
TouchDelegate
,以便将轻触代理边界内的轻触操作传递给子视图。
作为 ImageButton
子视图的轻触代理,父视图会收到所有轻触事件。如果轻触事件发生在子视图的点击矩形内,父视图会将轻触事件传递给子视图处理。
Kotlin
public class MainActivity : Activity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) // Post in the parent's message queue to make sure the parent // lays out its children before you call getHitRect() findViewById<View>(R.id.parent_layout).post { // The bounds for the delegate view (an ImageButton // in this example) val delegateArea = Rect() val myButton = findViewById<ImageButton>(R.id.button).apply { isEnabled = true setOnClickListener { Toast.makeText( this@MainActivity, "Touch occurred within ImageButton touch region.", Toast.LENGTH_SHORT ).show() } // The hit rectangle for the ImageButton getHitRect(delegateArea) } // Extend the touch area of the ImageButton beyond its bounds // on the right and bottom. delegateArea.right += 100 delegateArea.bottom += 100 // Sets the TouchDelegate on the parent view, such that touches // within the touch delegate bounds are routed to the child. (myButton.parent as? View)?.apply { // Instantiate a TouchDelegate. // "delegateArea" is the bounds in local coordinates of // the containing view to be mapped to the delegate view. // "myButton" is the child view that should receive motion // events. touchDelegate = TouchDelegate(delegateArea, myButton) } } } }
Java
public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // Get the parent view View parentView = findViewById(R.id.parent_layout); parentView.post(new Runnable() { // Post in the parent's message queue to make sure the parent // lays out its children before you call getHitRect() @Override public void run() { // The bounds for the delegate view (an ImageButton // in this example) Rect delegateArea = new Rect(); ImageButton myButton = (ImageButton) findViewById(R.id.button); myButton.setEnabled(true); myButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Toast.makeText(MainActivity.this, "Touch occurred within ImageButton touch region.", Toast.LENGTH_SHORT).show(); } }); // The hit rectangle for the ImageButton myButton.getHitRect(delegateArea); // Extend the touch area of the ImageButton beyond its bounds // on the right and bottom. delegateArea.right += 100; delegateArea.bottom += 100; // Instantiate a TouchDelegate. // "delegateArea" is the bounds in local coordinates of // the containing view to be mapped to the delegate view. // "myButton" is the child view that should receive motion // events. TouchDelegate touchDelegate = new TouchDelegate(delegateArea, myButton); // Sets the TouchDelegate on the parent view, such that touches // within the touch delegate bounds are routed to the child. if (View.class.isInstance(myButton.getParent())) { ((View) myButton.getParent()).setTouchDelegate(touchDelegate); } } }); } }