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