TouchDelegate 代码分析

March 22, 2016

TouchDelegate 代码分析

TouchDelegate 的作用是,运行我们增加(其实也可以减少,但没有什么意义)某个控件的触摸反馈区域。相当于我们给某个 View 添加一个委托代理,委托代理主要包含两个信息,一个是被代理的View,一个是实际期望的触摸区域,这个委托代理是由当前这个 View 的父容器来处理。当父容器获得一个touch event 的时候,会优先判断这个touch event 是否落在委托代理的区域内,如果在这个区域内,委托代理会把touch event传递给被代理的 View 。这样被代理的 View 的触摸区域就会大于它的显示区域。

TouchDelegate 使用

一个Button在一个LinearLayout里,Button显示的区域只有那么小,但是我们又期望Button的触摸区域要稍微大些,现在如果修改Button的大小可能不符合产品UI的需求。那么我们可以为这个Button添加一个TouchDelegate。

怎么使用,具体看官方文档,https://developer.android.com/training/gestures/viewgroup.html#delegate Extend a Child View's Touchable Area 。

XML代码:

<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>

Java Code :

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);
        }
    }
});

在这个例子里,RelativeLayout 里有一个ImageButton,需要增加ImageButton的触摸区域。现在实例化一个 TouchDelegate 对象,实例化的时候需要一个增加后的触摸区域,以及 ImageButton。然后这个 TochDelegate 对象是设置给 Parent,也就是 RelativeLayout 。

TouchDelegate 代码分析

使用的咱们不多介绍,好好看文档。接下来,咱们研究研究 TouchDelegate 怎么工作的。

咱们在 View 的 onTouchEvent() 方法里,看到了

android.view.View onTouchEvent()

if (mTouchDelegate != null) {
    if (mTouchDelegate.onTouchEvent(event)) {
        return true;
    }
}

就是说这个 View 如果有 TouchDelegate 对象的话,这个 event 会传递给 TouchDelegate 处理。那么这个 View 是谁?这个 View 不是上面例子的 ImageButton,而是 RelativeLayout,也就是 parent。同时,咱们可以看出,一个 Parent只能帮助一个 child 增加触摸区域。

接下来,咱们再看看 TouchDelegate 干了些啥。

android.view.TouchDelegate onTouchEvent()

/**
 * Will forward touch events to the delegate view if the event is within the bounds
 * specified in the constructor.
 * 
 * @param event The touch event to forward
 * @return True if the event was forwarded to the delegate, false otherwise.
 */
public boolean onTouchEvent(MotionEvent event) {
    int x = (int)event.getX();
    int y = (int)event.getY();
    boolean sendToDelegate = false;
    boolean hit = true;
    boolean handled = false;

    switch (event.getAction()) {
    case MotionEvent.ACTION_DOWN:
        Rect bounds = mBounds;

        if (bounds.contains(x, y)) {
            mDelegateTargeted = true;
            sendToDelegate = true;
        }
        break;
    case MotionEvent.ACTION_UP:
    case MotionEvent.ACTION_MOVE:
        sendToDelegate = mDelegateTargeted;
        if (sendToDelegate) {
            Rect slopBounds = mSlopBounds;
            if (!slopBounds.contains(x, y)) {
                hit = false;
            }
        }
        break;
    case MotionEvent.ACTION_CANCEL:
        sendToDelegate = mDelegateTargeted;
        mDelegateTargeted = false;
        break;
    }
    if (sendToDelegate) {
        final View delegateView = mDelegateView;
        
        if (hit) {
            // Offset event coordinates to be inside the target view
            event.setLocation(delegateView.getWidth() / 2, delegateView.getHeight() / 2);
        } else {
            // Offset event coordinates to be outside the target view (in case it does
            // something like tracking pressed state)
            int slop = mSlop;
            event.setLocation(-(slop * 2), -(slop * 2));
        }
        handled = delegateView.dispatchTouchEvent(event);
    }
    return handled;
}

TouchDelegate.onTouchEvent() 在得到 MotionEvent 对象后,要做的一件事情就是判断这个 touch 的位置是不是在范围内。如果在范围内,这个事件要交给 delegateView 。delegateView 是什么?就是在构造函数里传过来的 View,也就是上面的 ImageButton。当然,如果只是直接把 MotionEvent 交给 ImageButton,touch 的位置不做改变,ImageButton 那到一个超出它区域的event,还是不行,所以,在交给 ImageButton前做了这么一件事:

event.setLocation(delegateView.getWidth() / 2, delegateView.getHeight() / 2);

如果不在区域内呢。不在区域得分很多种。

首先,在ACTION_DOWN 的时候,如果不在区域内,sendToDelegate 是 false,ImageButton 不会接受到这个事件。
如果 ACTION_DOWN 的时候在区域内,ImageButton 会接收到这个事件。但是如果在 ACTION_UP 和 ACTION_MOVE 的时候超出了区域了呢。刚才 ImageButton 接收到一个 Down 事件,样式可能已经变成获得焦点的样式了,这个时候如果超出位置了,是不是需要改变样式。在上面的代码中,在 DOWN 的时候如果在区域内,sendToDelegate = true 了,然后在 MOVE 的过程中移出了区域,那么 hit = false 。那么TouchDelegate也需要告诉 ImageButton ,touch 已经不在你的范围了。

那么怎么告诉 ImageButton 呢。TouchDelegate 把 event 的位置设置成一个特殊的位置,那么ImageButton 在 dispatchTouchEvent 里就可以判定 touch move 出范围了。

event.setLocation(-(slop * 2), -(slop * 2));

这里插一句。我很不明白这里为什么会需要多定义一个 slop 变量,直接使用 mSlop 不好吗?增加一个 slop 解决了什么问题?!而且既然 x 和 y 轴都是相同的 -(slop * 2),为啥定义的 slop 不能直接等于 -(mSlop * 2)。

现在的重点是这个 slop 是毛玩意?为啥是这个值,这个值的道理是什么?

mSlop = ViewConfiguration.get(delegateView.getContext()).getScaledTouchSlop();
mSlopBounds = new Rect(bounds);
mSlopBounds.inset(-mSlop, -mSlop);

mSlopBounds 是咱们在实例化 TouchDelegate 的时候传过来的,是要告诉 Parent ,ImageView 实际想要控制的触摸区域。在 TouchDelegate 里,它又对这个区域做了一个调整。这个 mSlop 的值大概定义在 ViewConfiguration 类。在 android.view 包下。

android.view.ViewConfiguration

private static final int TOUCH_SLOP = 8;

这个值的默认是8,实际的值会是配置文件里的一个值。也就是说,TouchDelegate 对你期望控制的触摸区域稍微缩小了范围,然后当触摸超过范围后,touch event 的位置被设置为 -(slop * 2) , - (slop * 2) 的位置。这个位置可以肯定是超过了 ImageButton 的范围,这样 ImageButton 也同样可以认为 touch event 移出了范围。

--- EOF ---

添加新评论