在 ViewGroup
中处理轻触事件时需要特别小心,因为 ViewGroup
通常会有子视图,而这些子视图是除 ViewGroup
本身以外的其他轻触事件的目标。为了确保每个视图都能够正确地接收专门发给它的轻触事件,请替换 onInterceptTouchEvent()
方法。
请参阅以下相关资源:
在 ViewGroup 中拦截轻触事件
每当在 ViewGroup
的表面(包括其子级的表面)上检测到触摸事件时,就会调用 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 only determines whether you want to intercept the motion. // If this method returns true, onTouchEvent is called and you can 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 // Don't intercept the touch event. Let the child handle it. } MotionEvent.ACTION_MOVE -> { if (mIsScrolling) { // You're currently scrolling, so intercept the touch event. true } else { // If the user drags their finger horizontally more than the // touch slop, start the scroll. // Left as an exercise for the reader. val xDiff: Int = calculateDistanceX(ev) // Touch slop is calculated using ViewConfiguration constants. if (xDiff > mTouchSlop) { // Start scrolling! mIsScrolling = true true } else { false } } } ... else -> { // In general, don't intercept touch events. The child view // handles them. false } } } override fun onTouchEvent(event: MotionEvent): Boolean { // Here, you actually handle the touch event. For example, if the action // is ACTION_MOVE, scroll this container. This method is only called if // the touch event is 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 only determines whether you want to intercept the motion. // If this method returns true, onTouchEvent is called and you can 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; // Don't intercept touch event. Let the child handle it. } switch (action) { case MotionEvent.ACTION_MOVE: { if (mIsScrolling) { // You're currently scrolling, so intercept the touch event. return true; } // If the user drags their finger horizontally more than the // touch slop, start the scroll. // Left as an exercise for the reader. final int xDiff = calculateDistanceX(ev); // Touch slop is calculated using ViewConfiguration constants. if (xDiff > mTouchSlop) { // Start scrolling. mIsScrolling = true; return true; } break; } ... } // In general, don't intercept touch events. The child view handles them. return false; } @Override public boolean onTouchEvent(MotionEvent ev) { // Here, you actually handle the touch event. For example, if the // action is ACTION_MOVE, scroll this container. This method is only // called if the touch event is 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 occurs, do something. } return false } ... MotionEvent.ACTION_UP -> { ... if (velocityX in mMinFlingVelocity..mMaxFlingVelocity && velocityY < velocityX) { // The criteria are 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 occurs, do something. } ... case MotionEvent.ACTION_UP: { ... } if (mMinFlingVelocity <= velocityX && velocityX <= mMaxFlingVelocity && velocityY < velocityX) { // The criteria are 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, which is 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 // Set the TouchDelegate on the parent view so 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 receives // 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, which is 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 receives // motion events. TouchDelegate touchDelegate = new TouchDelegate(delegateArea, myButton); // Set the TouchDelegate on the parent view so that touches // within the touch delegate bounds are routed to the child. if (View.class.isInstance(myButton.getParent())) { ((View) myButton.getParent()).setTouchDelegate(touchDelegate); } } }); } }