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
216
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
329
330 int result = super.pointToPosition(x, y);
331 final int count = getCount();
332 if (result > count)
333 result = count;
334 if (result == -1) {
335
336
337
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
369
370
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
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
523
524
525
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 }