关于Android TouchDelegate源码解析

android.view.TouchDelegate是用来扩大View的触摸点击区域的。

用法很简单,套路是:
比如
mButton = new CheckBox(getContext());

Rect bounds = new Rect(0, 0, viewBound.getMeasuredWidth(), viewBound.getMeasuredHeight());
TouchDelegate delegate = new TouchDelegate(bounds, mButton);
viewBound.setTouchDelegate(delegate);

这样就可以扩大mButton的触摸点击区域了,将它的触摸区域设成viewBound的区域,也就是说点viewBound的任何地方都等同于点mButton。
既然是这样那我也可以设置另外一块和mButton毫无交集区域作为viewBound的点击范围。

public class TouchDelegateLayout extends FrameLayout {

    public TouchDelegateLayout(Context context) {
        super(context);
        init();
    }

    public TouchDelegateLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public TouchDelegateLayout(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init();
    }

    private CheckBox mButton;
    private void init() {
        mButton = new CheckBox(getContext());
        mButton.setText("Click Anywhere On Screen");

        addView(mButton, new FrameLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, Gravity.CENTER));
    }

    /*
     * TouchDelegate is applied to this view (parent) to delegate all touches
     * within the specified rectangle to the CheckBox (child).  Here, the rectangle
     * is the entire size of this parent view.
     * 
     * This must be done after the view has measured itself so we know how big to make the rect,
     * thus we've chosen to add the delegate in onMeasure()
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //Apply the whole area of this view as the delegate area
        Rect bounds = new Rect(0, 0, getMeasuredWidth()/3, getMeasuredHeight()/3);
        TouchDelegate delegate = new TouchDelegate(bounds, mButton);
        setTouchDelegate(delegate);
    }
}

如上,点击FrameLayout的左上角是可以让mButton选中的。为什么可以实现,TouchDelegate为我们做了什么?

public class TouchDelegate {

    /**
     * View that should receive forwarded touch events 
     */
    private View mDelegateView;

    /**
     * Bounds in local coordinates of the containing view that should be mapped to the delegate
     * view. This rect is used for initial hit testing.
     */
    private Rect mBounds;

    /**
     * mBounds inflated to include some slop. This rect is to track whether the motion events
     * should be considered to be be within the delegate view.
     */
    private Rect mSlopBounds;

    /**
     * True if the delegate had been targeted on a down event (intersected mBounds).
     */
    private boolean mDelegateTargeted;

    /**
     * The touchable region of the View extends above its actual extent.
     */
    public static final int ABOVE = 1;

    /**
     * The touchable region of the View extends below its actual extent.
     */
    public static final int BELOW = 2;

    /**
     * The touchable region of the View extends to the left of its
     * actual extent.
     */
    public static final int TO_LEFT = 4;

    /**
     * The touchable region of the View extends to the right of its
     * actual extent.
     */
    public static final int TO_RIGHT = 8;

    private int mSlop;

    /**
     * Constructor
     * 
     * @param bounds Bounds in local coordinates of the containing view that should be mapped to
     *        the delegate view
     * @param delegateView The view that should receive motion events
     */
    public TouchDelegate(Rect bounds, View delegateView) {
        mBounds = bounds;

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

    /**
     * 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仅仅是一个普通的不能再普通的java类而已。代码不多,就一个构造方法和一个onTouchEvent方法,而且这个onTouchEvent方法不是重写的,仅仅是自己定义的方法取这么个名字。那这个方法在哪调用的呢?
是在View.java里的

public boolean onTouchEvent(MotionEvent event) {
        final float x = event.getX();
        final float y = event.getY();
        final int viewFlags = mViewFlags;

        if ((viewFlags & ENABLED_MASK) == DISABLED) {
            if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                setPressed(false);
            }
            // A disabled view that is clickable still consumes the touch
            // events, it just doesn't respond to them.
            return (((viewFlags & CLICKABLE) == CLICKABLE ||
                    (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
        }

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

里面调用的,看到这里大家明白了,原来点击FrameLayout势必会调用到View.java里的onTouchEvent里来,然后又设了代理TouchDelegate,所有势必会调用TouchDelegate里的onTouchEvent方法。

现在我们来看此方法怎么做到的。
这是它的构造方法

public TouchDelegate(Rect bounds, View delegateView) {
//把Rect点击区域赋值
        mBounds = bounds;
//拿到android定义的touch边界值
        mSlop = ViewConfiguration.get(delegateView.getContext()).getScaledTouchSlop();
        //初始化touch边界值的Rect区域,初始化时直接用传过来的rect
        mSlopBounds = new Rect(bounds);
        //跟进代码里,则个方法意思是让这个Rect区域变宽点,可以看源码注释
        mSlopBounds.inset(-mSlop, -mSlop);
        //需要代理的view,上面里例子就是FrameLayout里的mButton
        mDelegateView = delegateView;
    }

OK,现在来看onTouchEvent

//注意这里变量的命名,很规范,让人一看就明白什么意思
public boolean onTouchEvent(MotionEvent event) {
        int x = (int)event.getX();
        int y = (int)event.getY();
        //是否发生event事件给需要代理的view
        boolean sendToDelegate = false;
        //是否点击在需代理的view上,这里不好翻译,大致意思看后面
        boolean hit = true;
        //是否已处理
        boolean handled = false;

        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            Rect bounds = mBounds;
//down事件包含在rect区域里,要发event事件给需代理的view
            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;
                //用ScaleTouchSlop扩大的区域是否包含了event的x y坐标,hit默认为true,默认包含
                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用法,特别是跟到源码里看就更不明白了,发现源码里有个offsetLocation(x - oldX, y - oldY)方法,这个方法实现了ViewGroup中的childView上的touchEvent事件的x y坐标是相对于自身的左上角为00的边界。
                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;
                //上面英文解释也很清楚,是为了追踪preesed状态的,就是当move事件一直移动还没up前移出了设定的rect点击区域的时候需要重写设置event的坐标
                event.setLocation(-(slop * 2), -(slop * 2));
            }
            handled = delegateView.dispatchTouchEvent(event);
        }
        return handled;
    }

Ok,这里就分析差不多了,至少我弄明白了。
然后说说MotionEvent里的offsetLocation方法,在ViewGroup中用来将childView的touchEvent的坐标偏移成相对自身左上角为0点的起始坐标的

引用篇博客http://blog.csdn.net/bigconvience/article/details/26391743

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,  
           View child, int desiredPointerIdBits) { 
           if (child == null || child.hasIdentityMatrix()) {  
               if (child == null) {  
                   handled = super.dispatchTouchEvent(event);  
               } else {  
                   final float offsetX = mScrollX - child.mLeft;  
                   final float offsetY = mScrollY - child.mTop;      
                   /*直接对MotionEvent进行坐标变换,将MotionEvent传递下去*/  
                   event.offsetLocation(offsetX, offsetY);  
                   handled = child.dispatchTouchEvent(event);  
                   /*回复MotionEvent*/  
                   event.offsetLocation(-offsetX, -offsetY);  
               }  
               return handled;  
           }  

主要是上面先偏移event.offsetLocation(offsetX, offsetY);
然后再偏移回来event.offsetLocation(-offsetX, -offsetY);

中间让childView自己去分发TouchEvent
handled = child.dispatchTouchEvent(event);

所以到这里整个TouchDelegate类里的所有疑问都理清楚了,完全可以自己实现一个类似的类了,不错!

主要是觉得这个类简单但是可以学习的东西很多,遂连夜记录下来!共勉

版权声明:本文为博主原创文章,未经博主允许不得转载。

android
您的回应...

相关话题

查看全部

也许你感兴趣

换一批

热门标签

更多