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
222
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
334
335 int result = super.pointToPosition(x, y);
336 final int count = getCount();
337 if (result > count)
338 result = count;
339 if (result == -1) {
340
341
342
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
374
375
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
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
513
514
515
516 } else {
517
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 }