1 /*
2  * Copyright (C) 2008 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.view;
18 
19 import android.graphics.Rect;
20 import android.view.MotionEvent;
21 import android.view.View;
22 import android.view.ViewConfiguration;
23 
24 /**
25  * Helper class to handle situations where you want a view to have a larger touch area than its
26  * actual view bounds. The view whose touch area is changed is called the delegate view. This
27  * class should be used by an ancestor of the delegate. To use a TouchDelegate, first create an
28  * instance that specifies the bounds that should be mapped to the delegate and the delegate
29  * view itself.
30  * <p>
31  * The ancestor should then forward all of its touch events received in its
32  * {@link android.view.View#onTouchEvent(MotionEvent)} to {@link #onTouchEvent(MotionEvent)}.
33  * </p>
34  */
35 public class TouchDelegate {
36 
37     /**
38      * View that should receive forwarded touch events
39      */
40     private View mDelegateView;
41 
42     /**
43      * Bounds in local coordinates of the containing view that should be mapped to the delegate
44      * view. This rect is used for initial hit testing.
45      */
46     private Rect mBounds;
47 
48     /**
49      * mBounds inflated to include some slop. This rect is to track whether the motion events
50      * should be considered to be be within the delegate view.
51      */
52     private Rect mSlopBounds;
53 
54     /**
55      * True if the delegate had been targeted on a down event (intersected mBounds).
56      */
57     private boolean mDelegateTargeted;
58 
59     /**
60      * The touchable region of the View extends above its actual extent.
61      */
62     public static final int ABOVE = 1;
63 
64     /**
65      * The touchable region of the View extends below its actual extent.
66      */
67     public static final int BELOW = 2;
68 
69     /**
70      * The touchable region of the View extends to the left of its
71      * actual extent.
72      */
73     public static final int TO_LEFT = 4;
74 
75     /**
76      * The touchable region of the View extends to the right of its
77      * actual extent.
78      */
79     public static final int TO_RIGHT = 8;
80 
81     private int mSlop;
82 
83     /**
84      * Constructor
85      *
86      * @param bounds Bounds in local coordinates of the containing view that should be mapped to
87      *        the delegate view
88      * @param delegateView The view that should receive motion events
89      */
TouchDelegate(Rect bounds, View delegateView)90     public TouchDelegate(Rect bounds, View delegateView) {
91         mBounds = bounds;
92 
93         mSlop = ViewConfiguration.get(delegateView.getContext()).getScaledTouchSlop();
94         mSlopBounds = new Rect(bounds);
95         mSlopBounds.inset(-mSlop, -mSlop);
96         mDelegateView = delegateView;
97     }
98 
99     /**
100      * Will forward touch events to the delegate view if the event is within the bounds
101      * specified in the constructor.
102      *
103      * @param event The touch event to forward
104      * @return True if the event was forwarded to the delegate, false otherwise.
105      */
onTouchEvent(MotionEvent event)106     public boolean onTouchEvent(MotionEvent event) {
107         int x = (int)event.getX();
108         int y = (int)event.getY();
109         boolean sendToDelegate = false;
110         boolean hit = true;
111         boolean handled = false;
112 
113         switch (event.getAction()) {
114         case MotionEvent.ACTION_DOWN:
115             Rect bounds = mBounds;
116 
117             if (bounds.contains(x, y)) {
118                 mDelegateTargeted = true;
119                 sendToDelegate = true;
120             }
121             break;
122         case MotionEvent.ACTION_UP:
123         case MotionEvent.ACTION_MOVE:
124             sendToDelegate = mDelegateTargeted;
125             if (sendToDelegate) {
126                 Rect slopBounds = mSlopBounds;
127                 if (!slopBounds.contains(x, y)) {
128                     hit = false;
129                 }
130             }
131             break;
132         case MotionEvent.ACTION_CANCEL:
133             sendToDelegate = mDelegateTargeted;
134             mDelegateTargeted = false;
135             break;
136         }
137         if (sendToDelegate) {
138             final View delegateView = mDelegateView;
139 
140             if (hit) {
141                 // Offset event coordinates to be inside the target view
142                 event.setLocation(delegateView.getWidth() / 2, delegateView.getHeight() / 2);
143             } else {
144                 // Offset event coordinates to be outside the target view (in case it does
145                 // something like tracking pressed state)
146                 int slop = mSlop;
147                 event.setLocation(-(slop * 2), -(slop * 2));
148             }
149             handled = delegateView.dispatchTouchEvent(event);
150         }
151         return handled;
152     }
153 }
154