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
43    private ImageView mDragView;
44    private WindowManager mWindowManager;
45    private WindowManager.LayoutParams mWindowParams;
46    /**
47     * At which position is the item currently being dragged. Note that this
48     * takes in to account header items.
49     */
50    private int mDragPos;
51    /**
52     * At which position was the item being dragged originally
53     */
54    private int mSrcDragPos;
55    private int mDragPointX;    // at what x offset inside the item did the user grab it
56    private int mDragPointY;    // at what y offset inside the item did the user grab it
57    private int mXOffset;  // the difference between screen coordinates and coordinates in this view
58    private int mYOffset;  // the difference between screen coordinates and coordinates in this view
59    private DragListener mDragListener;
60    private DropListener mDropListener;
61    private RemoveListener mRemoveListener;
62    private int mUpperBound;
63    private int mLowerBound;
64    private int mHeight;
65    private GestureDetector mGestureDetector;
66    private static final int FLING = 0;
67    private static final int SLIDE = 1;
68    private static final int TRASH = 2;
69    private int mRemoveMode = -1;
70    private Rect mTempRect = new Rect();
71    private Bitmap mDragBitmap;
72    private final int mTouchSlop;
73    private int mItemHeightNormal;
74    private int mItemHeightExpanded;
75    private int mItemHeightHalf;
76    private Drawable mTrashcan;
77
78    public TouchInterceptor(Context context, AttributeSet attrs) {
79        super(context, attrs);
80        SharedPreferences pref = context.getSharedPreferences("Music", 3);
81        mRemoveMode = pref.getInt("deletemode", -1);
82        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
83        Resources res = getResources();
84        mItemHeightNormal = res.getDimensionPixelSize(R.dimen.normal_height);
85        mItemHeightHalf = mItemHeightNormal / 2;
86        mItemHeightExpanded = res.getDimensionPixelSize(R.dimen.expanded_height);
87    }
88
89    @Override
90    public boolean onInterceptTouchEvent(MotionEvent ev) {
91        if (mRemoveListener != null && mGestureDetector == null) {
92            if (mRemoveMode == FLING) {
93                mGestureDetector = new GestureDetector(getContext(), new SimpleOnGestureListener() {
94                    @Override
95                    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
96                            float velocityY) {
97                        if (mDragView != null) {
98                            if (velocityX > 1000) {
99                                Rect r = mTempRect;
100                                mDragView.getDrawingRect(r);
101                                if ( e2.getX() > r.right * 2 / 3) {
102                                    // fast fling right with release near the right edge of the 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
158        if (y < 0) {
159            // when dragging off the top of the screen, calculate position
160            // by going back from a visible item
161            int pos = myPointToPosition(x, y + mItemHeightNormal);
162            if (pos > 0) {
163                return pos - 1;
164            }
165        }
166
167        Rect frame = mTempRect;
168        final int count = getChildCount();
169        for (int i = count - 1; i >= 0; i--) {
170            final View child = getChildAt(i);
171            child.getHitRect(frame);
172            if (frame.contains(x, y)) {
173                return getFirstVisiblePosition() + i;
174            }
175        }
176        return INVALID_POSITION;
177    }
178
179    private int getItemForPosition(int y) {
180        int adjustedy = y - mDragPointY - mItemHeightHalf;
181        int pos = myPointToPosition(0, adjustedy);
182        if (pos >= 0) {
183            if (pos <= mSrcDragPos) {
184                pos += 1;
185            }
186        } else if (adjustedy < 0) {
187            // this shouldn't happen anymore now that myPointToPosition deals
188            // with this situation
189            pos = 0;
190        }
191        return pos;
192    }
193
194    private void adjustScrollBounds(int y) {
195        if (y >= mHeight / 3) {
196            mUpperBound = mHeight / 3;
197        }
198        if (y <= mHeight * 2 / 3) {
199            mLowerBound = mHeight * 2 / 3;
200        }
201    }
202
203    /*
204     * Restore size and visibility for all listitems
205     */
206    private void unExpandViews(boolean deletion) {
207        for (int i = 0;; i++) {
208            View v = getChildAt(i);
209            if (v == null) {
210                if (deletion) {
211                    // HACK force update of mItemCount
212                    int position = getFirstVisiblePosition();
213                    int y = getChildAt(0).getTop();
214                    setAdapter(getAdapter());
215                    setSelectionFromTop(position, y);
216                    // end hack
217                }
218                try {
219                    layoutChildren(); // force children to be recreated where needed
220                    v = getChildAt(i);
221                } catch (IllegalStateException ex) {
222                    // layoutChildren throws this sometimes, presumably because we're
223                    // in the process of being torn down but are still getting touch
224                    // events
225                }
226                if (v == null) {
227                    return;
228                }
229            }
230            ViewGroup.LayoutParams params = v.getLayoutParams();
231            params.height = mItemHeightNormal;
232            v.setLayoutParams(params);
233            v.setVisibility(View.VISIBLE);
234        }
235    }
236
237    /* Adjust visibility and size to make it appear as though
238     * an item is being dragged around and other items are making
239     * room for it:
240     * If dropping the item would result in it still being in the
241     * same place, then make the dragged listitem's size normal,
242     * but make the item invisible.
243     * Otherwise, if the dragged listitem is still on screen, make
244     * it as small as possible and expand the item below the insert
245     * point.
246     * If the dragged item is not on screen, only expand the item
247     * below the current insertpoint.
248     */
249    private void doExpansion() {
250        int childnum = mDragPos - getFirstVisiblePosition();
251        if (mDragPos > mSrcDragPos) {
252            childnum++;
253        }
254        int numheaders = getHeaderViewsCount();
255
256        View first = getChildAt(mSrcDragPos - getFirstVisiblePosition());
257        for (int i = 0;; i++) {
258            View vv = getChildAt(i);
259            if (vv == null) {
260                break;
261            }
262
263            int height = mItemHeightNormal;
264            int visibility = View.VISIBLE;
265            if (mDragPos < numheaders && i == numheaders) {
266                // dragging on top of the header item, so adjust the item below
267                // instead
268                if (vv.equals(first)) {
269                    visibility = View.INVISIBLE;
270                } else {
271                    height = mItemHeightExpanded;
272                }
273            } else if (vv.equals(first)) {
274                // processing the item that is being dragged
275                if (mDragPos == mSrcDragPos || getPositionForView(vv) == getCount() - 1) {
276                    // hovering over the original location
277                    visibility = View.INVISIBLE;
278                } else {
279                    // not hovering over it
280                    // Ideally the item would be completely gone, but neither
281                    // setting its size to 0 nor settings visibility to GONE
282                    // has the desired effect.
283                    height = 1;
284                }
285            } else if (i == childnum) {
286                if (mDragPos >= numheaders && mDragPos < getCount() - 1) {
287                    height = mItemHeightExpanded;
288                }
289            }
290            ViewGroup.LayoutParams params = vv.getLayoutParams();
291            params.height = height;
292            vv.setLayoutParams(params);
293            vv.setVisibility(visibility);
294        }
295    }
296
297    @Override
298    public boolean onTouchEvent(MotionEvent ev) {
299        if (mGestureDetector != null) {
300            mGestureDetector.onTouchEvent(ev);
301        }
302        if ((mDragListener != null || mDropListener != null) && mDragView != null) {
303            int action = ev.getAction();
304            switch (action) {
305                case MotionEvent.ACTION_UP:
306                case MotionEvent.ACTION_CANCEL:
307                    Rect r = mTempRect;
308                    mDragView.getDrawingRect(r);
309                    stopDragging();
310                    if (mRemoveMode == SLIDE && ev.getX() > r.right * 3 / 4) {
311                        if (mRemoveListener != null) {
312                            mRemoveListener.remove(mSrcDragPos);
313                        }
314                        unExpandViews(true);
315                    } else {
316                        if (mDropListener != null && mDragPos >= 0 && mDragPos < getCount()) {
317                            mDropListener.drop(mSrcDragPos, mDragPos);
318                        }
319                        unExpandViews(false);
320                    }
321                    break;
322
323                case MotionEvent.ACTION_DOWN:
324                case MotionEvent.ACTION_MOVE:
325                    int x = (int) ev.getX();
326                    int y = (int) ev.getY();
327                    dragView(x, y);
328                    int itemnum = getItemForPosition(y);
329                    if (itemnum >= 0) {
330                        if (action == MotionEvent.ACTION_DOWN || itemnum != mDragPos) {
331                            if (mDragListener != null) {
332                                mDragListener.drag(mDragPos, itemnum);
333                            }
334                            mDragPos = itemnum;
335                            doExpansion();
336                        }
337                        int speed = 0;
338                        adjustScrollBounds(y);
339                        if (y > mLowerBound) {
340                            // scroll the list up a bit
341                            if (getLastVisiblePosition() < getCount() - 1) {
342                                speed = y > (mHeight + mLowerBound) / 2 ? 16 : 4;
343                            } else {
344                                speed = 1;
345                            }
346                        } else if (y < mUpperBound) {
347                            // scroll the list down a bit
348                            speed = y < mUpperBound / 2 ? -16 : -4;
349                            if (getFirstVisiblePosition() == 0
350                                    && getChildAt(0).getTop() >= getPaddingTop()) {
351                                // if we're already at the top, don't try to scroll, because
352                                // it causes the framework to do some extra drawing that messes
353                                // up our animation
354                                speed = 0;
355                            }
356                        }
357                        if (speed != 0) {
358                            smoothScrollBy(speed, 30);
359                        }
360                    }
361                    break;
362            }
363            return true;
364        }
365        return super.onTouchEvent(ev);
366    }
367
368    private void startDragging(Bitmap bm, int x, int y) {
369        stopDragging();
370
371        mWindowParams = new WindowManager.LayoutParams();
372        mWindowParams.gravity = Gravity.TOP | Gravity.LEFT;
373        mWindowParams.x = x - mDragPointX + mXOffset;
374        mWindowParams.y = y - mDragPointY + mYOffset;
375
376        mWindowParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
377        mWindowParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
378        mWindowParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
379                | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
380                | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
381                | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
382                | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS;
383        mWindowParams.format = PixelFormat.TRANSLUCENT;
384        mWindowParams.windowAnimations = 0;
385
386        Context context = getContext();
387        ImageView v = new ImageView(context);
388        //int backGroundColor = context.getResources().getColor(R.color.dragndrop_background);
389        //v.setBackgroundColor(backGroundColor);
390        v.setBackgroundResource(R.drawable.playlist_tile_drag);
391        v.setPadding(0, 0, 0, 0);
392        v.setImageBitmap(bm);
393        mDragBitmap = bm;
394
395        mWindowManager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
396        mWindowManager.addView(v, mWindowParams);
397        mDragView = v;
398    }
399
400    private void dragView(int x, int y) {
401        if (mRemoveMode == SLIDE) {
402            float alpha = 1.0f;
403            int width = mDragView.getWidth();
404            if (x > width / 2) {
405                alpha = ((float)(width - x)) / (width / 2);
406            }
407            mWindowParams.alpha = alpha;
408        }
409
410        if (mRemoveMode == FLING || mRemoveMode == TRASH) {
411            mWindowParams.x = x - mDragPointX + mXOffset;
412        } else {
413            mWindowParams.x = 0;
414        }
415        mWindowParams.y = y - mDragPointY + mYOffset;
416        mWindowManager.updateViewLayout(mDragView, mWindowParams);
417
418        if (mTrashcan != null) {
419            int width = mDragView.getWidth();
420            if (y > getHeight() * 3 / 4) {
421                mTrashcan.setLevel(2);
422            } else if (width > 0 && x > width / 4) {
423                mTrashcan.setLevel(1);
424            } else {
425                mTrashcan.setLevel(0);
426            }
427        }
428    }
429
430    private void stopDragging() {
431        if (mDragView != null) {
432            mDragView.setVisibility(GONE);
433            WindowManager wm = (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 {
465        void drag(int from, int to);
466    }
467    public interface DropListener {
468        void drop(int from, int to);
469    }
470    public interface RemoveListener {
471        void remove(int which);
472    }
473}
474