LibreOffice Module android (master) 1
TouchEventHandler.java
Go to the documentation of this file.
1/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
2 * This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
4 * You can obtain one at http://mozilla.org/MPL/2.0/. */
5
6package org.mozilla.gecko.gfx;
7
8import android.content.Context;
9import android.os.SystemClock;
10import android.util.Log;
11import android.view.GestureDetector;
12import android.view.MotionEvent;
13import android.view.View;
14
15import java.util.LinkedList;
16import java.util.Queue;
17
48public final class TouchEventHandler {
49 private static final String LOGTAG = "GeckoTouchEventHandler";
50
51 // The time limit for listeners to respond with preventDefault on touchevents
52 // before we begin panning the page
53 private final int EVENT_LISTENER_TIMEOUT = 200;
54
55 private final View mView;
56 private final GestureDetector mGestureDetector;
58 private final JavaPanZoomController mPanZoomController;
59
60 // the queue of events that we are holding on to while waiting for a preventDefault
61 // notification
62 private final Queue<MotionEvent> mEventQueue;
64
65 // whether or not we should wait for touch listeners to respond (this state is
66 // per-tab and is updated when we switch tabs).
67 private boolean mWaitForTouchListeners;
68
69 // true if we should hold incoming events in our queue. this is re-set for every
70 // block of events, this is cleared once we find out if the block has been
71 // default-prevented or not (or we time out waiting for that).
72 private boolean mHoldInQueue;
73
74 // true if we should dispatch incoming events to the gesture detector and the pan/zoom
75 // controller. if this is false, then the current block of events has been
76 // default-prevented, and we should not dispatch these events (although we'll still send
77 // them to gecko listeners).
78 private boolean mDispatchEvents;
79
80 // this next variable requires some explanation. strap yourself in.
81 //
82 // for each block of events, we do two things: (1) send the events to gecko and expect
83 // exactly one default-prevented notification in return, and (2) kick off a delayed
84 // ListenerTimeoutProcessor that triggers in case we don't hear from the listener in
85 // a timely fashion.
86 // since events are constantly coming in, we need to be able to handle more than one
87 // block of events in the queue.
88 //
89 // this means that there are ordering restrictions on these that we can take advantage of,
90 // and need to abide by. blocks of events in the queue will always be in the order that
91 // the user generated them. default-prevented notifications we get from gecko will be in
92 // the same order as the blocks of events in the queue. the ListenerTimeoutProcessors that
93 // have been posted will also fire in the same order as the blocks of events in the queue.
94 // HOWEVER, we may get multiple default-prevented notifications interleaved with multiple
95 // ListenerTimeoutProcessor firings, and that interleaving is not predictable.
96 //
97 // therefore, we need to make sure that for each block of events, we process the queued
98 // events exactly once, either when we get the default-prevented notification, or when the
99 // timeout expires (whichever happens first). there is no way to associate the
100 // default-prevented notification with a particular block of events other than via ordering,
101 //
102 // so what we do to accomplish this is to track a "processing balance", which is the number
103 // of default-prevented notifications that we have received, minus the number of ListenerTimeoutProcessors
104 // that have fired. (think "balance" as in teeter-totter balance). this value is:
105 // - zero when we are in a state where the next default-prevented notification we expect
106 // to receive and the next ListenerTimeoutProcessor we expect to fire both correspond to
107 // the next block of events in the queue.
108 // - positive when we are in a state where we have received more default-prevented notifications
109 // than ListenerTimeoutProcessors. This means that the next default-prevented notification
110 // does correspond to the block at the head of the queue, but the next n ListenerTimeoutProcessors
111 // need to be ignored as they are for blocks we have already processed. (n is the absolute value
112 // of the balance.)
113 // - negative when we are in a state where we have received more ListenerTimeoutProcessors than
114 // default-prevented notifications. This means that the next ListenerTimeoutProcessor that
115 // we receive does correspond to the block at the head of the queue, but the next n
116 // default-prevented notifications need to be ignored as they are for blocks we have already
117 // processed. (n is the absolute value of the balance.)
119
120 TouchEventHandler(Context context, View view, JavaPanZoomController panZoomController) {
121 mView = view;
122
123 mEventQueue = new LinkedList<MotionEvent>();
124 mPanZoomController = panZoomController;
125 mGestureDetector = new GestureDetector(context, mPanZoomController);
128 mDispatchEvents = true;
129
130 mGestureDetector.setOnDoubleTapListener(mPanZoomController);
131 }
132
133 void destroy() {
134 }
135
136 /* This function MUST be called on the UI thread */
137 public boolean handleEvent(MotionEvent event) {
138 if (isDownEvent(event)) {
139 // this is the start of a new block of events! whee!
141
142 // Set mDispatchEvents to true so that we are guaranteed to either queue these
143 // events or dispatch them. The only time we should not do either is once we've
144 // heard back from content to preventDefault this block.
145 mDispatchEvents = true;
146 if (mHoldInQueue) {
147 // if the new block we are starting is the current block (i.e. there are no
148 // other blocks waiting in the queue, then we should let the pan/zoom controller
149 // know we are waiting for the touch listeners to run
150 if (mEventQueue.isEmpty()) {
151 mPanZoomController.startingNewEventBlock(event, true);
152 }
153 } else {
154 // we're not going to be holding this block of events in the queue, but we need
155 // a marker of some sort so that the processEventBlock loop deals with the blocks
156 // in the right order as notifications come in. we use a single null event in
157 // the queue as a placeholder for a block of events that has already been dispatched.
158 mEventQueue.add(null);
159 mPanZoomController.startingNewEventBlock(event, false);
160 }
161
162 // set the timeout so that we dispatch these events and update mProcessingBalance
163 // if we don't get a default-prevented notification
165 }
166
167 // if we need to hold the events, add it to the queue. if we need to dispatch
168 // it directly, do that. it is possible that both mHoldInQueue and mDispatchEvents
169 // are false, in which case we are processing a block of events that we know
170 // has been default-prevented. in that case we don't keep the events as we don't
171 // need them (but we still pass them to the gecko listener).
172 if (mHoldInQueue) {
173 mEventQueue.add(MotionEvent.obtain(event));
174 } else if (mDispatchEvents) {
175 dispatchEvent(event);
176 } else if (touchFinished(event)) {
177 mPanZoomController.preventedTouchFinished();
178 }
179
180 return true;
181 }
182
191 public void handleEventListenerAction(boolean allowDefaultAction) {
192 if (mProcessingBalance > 0) {
193 // this event listener that triggered this took too long, and the corresponding
194 // ListenerTimeoutProcessor runnable already ran for the event in question. the
195 // block of events this is for has already been processed, so we don't need to
196 // do anything here.
197 } else {
198 processEventBlock(allowDefaultAction);
199 }
201 }
202
203 /* This function MUST be called on the UI thread. */
204 public void setWaitForTouchListeners(boolean aValue) {
205 mWaitForTouchListeners = aValue;
206 }
207
208 private boolean isDownEvent(MotionEvent event) {
209 int action = (event.getAction() & MotionEvent.ACTION_MASK);
210 return (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_POINTER_DOWN);
211 }
212
213 private boolean touchFinished(MotionEvent event) {
214 int action = (event.getAction() & MotionEvent.ACTION_MASK);
215 return (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL);
216 }
217
221 private void dispatchEvent(MotionEvent event) {
222 if (mGestureDetector.onTouchEvent(event)) {
223 return;
224 }
227 return;
228 }
229 mPanZoomController.handleEvent(event);
230 }
231
236 private void processEventBlock(boolean allowDefaultAction) {
237 if (!allowDefaultAction) {
238 // if the block has been default-prevented, cancel whatever stuff we had in
239 // progress in the gesture detector and pan zoom controller
240 long now = SystemClock.uptimeMillis();
241 dispatchEvent(MotionEvent.obtain(now, now, MotionEvent.ACTION_CANCEL, 0, 0, 0));
242 }
243
244 if (mEventQueue.isEmpty()) {
245 Log.e(LOGTAG, "Unexpected empty event queue in processEventBlock!", new Exception());
246 return;
247 }
248
249 // the odd loop condition is because the first event in the queue will
250 // always be a DOWN or POINTER_DOWN event, and we want to process all
251 // the events in the queue starting at that one, up to but not including
252 // the next DOWN or POINTER_DOWN event.
253
254 MotionEvent event = mEventQueue.poll();
255 while (true) {
256 // event being null here is valid and represents a block of events
257 // that has already been dispatched.
258
259 if (event != null) {
260 // for each event we process, only dispatch it if the block hasn't been
261 // default-prevented.
262 if (allowDefaultAction) {
263 dispatchEvent(event);
264 } else if (touchFinished(event)) {
265 mPanZoomController.preventedTouchFinished();
266 }
267 }
268 if (mEventQueue.isEmpty()) {
269 // we have processed the backlog of events, and are all caught up.
270 // now we can set clear the hold flag and set the dispatch flag so
271 // that the handleEvent() function can do the right thing for all
272 // remaining events in this block (which is still ongoing) without
273 // having to put them in the queue.
274 mHoldInQueue = false;
275 mDispatchEvents = allowDefaultAction;
276 break;
277 }
278 event = mEventQueue.peek();
279 if (event == null || isDownEvent(event)) {
280 // we have finished processing the block we were interested in.
281 // now we wait for the next call to processEventBlock
282 if (event != null) {
283 mPanZoomController.startingNewEventBlock(event, true);
284 }
285 break;
286 }
287 // pop the event we peeked above, as it is still part of the block and
288 // we want to keep processing
289 mEventQueue.remove();
290 }
291 }
292
293 private class ListenerTimeoutProcessor implements Runnable {
294 /* This MUST be run on the UI thread */
295 public void run() {
296 if (mProcessingBalance < 0) {
297 // gecko already responded with default-prevented notification, and so
298 // the block of events this ListenerTimeoutProcessor corresponds to have
299 // already been removed from the queue.
300 } else {
301 processEventBlock(true);
302 }
304 }
305 }
306}
A less buggy, and smoother, replacement for the built-in Android ScaleGestureDetector.
boolean isInProgress()
Returns true if the scale gesture is in progress and false otherwise.
void onTouchEvent(MotionEvent event)
Forward touch events to this function.
This class handles incoming touch events from the user and sends them to listeners in Gecko and/or pe...
final SimpleScaleGestureDetector mScaleGestureDetector
void dispatchEvent(MotionEvent event)
Dispatch the event to the gesture detectors and the pan/zoom controller.
void processEventBlock(boolean allowDefaultAction)
Process the block of events at the head of the queue now that we know whether it has been default-pre...
final JavaPanZoomController mPanZoomController
void handleEventListenerAction(boolean allowDefaultAction)
This function is how gecko sends us a default-prevented notification.
final ListenerTimeoutProcessor mListenerTimeoutProcessor
DateTime now
@ Exception
action