1/*
2 * Copyright (C) 2008 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.music;
18
19import android.content.Context;
20import android.content.SharedPreferences;
21import android.content.res.Resources;
22import android.graphics.Bitmap;
23import android.graphics.PixelFormat;
24import android.graphics.Rect;
25import android.graphics.drawable.Drawable;
26import android.graphics.drawable.LevelListDrawable;
27import android.util.AttributeSet;
28import android.util.Log;
29import android.view.GestureDetector;
30import android.view.Gravity;
31import android.view.MotionEvent;
32import android.view.View;
33import android.view.ViewConfiguration;
34import android.view.ViewGroup;
35import android.view.WindowManager;
36import android.view.GestureDetector.SimpleOnGestureListener;
37import android.widget.AdapterView;
38import android.widget.ImageView;
39import android.widget.ListView;
40
41public class TouchInterceptor extends ListView {
42    private ImageView mDragView;
43    private WindowManager mWindowManager;
44    private WindowManager.LayoutParams mWindowParams;
45    /**
46     * At which position is the item currently being dragged. Note that this
47     * takes in to account header items.
48     */
49    private int mDragPos;
50    /**
51     * At which position was the item being dragged originally
52     */
53    private int mSrcDragPos;
54    private int mDragPointX; // at what x offset inside the item did the user grab it
55    private int mDragPointY; // at what y offset inside the item did the user grab it
56    private int mXOffset; // the difference between screen coordinates and coordinates in this view
57    private int mYOffset; // the difference between screen coordinates and coordinates in this view
58    private DragListener mDragListener;
59    private DropListener mDropListener;
60    private RemoveListener mRemoveListener;
61    private int mUpperBound;
62    private int mLowerBound;
63    private int mHeight;
64    private GestureDetector mGestureDetector;
65    private static final int FLING = 0;
66    private static final int SLIDE = 1;
67    private static final int TRASH = 2;
68    private int mRemoveMode = -1;
69    private Rect mTempRect = new Rect();
70    private Bitmap mDragBitmap;
71    private final int mTouchSlop;
72    private int mItemHeightNormal;
73    private int mItemHeightExpanded;
74    private int mItemHeightHalf;
75    private Drawable mTrashcan;
76
77    public TouchInterceptor(Context context, AttributeSet attrs) {
78        super(context, attrs);
79        SharedPreferences pref = context.getSharedPreferences("Music", 0);
80        mRemoveMode = pref.getInt("deletemode", -1);
81        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
82        Resources res = getResources();
83        mItemHeightNormal = res.getDimensionPixelSize(R.dimen.normal_height);
84        mItemHeightHalf = mItemHeightNormal / 2;
85        mItemHeightExpanded = res.getDimensionPixelSize(R.dimen.expanded_height);
86    }
87
88    @Override
89    public boolean onInterceptTouchEvent(MotionEvent ev) {
90        if (mRemoveListener != null && mGestureDetector == null) {
91            if (mRemoveMode == FLING) {
92                mGestureDetector = new GestureDetector(getContext(), new SimpleOnGestureListener() {
93                    @Override
94                    public boolean onFling(
95                            MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
96                        if (mDragView != null) {
97                            if (velocityX > 1000) {
98                                Rect r = mTempRect;
99                                mDragView.getDrawingRect(r);
100                                if (e2.getX() > r.right * 2 / 3) {
101                                    // fast fling right with release near the right edge of the
102                                    // screen
103                                    stopDragging();
104                                    mRemoveListener.remove(mSrcDragPos);
105                                    unExpandViews(true);
106                                }
107                            }
108                            // flinging while dragging should have no effect
109                            return true;
110                        }
111                        return false;
112                    }
113                });
114            }
115        }
116        if (mDragListener != null || mDropListener != null) {
117            switch (ev.getAction()) {
118                case MotionEvent.ACTION_DOWN:
119                    int x = (int) ev.getX();
120                    int y = (int) ev.getY();
121                    int itemnum = pointToPosition(x, y);
122                    if (itemnum == AdapterView.INVALID_POSITION) {
123                        break;
124                    }
125                    ViewGroup item = (ViewGroup) getChildAt(itemnum - getFirstVisiblePosition());
126                    mDragPointX = x - item.getLeft();
127                    mDragPointY = y - item.getTop();
128                    mXOffset = ((int) ev.getRawX()) - x;
129                    mYOffset = ((int) ev.getRawY()) - y;
130                    // The left side of the item is the grabber for dragging the item
131                    if (x < 64) {
132                        item.setDrawingCacheEnabled(true);
133                        // Create a copy of the drawing cache so that it does not get recycled
134                        // by the framework when the list tries to clean up memory
135                        Bitmap bitmap = Bitmap.createBitmap(item.getDrawingCache());
136                        startDragging(bitmap, x, y);
137                        mDragPos = itemnum;
138                        mSrcDragPos = mDragPos;
139                        mHeight = getHeight();
140                        int touchSlop = mTouchSlop;
141                        mUpperBound = Math.min(y - touchSlop, mHeight / 3);
142                        mLowerBound = Math.max(y + touchSlop, mHeight * 2 / 3);
143                        return false;
144                    }
145                    stopDragging();
146                    break;
147            }
148        }
149        return super.onInterceptTouchEvent(ev);
150    }
151
152    /*
153     * pointToPosition() doesn't consider invisible views, but we
154     * need to, so implement a slightly different version.
155     */
156    private int myPointToPosition(int x, int y) {
157        if (y < 0) {
158            // when dragging off the top of the screen, calculate position
159            // by going back from a visible item
160            int pos = myPointToPosition(x, y + mItemHeightNormal);
161            if (pos > 0) {
162                return pos - 1;
163            }
164        }
165
166        Rect frame = mTempRect;
167        final int count = getChildCount();
168        for (int i = count - 1; i >= 0; i--) {
169            final View child = getChildAt(i);
170            child.getHitRect(frame);
171            if (frame.contains(x, y)) {
172                return getFirstVisiblePosition() + i;
173            }
174        }
175        return INVALID_POSITION;
176    }
177
178    private int getItemForPosition(int y) {
179        int adjustedy = y - mDragPointY - mItemHeightHalf;
180        int pos = myPointToPosition(0, adjustedy);
181        if (pos >= 0) {
182            if (pos <= mSrcDragPos) {
183                pos += 1;
184            }
185        } else if (adjustedy < 0) {
186            // this shouldn't happen anymore now that myPointToPosition deals
187            // with this situation
188            pos = 0;
189        }
190        return pos;
191    }
192
193    private void adjustScrollBounds(int y) {
194        if (y >= mHeight / 3) {
195            mUpperBound = mHeight / 3;
196        }
197        if (y <= mHeight * 2 / 3) {
198            mLowerBound = mHeight * 2 / 3;
199        }
200    }
201
202    /*
203     * Restore size and visibility for all listitems
204     */
205    private void unExpandViews(boolean deletion) {
206        for (int i = 0;; i++) {
207            View v = getChildAt(i);
208            if (v == null) {
209                if (deletion) {
210                    // HACK force update of mItemCount
211                    int position = getFirstVisiblePosition();
212                    int y = getChildAt(0).getTop();
213                    setAdapter(getAdapter());
214                    setSelectionFromTop(position, y);
215                    // end hack
216                }
217                try {
218                    layoutChildren(); // force children to be recreated where needed
219                    v = getChildAt(i);
220                } catch (IllegalStateException ex) {
221                    // layoutChildren throws this sometimes, presumably because we're
222                    // in the process of being torn down but are still getting touch
223                    // events
224                }
225                if (v == null) {
226                    return;
227                }
228            }
229            ViewGroup.LayoutParams params = v.getLayoutParams();
230            params.height = mItemHeightNormal;
231            v.setLayoutParams(params);
232            v.setVisibility(View.VISIBLE);
233        }
234    }
235
236    /* Adjust visibility and size to make it appear as though
237     * an item is being dragged around and other items are making
238     * room for it:
239     * If dropping the item would result in it still being in the
240     * same place, then make the dragged listitem's size normal,
241     * but make the item invisible.
242     * Otherwise, if the dragged listitem is still on screen, make
243     * it as small as possible and expand the item below the insert
244     * point.
245     * If the dragged item is not on screen, only expand the item
246     * below the current insertpoint.
247     */
248    private void doExpansion() {
249        int childnum = mDragPos - getFirstVisiblePosition();
250        if (mDragPos > mSrcDragPos) {
251            childnum++;
252        }
253        int numheaders = getHeaderViewsCount();
254
255        View first = getChildAt(mSrcDragPos - getFirstVisiblePosition());
256        for (int i = 0;; i++) {
257            View vv = getChildAt(i);
258            if (vv == null) {
259                break;
260            }
261
262            int height = mItemHeightNormal;
263            int visibility = View.VISIBLE;
264            if (mDragPos < numheaders && i == numheaders) {
265                // dragging on top of the header item, so adjust the item below
266                // instead
267                if (vv.equals(first)) {
268                    visibility = View.INVISIBLE;
269                } else {
270                    height = mItemHeightExpanded;
271                }
272            } else if (vv.equals(first)) {
273                // processing the item that is being dragged
274                if (mDragPos == mSrcDragPos || getPositionForView(vv) == getCount() - 1) {
275                    // hovering over the original location
276                    visibility = View.INVISIBLE;
277                } else {
278                    // not hovering over it
279                    // Ideally the item would be completely gone, but neither
280                    // setting its size to 0 nor settings visibility to GONE
281                    // has the desired effect.
282                    height = 1;
283                }
284            } else if (i == childnum) {
285                if (mDragPos >= numheaders && mDragPos < getCount() - 1) {
286                    height = mItemHeightExpanded;
287                }
288            }
289            ViewGroup.LayoutParams params = vv.getLayoutParams();
290            params.height = height;
291            vv.setLayoutParams(params);
292            vv.setVisibility(visibility);
293        }
294    }
295
296    @Override
297    public boolean onTouchEvent(MotionEvent ev) {
298        if (mGestureDetector != null) {
299            mGestureDetector.onTouchEvent(ev);
300        }
301        if ((mDragListener != null || mDropListener != null) && mDragView != null) {
302            int action = ev.getAction();
303            switch (action) {
304                case MotionEvent.ACTION_UP:
305                case MotionEvent.ACTION_CANCEL:
306                    Rect r = mTempRect;
307                    mDragView.getDrawingRect(r);
308                    stopDragging();
309                    if (mRemoveMode == SLIDE && ev.getX() > r.right * 3 / 4) {
310                        if (mRemoveListener != null) {
311                            mRemoveListener.remove(mSrcDragPos);
312                        }
313                        unExpandViews(true);
314                    } else {
315                        if (mDropListener != null && mDragPos >= 0 && mDragPos < getCount()) {
316                            mDropListener.drop(mSrcDragPos, mDragPos);
317                        }
318                        unExpandViews(false);
319                    }
320                    break;
321
322                case MotionEvent.ACTION_DOWN:
323                case MotionEvent.ACTION_MOVE:
324                    int x = (int) ev.getX();
325                    int y = (int) ev.getY();
326                    dragView(x, y);
327                    int itemnum = getItemForPosition(y);
328                    if (itemnum >= 0) {
329                        if (action == MotionEvent.ACTION_DOWN || itemnum != mDragPos) {
330                            if (mDragListener != null) {
331                                mDragListener.drag(mDragPos, itemnum);
332                            }
333                            mDragPos = itemnum;
334                            doExpansion();
335                        }
336                        int speed = 0;
337                        adjustScrollBounds(y);
338                        if (y > mLowerBound) {
339                            // scroll the list up a bit
340                            if (getLastVisiblePosition() < getCount() - 1) {
341                                speed = y > (mHeight + mLowerBound) / 2 ? 16 : 4;
342                            } else {
343                                speed = 1;
344                            }
345                        } else if (y < mUpperBound) {
346                            // scroll the list down a bit
347                            speed = y < mUpperBound / 2 ? -16 : -4;
348                            if (getFirstVisiblePosition() == 0
349                                    && getChildAt(0).getTop() >= getPaddingTop()) {
350                                // if we're already at the top, don't try to scroll, because
351                                // it causes the framework to do some extra drawing that messes
352                                // up our animation
353                                speed = 0;
354                            }
355                        }
356                        if (speed != 0) {
357                            smoothScrollBy(speed, 30);
358                        }
359                    }
360                    break;
361            }
362            return true;
363        }
364        return super.onTouchEvent(ev);
365    }
366
367    private void startDragging(Bitmap bm, int x, int y) {
368        stopDragging();
369
370        mWindowParams = new WindowManager.LayoutParams();
371        mWindowParams.gravity = Gravity.TOP | Gravity.LEFT;
372        mWindowParams.x = x - mDragPointX + mXOffset;
373        mWindowParams.y = y - mDragPointY + mYOffset;
374
375        mWindowParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
376        mWindowParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
377        mWindowParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
378                | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
379                | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
380                | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
381                | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS;
382        mWindowParams.format = PixelFormat.TRANSLUCENT;
383        mWindowParams.windowAnimations = 0;
384
385        Context context = getContext();
386        ImageView v = new ImageView(context);
387        // int backGroundColor = context.getResources().getColor(R.color.dragndrop_background);
388        // v.setBackgroundColor(backGroundColor);
389        v.setBackgroundResource(R.drawable.playlist_tile_drag);
390        v.setPadding(0, 0, 0, 0);
391        v.setImageBitmap(bm);
392        mDragBitmap = bm;
393
394        mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
395        mWindowManager.addView(v, mWindowParams);
396        mDragView = v;
397    }
398
399    private void dragView(int x, int y) {
400        if (mRemoveMode == SLIDE) {
401            float alpha = 1.0f;
402            int width = mDragView.getWidth();
403            if (x > width / 2) {
404                alpha = ((float) (width - x)) / (width / 2);
405            }
406            mWindowParams.alpha = alpha;
407        }
408
409        if (mRemoveMode == FLING || mRemoveMode == TRASH) {
410            mWindowParams.x = x - mDragPointX + mXOffset;
411        } else {
412            mWindowParams.x = 0;
413        }
414        mWindowParams.y = y - mDragPointY + mYOffset;
415        mWindowManager.updateViewLayout(mDragView, mWindowParams);
416
417        if (mTrashcan != null) {
418            int width = mDragView.getWidth();
419            if (y > getHeight() * 3 / 4) {
420                mTrashcan.setLevel(2);
421            } else if (width > 0 && x > width / 4) {
422                mTrashcan.setLevel(1);
423            } else {
424                mTrashcan.setLevel(0);
425            }
426        }
427    }
428
429    private void stopDragging() {
430        if (mDragView != null) {
431            mDragView.setVisibility(GONE);
432            WindowManager wm =
433                    (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
434            wm.removeView(mDragView);
435            mDragView.setImageDrawable(null);
436            mDragView = null;
437        }
438        if (mDragBitmap != null) {
439            mDragBitmap.recycle();
440            mDragBitmap = null;
441        }
442        if (mTrashcan != null) {
443            mTrashcan.setLevel(0);
444        }
445    }
446
447    public void setTrashcan(Drawable trash) {
448        mTrashcan = trash;
449        mRemoveMode = TRASH;
450    }
451
452    public void setDragListener(DragListener l) {
453        mDragListener = l;
454    }
455
456    public void setDropListener(DropListener l) {
457        mDropListener = l;
458    }
459
460    public void setRemoveListener(RemoveListener l) {
461        mRemoveListener = l;
462    }
463
464    public interface DragListener { void drag(int from, int to); }
465    public interface DropListener { void drop(int from, int to); }
466    public interface RemoveListener { void remove(int which); }
467}
468