1/*
2 * Copyright (C) 2012 Google Inc.
3 * Licensed to The Android Open Source Project.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 *      http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18package com.android.dialer.list;
19
20import android.animation.Animator;
21import android.animation.AnimatorListenerAdapter;
22import android.content.Context;
23import android.content.res.Configuration;
24import android.graphics.Bitmap;
25import android.os.Handler;
26import android.text.TextUtils;
27import android.util.AttributeSet;
28import android.util.Log;
29import android.view.DragEvent;
30import android.view.MotionEvent;
31import android.view.View;
32import android.view.ViewConfiguration;
33import android.widget.GridView;
34import android.widget.ImageView;
35
36import com.android.dialer.R;
37import com.android.dialer.list.DragDropController.DragItemContainer;
38
39/**
40 * Viewgroup that presents the user's speed dial contacts in a grid.
41 */
42public class PhoneFavoriteListView extends GridView implements OnDragDropListener,
43        DragItemContainer {
44
45    public static final String LOG_TAG = PhoneFavoriteListView.class.getSimpleName();
46
47    private float mTouchSlop;
48
49    private int mTopScrollBound;
50    private int mBottomScrollBound;
51    private int mLastDragY;
52
53    private Handler mScrollHandler;
54    private final long SCROLL_HANDLER_DELAY_MILLIS = 5;
55    private final int DRAG_SCROLL_PX_UNIT = 25;
56
57    private boolean mIsDragScrollerRunning = false;
58    private int mTouchDownForDragStartX;
59    private int mTouchDownForDragStartY;
60
61    private Bitmap mDragShadowBitmap;
62    private ImageView mDragShadowOverlay;
63    private View mDragShadowParent;
64    private int mAnimationDuration;
65
66    final int[] mLocationOnScreen = new int[2];
67
68    // X and Y offsets inside the item from where the user grabbed to the
69    // child's left coordinate. This is used to aid in the drawing of the drag shadow.
70    private int mTouchOffsetToChildLeft;
71    private int mTouchOffsetToChildTop;
72
73    private int mDragShadowLeft;
74    private int mDragShadowTop;
75
76    private DragDropController mDragDropController = new DragDropController(this);
77
78    private final float DRAG_SHADOW_ALPHA = 0.7f;
79
80    /**
81     * {@link #mTopScrollBound} and {@link mBottomScrollBound} will be
82     * offseted to the top / bottom by {@link #getHeight} * {@link #BOUND_GAP_RATIO} pixels.
83     */
84    private final float BOUND_GAP_RATIO = 0.2f;
85
86    private final Runnable mDragScroller = new Runnable() {
87        @Override
88        public void run() {
89            if (mLastDragY <= mTopScrollBound) {
90                smoothScrollBy(-DRAG_SCROLL_PX_UNIT, (int) SCROLL_HANDLER_DELAY_MILLIS);
91            } else if (mLastDragY >= mBottomScrollBound) {
92                smoothScrollBy(DRAG_SCROLL_PX_UNIT, (int) SCROLL_HANDLER_DELAY_MILLIS);
93            }
94            mScrollHandler.postDelayed(this, SCROLL_HANDLER_DELAY_MILLIS);
95        }
96    };
97
98    private final AnimatorListenerAdapter mDragShadowOverAnimatorListener =
99            new AnimatorListenerAdapter() {
100        @Override
101        public void onAnimationEnd(Animator animation) {
102            if (mDragShadowBitmap != null) {
103                mDragShadowBitmap.recycle();
104                mDragShadowBitmap = null;
105            }
106            mDragShadowOverlay.setVisibility(GONE);
107            mDragShadowOverlay.setImageBitmap(null);
108        }
109    };
110
111    public PhoneFavoriteListView(Context context) {
112        this(context, null);
113    }
114
115    public PhoneFavoriteListView(Context context, AttributeSet attrs) {
116        this(context, attrs, -1);
117    }
118
119    public PhoneFavoriteListView(Context context, AttributeSet attrs, int defStyle) {
120        super(context, attrs, defStyle);
121        mAnimationDuration = context.getResources().getInteger(R.integer.fade_duration);
122        mTouchSlop = ViewConfiguration.get(context).getScaledPagingTouchSlop();
123        mDragDropController.addOnDragDropListener(this);
124    }
125
126    @Override
127    protected void onConfigurationChanged(Configuration newConfig) {
128        super.onConfigurationChanged(newConfig);
129        mTouchSlop = ViewConfiguration.get(getContext()).getScaledPagingTouchSlop();
130    }
131
132    /**
133     * TODO: This is all swipe to remove code (nothing to do with drag to remove). This should
134     * be cleaned up and removed once drag to remove becomes the only way to remove contacts.
135     */
136    @Override
137    public boolean onInterceptTouchEvent(MotionEvent ev) {
138        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
139            mTouchDownForDragStartX = (int) ev.getX();
140            mTouchDownForDragStartY = (int) ev.getY();
141        }
142
143        return super.onInterceptTouchEvent(ev);
144    }
145
146    @Override
147    public boolean onDragEvent(DragEvent event) {
148        final int action = event.getAction();
149        final int eX = (int) event.getX();
150        final int eY = (int) event.getY();
151        switch (action) {
152            case DragEvent.ACTION_DRAG_STARTED: {
153                if (!PhoneFavoriteTileView.DRAG_PHONE_FAVORITE_TILE.equals(event.getLocalState())) {
154                    // Ignore any drag events that were not propagated by long pressing
155                    // on a {@link PhoneFavoriteTileView}
156                    return false;
157                }
158                if (!mDragDropController.handleDragStarted(eX, eY)) {
159                    return false;
160                }
161                break;
162            }
163            case DragEvent.ACTION_DRAG_LOCATION:
164                mLastDragY = eY;
165                mDragDropController.handleDragHovered(this, eX, eY);
166                // Kick off {@link #mScrollHandler} if it's not started yet.
167                if (!mIsDragScrollerRunning &&
168                        // And if the distance traveled while dragging exceeds the touch slop
169                        (Math.abs(mLastDragY - mTouchDownForDragStartY) >= 4 * mTouchSlop)) {
170                    mIsDragScrollerRunning = true;
171                    ensureScrollHandler();
172                    mScrollHandler.postDelayed(mDragScroller, SCROLL_HANDLER_DELAY_MILLIS);
173                }
174                break;
175            case DragEvent.ACTION_DRAG_ENTERED:
176                final int boundGap = (int) (getHeight() * BOUND_GAP_RATIO);
177                mTopScrollBound = (getTop() + boundGap);
178                mBottomScrollBound = (getBottom() - boundGap);
179                break;
180            case DragEvent.ACTION_DRAG_EXITED:
181            case DragEvent.ACTION_DRAG_ENDED:
182            case DragEvent.ACTION_DROP:
183                ensureScrollHandler();
184                mScrollHandler.removeCallbacks(mDragScroller);
185                mIsDragScrollerRunning = false;
186                // Either a successful drop or it's ended with out drop.
187                if (action == DragEvent.ACTION_DROP || action == DragEvent.ACTION_DRAG_ENDED) {
188                    mDragDropController.handleDragFinished(eX, eY, false);
189                }
190                break;
191            default:
192                break;
193        }
194        // This ListView will consume the drag events on behalf of its children.
195        return true;
196    }
197
198    public void setDragShadowOverlay(ImageView overlay) {
199        mDragShadowOverlay = overlay;
200        mDragShadowParent = (View) mDragShadowOverlay.getParent();
201    }
202
203    /**
204     * Find the view under the pointer.
205     */
206    private View getViewAtPosition(int x, int y) {
207        final int count = getChildCount();
208        View child;
209        for (int childIdx = 0; childIdx < count; childIdx++) {
210            child = getChildAt(childIdx);
211            if (y >= child.getTop() && y <= child.getBottom() && x >= child.getLeft()
212                    && x <= child.getRight()) {
213                return child;
214            }
215        }
216        return null;
217    }
218
219    private void ensureScrollHandler() {
220        if (mScrollHandler == null) {
221            mScrollHandler = getHandler();
222        }
223    }
224
225    public DragDropController getDragDropController() {
226        return mDragDropController;
227    }
228
229    @Override
230    public void onDragStarted(int x, int y, PhoneFavoriteSquareTileView tileView) {
231        if (mDragShadowOverlay == null) {
232            return;
233        }
234
235        mDragShadowOverlay.clearAnimation();
236        mDragShadowBitmap = createDraggedChildBitmap(tileView);
237        if (mDragShadowBitmap == null) {
238            return;
239        }
240
241        tileView.getLocationOnScreen(mLocationOnScreen);
242        mDragShadowLeft = mLocationOnScreen[0];
243        mDragShadowTop = mLocationOnScreen[1];
244
245        // x and y are the coordinates of the on-screen touch event. Using these
246        // and the on-screen location of the tileView, calculate the difference between
247        // the position of the user's finger and the position of the tileView. These will
248        // be used to offset the location of the drag shadow so that it appears that the
249        // tileView is positioned directly under the user's finger.
250        mTouchOffsetToChildLeft = x - mDragShadowLeft;
251        mTouchOffsetToChildTop = y - mDragShadowTop;
252
253        mDragShadowParent.getLocationOnScreen(mLocationOnScreen);
254        mDragShadowLeft -= mLocationOnScreen[0];
255        mDragShadowTop -= mLocationOnScreen[1];
256
257        mDragShadowOverlay.setImageBitmap(mDragShadowBitmap);
258        mDragShadowOverlay.setVisibility(VISIBLE);
259        mDragShadowOverlay.setAlpha(DRAG_SHADOW_ALPHA);
260
261        mDragShadowOverlay.setX(mDragShadowLeft);
262        mDragShadowOverlay.setY(mDragShadowTop);
263    }
264
265    @Override
266    public void onDragHovered(int x, int y, PhoneFavoriteSquareTileView tileView) {
267        // Update the drag shadow location.
268        mDragShadowParent.getLocationOnScreen(mLocationOnScreen);
269        mDragShadowLeft = x - mTouchOffsetToChildLeft - mLocationOnScreen[0];
270        mDragShadowTop = y - mTouchOffsetToChildTop - mLocationOnScreen[1];
271        // Draw the drag shadow at its last known location if the drag shadow exists.
272        if (mDragShadowOverlay != null) {
273            mDragShadowOverlay.setX(mDragShadowLeft);
274            mDragShadowOverlay.setY(mDragShadowTop);
275        }
276    }
277
278    @Override
279    public void onDragFinished(int x, int y) {
280        if (mDragShadowOverlay != null) {
281            mDragShadowOverlay.clearAnimation();
282            mDragShadowOverlay.animate().alpha(0.0f)
283                    .setDuration(mAnimationDuration)
284                    .setListener(mDragShadowOverAnimatorListener)
285                    .start();
286        }
287    }
288
289    @Override
290    public void onDroppedOnRemove() {}
291
292    private Bitmap createDraggedChildBitmap(View view) {
293        view.setDrawingCacheEnabled(true);
294        final Bitmap cache = view.getDrawingCache();
295
296        Bitmap bitmap = null;
297        if (cache != null) {
298            try {
299                bitmap = cache.copy(Bitmap.Config.ARGB_8888, false);
300            } catch (final OutOfMemoryError e) {
301                Log.w(LOG_TAG, "Failed to copy bitmap from Drawing cache", e);
302                bitmap = null;
303            }
304        }
305
306        view.destroyDrawingCache();
307        view.setDrawingCacheEnabled(false);
308
309        return bitmap;
310    }
311
312    @Override
313    public PhoneFavoriteSquareTileView getViewForLocation(int x, int y) {
314        getLocationOnScreen(mLocationOnScreen);
315        // Calculate the X and Y coordinates of the drag event relative to the view
316        final int viewX = x - mLocationOnScreen[0];
317        final int viewY = y - mLocationOnScreen[1];
318        final View child = getViewAtPosition(viewX, viewY);
319
320        if (!(child instanceof PhoneFavoriteSquareTileView)) {
321            return null;
322        }
323
324        return (PhoneFavoriteSquareTileView) child;
325    }
326}
327