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  package sk.baka.ambient.views.gesturelist;
19  
20  import java.util.ArrayList;
21  import java.util.List;
22  
23  import sk.baka.ambient.AmbientApplication;
24  import sk.baka.ambient.R;
25  import sk.baka.ambient.commons.Interval;
26  import sk.baka.ambient.commons.MiscUtils;
27  import sk.baka.ambient.views.ContainedPopupWindow;
28  import sk.baka.ambient.views.Iconify;
29  import sk.baka.ambient.views.ViewUtils;
30  import sk.baka.ambient.views.gesturelist.keypad.KeypadDpadSearch;
31  import sk.baka.ambient.views.gesturelist.keypad.KeypadManager;
32  import android.content.Context;
33  import android.content.res.TypedArray;
34  import android.graphics.Color;
35  import android.graphics.Point;
36  import android.graphics.Rect;
37  import android.graphics.drawable.Drawable;
38  import android.graphics.drawable.PaintDrawable;
39  import android.text.SpannableStringBuilder;
40  import android.util.AttributeSet;
41  import android.util.Log;
42  import android.view.KeyEvent;
43  import android.view.MotionEvent;
44  import android.view.View;
45  import android.widget.AdapterView;
46  import android.widget.ListAdapter;
47  import android.widget.ListView;
48  import android.widget.TextView;
49  
50  /***
51   * <p>
52   * Captures the {@link MotionEvent motion events} and generates more high-level
53   * gesture events.
54   * </p>
55   * <p>
56   * To configure the view you may need to set the two properties:
57   * </p>
58   * <ul>
59   * <li>The {@link #dragDropViews} list - list of targets where the items can be
60   * dropped. May be <code>null</code> - this has the same meaning as an empty
61   * list</li>
62   * <li>The event handler {@link #listener} </li>
63   * </ul>
64   * <p>
65   * The view offers a lot simplified API than the classical {@link ListView}, at
66   * the price of disabling support for other adapters. For example, there is no
67   * need to set the {@link ListAdapter} and evade the minefield of
68   * list-scroll-position-resetting functions like {@link #getAdapter()}
69   * </p>
70   * <p>
71   * To work with this modified list view, you just need to modify model using the
72   * {@link #getModel() model holder}.
73   * </p>
74   * 
75   * @author Martin Vysny
76   */
77  public class GesturesListView extends ListView {
78  
79  	/***
80  	 * Layout ID for each item in the list.
81  	 */
82  	int itemLayoutId;
83  
84  	/***
85  	 * The string id of the "Delete" (L, LL) gesture.
86  	 */
87  	public int hintDeleteId = R.string.delete;
88  	/***
89  	 * The string id of the "Delete/Copy/Move" (touchpad L?) gesture.
90  	 */
91  	public int hintDeleteCopyMoveId = R.string.delete_copy_move;
92  	/***
93  	 * The string id of the "Delete/Move/Paste" (keyboard L?) gesture.
94  	 */
95  	public int hintDeleteMovePasteId = R.string.delete_move_paste;
96  
97  	/***
98  	 * Returns the clipboard contents.
99  	 * @return the clipboard contents, <code>null</code> if the clipboard is incompatible or empty.
100 	 */
101 	public TrackListClipboardObject getClipboard() {
102 		if (listener == null) {
103 			return null;
104 		}
105 		return TrackListClipboardObject.fromObject(listener.getClipboard());
106 	}
107 
108 	/***
109 	 * Invoke to let the listview know that the clipboard was modified.
110 	 */
111 	public void clipboardChanged() {
112 		model.adapter.eopModified();
113 	}
114 
115 	/***
116 	 * The model for this listview.
117 	 */
118 	private ModelHolder model;
119 
120 	/***
121 	 * Returns the model holder.
122 	 * 
123 	 * @return a holder of live list of item data.
124 	 */
125 	public ModelHolder getModel() {
126 		return model;
127 	}
128 
129 	/***
130 	 * Returns current highlight.
131 	 * 
132 	 * @return current highlight, never <code>null</code>.
133 	 */
134 	public Interval getHighlight() {
135 		return model.getHighlight();
136 	}
137 
138 	/***
139 	 * Creates new view.
140 	 * 
141 	 * @param context
142 	 *            the context
143 	 * @param itemLayoutId
144 	 *            layout ID for each item in the list.
145 	 */
146 	public GesturesListView(Context context, final int itemLayoutId) {
147 		super(context);
148 		this.itemLayoutId = itemLayoutId;
149 		checkValues();
150 		init();
151 	}
152 
153 	/***
154 	 * Creates new view.
155 	 * 
156 	 * @param context
157 	 * @param attrs
158 	 * @param defStyle
159 	 */
160 	public GesturesListView(Context context, AttributeSet attrs, int defStyle) {
161 		super(context, attrs, defStyle);
162 		init(attrs);
163 	}
164 
165 	/***
166 	 * Creates new view.
167 	 * 
168 	 * @param context
169 	 * @param attrs
170 	 */
171 	public GesturesListView(Context context, AttributeSet attrs) {
172 		super(context, attrs);
173 		init(attrs);
174 	}
175 
176 	/***
177 	 * Initializes the view.
178 	 * 
179 	 * @param attrs
180 	 */
181 	private void init(AttributeSet attrs) {
182 		final TypedArray a = getContext().obtainStyledAttributes(
183 				attrs, R.styleable.GesturesListView);
184 		itemLayoutId = a.getResourceId(
185 				R.styleable.GesturesListView_itemLayoutId, -1);
186 		hintDeleteId = a.getResourceId(
187 				R.styleable.GesturesListView_hintDeleteId, R.string.delete);
188 		hintDeleteCopyMoveId = a.getResourceId(
189 				R.styleable.GesturesListView_hintDeleteCopyMoveId,
190 				R.string.delete_copy_move);
191 		hintDeleteMovePasteId = a.getResourceId(
192 				R.styleable.GesturesListView_hintDeleteMovePasteId,
193 				R.string.delete_move_paste);
194 		checkValues();
195 		init();
196 	}
197 
198 	private void checkValues() {
199 		if (itemLayoutId < 0) {
200 			throw new IllegalArgumentException(
201 					"The itemLayoutId attribute missing");
202 		}
203 	}
204 
205 	/***
206 	 * Initializes the component.
207 	 */
208 	private void init() {
209 		model = new ModelHolder(this);
210 		super.setAdapter(model.adapter);
211 		super.setOnItemClickListener(new OnItemClickListener() {
212 			@SuppressWarnings("unchecked")
213 			public void onItemClick(android.widget.AdapterView arg0,
214 					android.view.View arg1, int arg2, long arg3) {
215 				if (arg2 < 0) {
216 					return;
217 				}
218 				listener.itemActivated(arg2, model.getModel().get(arg2));
219 			}
220 		});
221 		// setup the selection change listener so that the keypad controller
222 		// will receive selection-changed events
223 		setOnItemSelectedListener(null);
224 	}
225 
226 	@Override
227 	protected void onAttachedToWindow() {
228 		super.onAttachedToWindow();
229 		setSelector(selector);
230 	}
231 
232 	@Override
233 	public void setOnItemClickListener(OnItemClickListener l) {
234 		Log.w(GesturesListView.class.getSimpleName(),
235 				"Trying to set itemclick listener, ignoring");
236 	}
237 
238 	@Override
239 	public void setAdapter(ListAdapter adapter) {
240 		Log.w(GesturesListView.class.getSimpleName(),
241 				"Trying to set custom adapter, ignoring");
242 	}
243 
244 	/***
245 	 * If not empty then the LU/LD gestures will drag'n'drop selected items to
246 	 * these views. The view is able to drag'n'drop items onto itself - in this
247 	 * case the items are moved to new location.
248 	 */
249 	public final List<GesturesListView> dragDropViews = new ArrayList<GesturesListView>();
250 
251 	/***
252 	 * Checks if the move events (
253 	 * {@link IGestureListViewListener#moveItems(Interval, int)} and
254 	 * {@link IGestureListViewListener#moveItemsByOne(Interval, boolean)}) can
255 	 * be invoked.
256 	 * 
257 	 * @return <code>true</code> if this list view can drag'n'drop items onto
258 	 *         itself, <code>false</code> otherwise.
259 	 */
260 	public boolean canMove() {
261 		return !listener.isReadOnly();
262 	}
263 
264 	/***
265 	 * Handles the touchpad events and controls this view.
266 	 */
267 	final TouchPadController touchController = new TouchPadController(this);
268 
269 	@Override
270 	public boolean onTouchEvent(MotionEvent event) {
271 		keyMan.activate(null);
272 		final boolean isEventHandled = touchController.onTouchEvent(event);
273 		if (isEventHandled) {
274 			return true;
275 		}
276 		return super.onTouchEvent(event);
277 	}
278 
279 	/***
280 	 * The keypad manager.
281 	 */
282 	private final KeypadManager keyMan = new KeypadManager(this);
283 	
284 	@Override
285 	public boolean onKeyDown(int keyCode, KeyEvent event) {
286 		return keyMan.onKeyDown(keyCode, event) ? true : super.onKeyDown(
287 				keyCode, event);
288 	}
289 
290 	@Override
291 	public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) {
292 		return keyMan.onKeyMultiple(keyCode, repeatCount, event) ? true
293 				: super.onKeyMultiple(keyCode, repeatCount, event);
294 	}
295 
296 	@Override
297 	public boolean onKeyUp(int keyCode, KeyEvent event) {
298 		return keyMan.onKeyUp(keyCode, event) ? true : super.onKeyUp(
299 				keyCode, event);
300 	}
301 
302 	/***
303 	 * The listview selection is drawn by this paint.
304 	 */
305 	private final Drawable transparent;
306 	private final Drawable selector;
307 	{
308 		transparent = new PaintDrawable(Color.TRANSPARENT);
309 		final int cursorColor = getResources()
310 				.getColor(R.color.listview_cursor);
311 		selector = new PaintDrawable(cursorColor);
312 	}
313 
314 	/***
315 	 * Remembers the original selector and sets a transparent (non-visible)
316 	 * selector.
317 	 */
318 	void transparentSelector() {
319 		setSelector(transparent);
320 	}
321 
322 	/***
323 	 * Restores the original selector.
324 	 */
325 	void restoreSelector() {
326 		setSelector(selector);
327 	}
328 
329 	private final ViewUtils utils = new ViewUtils();
330 
331 	@Override
332 	public int pointToPosition(int x, int y) {
333 		// a bug in Android? Sometimes the returned position is over the
334 		// .getCount()
335 		int result = super.pointToPosition(x, y);
336 		final int count = getCount();
337 		if (result > count)
338 			result = count;
339 		if (result == -1) {
340 			// we may be pointing onto a thin line under the item
341 			// (http://code.google.com/p/android/issues/detail?id=520).
342 			// Workaround
343 			int indexPrev = super.pointToPosition(x, y - 1);
344 			int indexNext = super.pointToPosition(x, y + 1);
345 			if (indexPrev + 1 == indexNext) {
346 				result = indexPrev;
347 			}
348 		}
349 		return result;
350 	}
351 
352 	/***
353 	 * Finds a view from the {@link #dragDropViews registered list of views}
354 	 * that contains given point.
355 	 * 
356 	 * @param point
357 	 *            the point in this view's coordinate system
358 	 * @return view containing given point or <code>null</code> if no such
359 	 *         view exists.
360 	 */
361 	public GesturesListView findView(final Point point) {
362 		if (dragDropViews == null)
363 			return null;
364 		final Rect viewRect = new Rect();
365 		viewRect.left = 0;
366 		viewRect.top = 0;
367 		for (final GesturesListView view : dragDropViews) {
368 			utils.translateCoordinates(point, this, view);
369 			if (view.getVisibility() == View.VISIBLE) {
370 				viewRect.right = view.getWidth();
371 				viewRect.bottom = view.getHeight();
372 			} else {
373 				// the view may be hidden - this is the case of the playlist
374 				// with
375 				// zero items. Just pretend that the view contains the point.
376 				viewRect.right = Integer.MAX_VALUE;
377 				viewRect.bottom = Integer.MAX_VALUE;
378 			}
379 			if (viewRect.contains(utils.translated.x, utils.translated.y))
380 				return view;
381 		}
382 		return null;
383 	}
384 
385 	/***
386 	 * Returns item index the event coordinates is pointing to.
387 	 * 
388 	 * @param event
389 	 *            the event
390 	 * @return the item index.
391 	 */
392 	public int getItemIndex(final MotionEvent event) {
393 		final int result = pointToPosition((int) event.getX(), (int) event
394 				.getY());
395 		return result;
396 	}
397 
398 	/***
399 	 * The gesture listener.
400 	 */
401 	public IGestureListViewListener listener;
402 
403 	/***
404 	 * Shows the mode the listview is currently in.
405 	 */
406 	private final ContainedPopupWindow viewModeHint = new ContainedPopupWindow(
407 			this, new ContainedPopupWindow.IPlaceable() {
408 				public int[] getCoordinates(ContainedPopupWindow popup,
409 						CharSequence text, final TextView textView) {
410 					return new int[] {
411 							getWidth()
412 									- ViewUtils.intMeasureText(textView
413 											.getPaint(), text.toString()) - 10,
414 							0 };
415 				}
416 			});
417 
418 	/***
419 	 * The controller which displayed the {@link #viewModeHint} lastly.
420 	 */
421 	private Object hintController = null;
422 
423 	/***
424 	 * Sets the tooltip text for the view. Use {@link #clearTooltip(Object)} to
425 	 * remove the tooltip.
426 	 * 
427 	 * @param resId
428 	 *            the string to show.
429 	 * @param controller
430 	 *            callee
431 	 * @param persistent
432 	 *            if <code>false</code> then the mode hint will disappear
433 	 *            automatically after 2 seconds.
434 	 * @param params
435 	 *            optional formatter parameters.
436 	 */
437 	public void setTooltip(final int resId, final Object controller,
438 			final boolean persistent, final Object... params) {
439 		hintController = controller;
440 		final SpannableStringBuilder sb = new SpannableStringBuilder(MiscUtils
441 				.format(resId, params));
442 		Iconify.iconify(getContext(), sb);
443 		viewModeHint.setText(sb);
444 		if (!persistent) {
445 			viewModeHint.dismissAfter(2500);
446 		}
447 	}
448 
449 	/***
450 	 * Clears the mode and dismisses the window.
451 	 * 
452 	 * @param controller
453 	 *            callee
454 	 */
455 	public void clearTooltip(final Object controller) {
456 		if (hintController != controller) {
457 			// prevent a controller to hide hint from another controller.
458 			return;
459 		}
460 		viewModeHint.dismiss();
461 	}
462 
463 	@Override
464 	public void setOnItemSelectedListener(OnItemSelectedListener listener) {
465 		super.setOnItemSelectedListener(new SelectionChanged(listener));
466 	}
467 
468 	private class SelectionChanged implements OnItemSelectedListener {
469 
470 		private final OnItemSelectedListener delegate;
471 
472 		/***
473 		 * Creates new event wrapper instance.
474 		 * 
475 		 * @param delegate
476 		 *            the delegate.
477 		 */
478 		public SelectionChanged(final OnItemSelectedListener delegate) {
479 			this.delegate = delegate;
480 		}
481 
482 		public void onItemSelected(AdapterView<?> arg0, View arg1, int arg2,
483 				long arg3) {
484 			if (delegate != null) {
485 				delegate.onItemSelected(arg0, arg1, arg2, arg3);
486 			}
487 			doSelectionChanged();
488 		}
489 
490 		public void onNothingSelected(AdapterView<?> arg0) {
491 			if (delegate != null) {
492 				delegate.onNothingSelected(arg0);
493 			}
494 			doSelectionChanged();
495 		}
496 
497 		private void doSelectionChanged() {
498 			AmbientApplication.getHandler().post(new Runnable() {
499 				public void run() {
500 					keyMan.selectionChanged();
501 				}
502 			});
503 		}
504 	}
505 
506 	@Override
507 	protected void onFocusChanged(boolean gainFocus, int direction,
508 			Rect previouslyFocusedRect) {
509 		if (!gainFocus) {
510 			keyMan.activate(null);
511 			viewModeHint.dismiss();
512 			// we cannot lose highlight - if we lose highlight then we cannot
513 			// queue tracks using keypad :)
514 			// model.highlight(Interval.EMPTY);
515 			// model.notifyModified();
516 		} else {
517 			// display hint
518 			viewModeHint.setText(Iconify.iconify(getContext(), listener
519 					.isReadOnly() ? R.string.glv_unmodifiable_hint
520 					: R.string.glv_modifiable_hint));
521 			viewModeHint.dismissAfter(1000);
522 		}
523 	}
524 
525 	/***
526 	 * Checks if given item is the EOP item.
527 	 * 
528 	 * @param position
529 	 *            the item index
530 	 * @return <code>true</code> if it is EOP, <code>false</code> otherwise.
531 	 */
532 	public boolean isEOP(final int position) {
533 		return model.adapter.isEOP(position);
534 	}
535 
536 	/***
537 	 * Zooms or un-zooms the items.
538 	 * 
539 	 * @param zoom
540 	 *            <code>true</code> if zoom the view.
541 	 */
542 	public void zoom(final int zoom) {
543 		model.adapter.zoom(zoom);
544 	}
545 
546 	/***
547 	 * Activates the binary search on this list view.
548 	 */
549 	public void activateDpadSearch() {
550 		keyMan.activate(KeypadDpadSearch.class);
551 	}
552 }