View Javadoc

1   /***
2    *     Ambient - A music player for the Android platform
3    Copyright (C) 2007 Martin Vysny
4    
5    This program is free software: you can redistribute it and/or modify
6    it under the terms of the GNU General Public License as published by
7    the Free Software Foundation, either version 3 of the License, or
8    (at your option) any later version.
9    
10   This program is distributed in the hope that it will be useful,
11   but WITHOUT ANY WARRANTY; without even the implied warranty of
12   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   GNU General Public License for more details.
14  
15   You should have received a copy of the GNU General Public License
16   along with this program.  If not, see <http://www.gnu.org/licenses/>.
17   */
18  
19  package sk.baka.ambient.views.gesturelist;
20  
21  import java.util.List;
22  import java.util.concurrent.CopyOnWriteArrayList;
23  
24  import sk.baka.ambient.AmbientApplication;
25  import sk.baka.ambient.R;
26  import sk.baka.ambient.collection.TrackMetadataBean;
27  import sk.baka.ambient.commons.Interval;
28  import sk.baka.ambient.views.ViewUtils;
29  import sk.baka.ambient.views.gesturelist.MouseGesturesRecognizer.GestureEnum;
30  import android.graphics.Point;
31  import android.os.Handler;
32  import android.view.Gravity;
33  import android.view.MotionEvent;
34  import android.view.View;
35  import android.view.ViewGroup.LayoutParams;
36  import android.widget.PopupWindow;
37  import android.widget.TextView;
38  
39  /***
40   * Controls the {@link GesturesListView} component via the touchpad.
41   * 
42   * @author Martin Vysny
43   */
44  final class TouchPadController {
45  	private final Handler handler = AmbientApplication.getHandler();
46  
47  	/***
48  	 * The view being controlled.
49  	 */
50  	private final GesturesListView owningView;
51  
52  	/***
53  	 * Creates new controller.
54  	 * 
55  	 * @param view
56  	 *            the view to control.
57  	 */
58  	TouchPadController(final GesturesListView view) {
59  		super();
60  		this.owningView = view;
61  	}
62  
63  	/***
64  	 * The gesture recognizer.
65  	 */
66  	private final MouseGesturesRecognizer gestureRecognizer = new MouseGesturesRecognizer();
67  
68  	/***
69  	 * If <code>false</code> then gesture handling is disabled until next
70  	 * PEN_DOWN event. Useful when leaving scrolling to the android component.
71  	 */
72  	private boolean enableGestureProcessing = true;
73  
74  	/***
75  	 * If <code>true</code> then we are selecting items with the R/RD gesture.
76  	 * Default event processing is disabled.
77  	 */
78  	private boolean highlighting = false;
79  
80  	/***
81  	 * If <code>true</code> then super {@link #onTouchEvent(MotionEvent)} is
82  	 * not invoked.
83  	 */
84  	private boolean suppressHandler = false;
85  
86  	/***
87  	 * Initial PEN_DOWN point.
88  	 */
89  	private Point initialPoint;
90  
91  	/***
92  	 * Initial index of the item.
93  	 */
94  	private int initialItem;
95  
96  	/***
97  	 * If <code>true</code> then the LU/LD gestures are in effect.
98  	 */
99  	private boolean draggingItems = false;
100 
101 	/***
102 	 * Reflects last {@link MotionEvent} coordinates.
103 	 */
104 	private final Point currentEventPoint = new Point();
105 
106 	/***
107 	 * If {@link #draggingItems dragging} then this window contain an overview
108 	 * of dragged items attached to the cursor.
109 	 */
110 	private PopupWindow dragHintWindow;
111 
112 	private boolean canHighlight() {
113 		if (owningView.listener.canHighlight()) {
114 			return true;
115 		}
116 		owningView.setMode(R.string.cannotSelect, this, false);
117 		initialItem = -1;
118 		draggingItems = false;
119 		highlighting = false;
120 		enableGestureProcessing = false;
121 		suppressHandler = false;
122 		return false;
123 	}
124 
125 	/***
126 	 * Handles the {@link View#onTouchEvent(MotionEvent)} events.
127 	 * 
128 	 * @param event
129 	 *            the event to handle
130 	 * @return if <code>false</code> then the event is not handled and should
131 	 *         be routed to super class.
132 	 */
133 	boolean onTouchEvent(MotionEvent event) {
134 		currentEventPoint.x = (int) event.getX();
135 		currentEventPoint.y = (int) event.getY();
136 		if (event.getAction() == MotionEvent.ACTION_DOWN) {
137 			enableGestureProcessing = true;
138 			suppressHandler = false;
139 			initialPoint = ViewUtils.clone(currentEventPoint);
140 			initialItem = owningView.pointToPosition(initialPoint.x,
141 					initialPoint.y);
142 			final boolean isEOP = owningView.isEOP(initialItem);
143 			if (isEOP) {
144 				initialItem = -1;
145 			}
146 		}
147 		if (enableGestureProcessing) {
148 			handleGestures(event);
149 		}
150 		if (highlighting) {
151 			// post highlight event
152 			int item = owningView.getItemIndex(event);
153 			if (item < 0) {
154 				if (event.getY() < 0) {
155 					item = 0;
156 				} else {
157 					item = owningView.getCount() - 1;
158 				}
159 			}
160 			if (!owningView.getHighlight().equals(initialItem, item)) {
161 				owningView.getModel().highlight(
162 						Interval.fromRange(initialItem, item));
163 				owningView.getModel().notifyModified();
164 			}
165 			if (ViewUtils.isPenUp(event)) {
166 				owningView.clearMode(this);
167 				highlighting = false;
168 				owningView.restoreSelector();
169 				handleScrolling(null, true);
170 			} else {
171 				handleScrolling(currentEventPoint, false);
172 			}
173 		}
174 		if (draggingItems) {
175 			utils.translateCoordinatesToRoot(currentEventPoint, owningView);
176 			if (dragHintWindow != null) {
177 				dragHintWindow.update(utils.translated.x, utils.translated.y,
178 						-1, -1);
179 			}
180 			final GesturesListView target = owningView
181 					.findView(currentEventPoint);
182 			if (ViewUtils.isPenUp(event)) {
183 				// PEN_UP. Look up a GesturesListView underneath the cursor
184 				// and drop the items there.
185 				owningView.clearMode(this);
186 				setDragging(false);
187 				if (dragHintWindow != null) {
188 					dragHintWindow.dismiss();
189 					dragHintWindow = null;
190 				}
191 				if (target != null) {
192 					if (!ViewUtils.isCancel(event)) {
193 						invokeDragDropEvent(currentEventPoint);
194 					}
195 					target.touchController.handleScrolling(null, true);
196 				}
197 			} else {
198 				if (target != null) {
199 					utils.translateCoordinates(currentEventPoint, owningView,
200 							target);
201 					target.touchController.handleScrolling(utils.translated,
202 							false);
203 				}
204 			}
205 		}
206 		return suppressHandler;
207 	}
208 
209 	/***
210 	 * Checks if this event activates a gesture, and in such case acts
211 	 * accordingly.
212 	 * 
213 	 * @param event
214 	 *            the mouse event.
215 	 */
216 	private void handleGestures(MotionEvent event) {
217 		final GestureEnum g = gestureRecognizer.processMouseEvent(event);
218 		final String gesture = gestureRecognizer.getGesture();
219 		if (g == GestureEnum.NewGesture) {
220 			final char firstGesture = gestureRecognizer.getLastGesture();
221 			// prevent handler to grab PEN_UP event which will result in
222 			// onClick event
223 			suppressHandler = true;
224 			if ((firstGesture == MouseGesturesRecognizer.DOWN_MOVE)
225 					|| (firstGesture == MouseGesturesRecognizer.UP_MOVE)) {
226 				// scrolling event. disable gesture processing and let
227 				// android scroll by itself
228 				enableGestureProcessing = false;
229 				suppressHandler = false;
230 			} else if (firstGesture == MouseGesturesRecognizer.RIGHT_MOVE) {
231 				// select a single item
232 				if (canHighlight()) {
233 					final int item = initialItem;
234 					if (item >= 0) {
235 						owningView.getModel()
236 								.highlight(Interval.fromItem(item));
237 						owningView.getModel().notifyModified();
238 					}
239 					owningView.setMode(R.string.selection, this, true);
240 				}
241 			} else {
242 				assert firstGesture == MouseGesturesRecognizer.LEFT_MOVE;
243 				owningView.setMode(owningView.hintDeleteCopyMoveId, this, true);
244 				// do nothing, wait for further gestures
245 			}
246 		} else if (g == GestureEnum.GestureFinished) {
247 			boolean clearMode = true;
248 			if ("L".equals(gesture)) {
249 				// remove selected items
250 				owningView.setMode(owningView.hintDeleteId, this, false);
251 				clearMode = false;
252 				owningView.listener.removeItems(owningView.getModel()
253 						.getHighlight(initialItem));
254 				if (!owningView.getHighlight().isEmpty()) {
255 					owningView.getModel().highlight(Interval.EMPTY);
256 					owningView.getModel().notifyModified();
257 				}
258 			} else if ("R".equals(gesture)) {
259 				// select all items
260 				clearMode = false;
261 				if (canHighlight()) {
262 					owningView.setMode(R.string.select_all, this, false);
263 					owningView.getModel().highlight(
264 							owningView.getModel().getAllItems());
265 					owningView.getModel().notifyModified();
266 				}
267 			}
268 			enableGestureProcessing = false;
269 			if (clearMode) {
270 				owningView.clearMode(this);
271 			}
272 		} else if (g == GestureEnum.ContinuingGesture) {
273 			if ("RU".equals(gesture)) {
274 				// deselect items
275 				owningView.getModel().highlight(Interval.EMPTY);
276 				owningView.getModel().notifyModified();
277 				owningView.setMode(R.string.deselect_all, this, false);
278 			} else if ("RD".equals(gesture)) {
279 				// selecting multiple items for sure.
280 				if (owningView.listener.canHighlight()) {
281 					highlighting = true;
282 					owningView.transparentSelector();
283 				}
284 			} else if (gesture.startsWith("L")
285 					&& canComputeItems()) {
286 				// drag'n'drop / move items up/down
287 				setDragging(true);
288 				final String hint = owningView.listener.getHint(owningView
289 						.getModel().getHighlight(initialItem));
290 				if (hint != null) {
291 					dragHintWindow = new PopupWindow(owningView.getContext());
292 					final TextView textView = new TextView(owningView
293 							.getContext());
294 					textView.setText(hint);
295 					dragHintWindow.setContentView(textView);
296 					dragHintWindow.showAtLocation(owningView.getRootView(),
297 							Gravity.NO_GRAVITY, 0, 0);
298 					dragHintWindow.update(0, 0, LayoutParams.WRAP_CONTENT,
299 							LayoutParams.WRAP_CONTENT);
300 				}
301 				owningView.setMode(R.string.dragDrop, this, true);
302 			}
303 			enableGestureProcessing = false;
304 		}
305 	}
306 
307 	private boolean canComputeItems() {
308 		if (owningView.listener.canComputeItems()) {
309 			return true;
310 		}
311 		owningView.setMode(R.string.cannotDragDrop, this, false);
312 		return false;
313 	}
314 
315 	private void setDragging(boolean b) {
316 		draggingItems = b;
317 		for (final GesturesListView target : owningView.dragDropViews) {
318 			target.getModel().adapter.setEOP(b);
319 		}
320 	}
321 
322 	/***
323 	 * Activates or deactivates scrolling according to the point location in the
324 	 * view.
325 	 * 
326 	 * @param point
327 	 *            the point in this view's coordinate system
328 	 * @param cancel
329 	 *            if <code>true</code> then scrolling is canceled regardless of
330 	 *            the point location.
331 	 */
332 	private void handleScrolling(final Point point, final boolean cancel) {
333 		if (cancel) {
334 			handler.removeCallbacks(scroller);
335 			scrolling = 0;
336 		} else {
337 			// scroll if the pen is in the bottom area
338 			if (owningView.getHeight() - point.y < 30) {
339 				if (scrolling < 1) {
340 					scrolling = 1;
341 					currentScrolledItem = owningView.pointToPosition(point.x,
342 							point.y)
343 							- scrolling - 1;
344 					handler.post(scroller);
345 				}
346 			} else if (point.y < 30) {
347 				if (scrolling > -1) {
348 					scrolling = -1;
349 					currentScrolledItem = owningView.pointToPosition(point.x,
350 							point.y)
351 							- scrolling + 1;
352 					handler.post(scroller);
353 				}
354 			} else {
355 				if (scrolling != 0) {
356 					handler.removeCallbacks(scroller);
357 					scrolling = 0;
358 				}
359 			}
360 		}
361 	}
362 
363 	private final ViewUtils utils = new ViewUtils();
364 
365 	/***
366 	 * Finds a view containing given point and invokes drag event on the view.
367 	 * The method does nothing if given point does not intersect any registered
368 	 * view.
369 	 * 
370 	 * @param point
371 	 *            the point, relative to this view.
372 	 */
373 	private void invokeDragDropEvent(final Point point) {
374 		final GesturesListView view = owningView.findView(point);
375 		if (view == null)
376 			return;
377 		// we found the view. compute relative position
378 		utils.translateCoordinates(point, owningView, view);
379 		final Point p = ViewUtils.clone(utils.translated);
380 		final Interval hl = owningView.getModel().getHighlight(
381 				initialItem);
382 		// compute item index, where to insert.
383 		int _index = view.pointToPosition(p.x, p.y);
384 		if (_index < 0) {
385 			_index = view.getCount();
386 		}
387 		final int index = _index;
388 		final boolean isMoving = (view == owningView);
389 		if (isMoving) {
390 			final Interval newInterval = view.listener.moveItems(hl, index);
391 			view.getModel().highlight(newInterval);
392 			view.getModel().notifyModified();
393 			return;
394 		}
395 		final boolean isLongOp = owningView.listener
396 				.isComputeTracksLong(hl);
397 		final boolean isOnlineOp = owningView.listener
398 				.isComputeTracksOnlineOp(hl);
399 		// run the dragdrop/move operation
400 		final Runnable dragDrop = new Runnable() {
401 			private volatile List<TrackMetadataBean> tracks;
402 
403 			private volatile boolean obtainedTracks = false;
404 
405 			public void run() {
406 				// drag'n'drop
407 				if (!obtainedTracks) {
408 					tracks = owningView.listener.computeTracks(hl);
409 					obtainedTracks = true;
410 					if (tracks != null) {
411 						// copy items to a thread-safe list
412 						tracks = new CopyOnWriteArrayList<TrackMetadataBean>(
413 								tracks);
414 					}
415 					handler.post(this);
416 				} else {
417 					if ((tracks != null) && !tracks.isEmpty()) {
418 						view.listener.dropItems(tracks, p.x, p.y, index);
419 					}
420 				}
421 			}
422 		};
423 		if (isLongOp) {
424 			AmbientApplication.getInstance().getBackgroundTasks().schedule(
425 					dragDrop,
426 					GesturesListView.class,
427 					isOnlineOp,
428 					owningView.getResources().getString(
429 							R.string.fb_adding_tracks));
430 		} else {
431 			try {
432 				dragDrop.run();
433 			} catch (final Exception ex) {
434 				if (!Thread.currentThread().isInterrupted()) {
435 					final AmbientApplication app = AmbientApplication
436 							.getInstance();
437 					app.error(KeypadController.class, true, app
438 							.getString(R.string.error), ex);
439 				}
440 			}
441 		}
442 	}
443 
444 	/***
445 	 * If not <code>0</code> then the {@link #scroller} is active.
446 	 */
447 	private short scrolling = 0;
448 
449 	/***
450 	 * The list view's current selection is set to this item, which causes the
451 	 * list view to be scrolled automatically.
452 	 */
453 	private int currentScrolledItem = 0;
454 
455 	/***
456 	 * Scrolls the view as requested and reschedules itself.
457 	 */
458 	private final Runnable scroller = new Runnable() {
459 		public void run() {
460 			currentScrolledItem += scrolling;
461 			owningView.setSelection(currentScrolledItem);
462 			handler.postDelayed(this, 250);
463 		}
464 	};
465 }