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 
18 package com.android.dialer.list;
19 
20 import android.animation.Animator;
21 import android.animation.AnimatorListenerAdapter;
22 import android.content.Context;
23 import android.content.res.Configuration;
24 import android.graphics.Bitmap;
25 import android.os.Handler;
26 import android.text.TextUtils;
27 import android.util.AttributeSet;
28 import android.util.Log;
29 import android.view.DragEvent;
30 import android.view.MotionEvent;
31 import android.view.View;
32 import android.view.ViewConfiguration;
33 import android.widget.GridView;
34 import android.widget.ImageView;
35 
36 import com.android.dialer.R;
37 import com.android.dialer.list.DragDropController.DragItemContainer;
38 
39 /**
40  * Viewgroup that presents the user's speed dial contacts in a grid.
41  */
42 public 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 
PhoneFavoriteListView(Context context)111     public PhoneFavoriteListView(Context context) {
112         this(context, null);
113     }
114 
PhoneFavoriteListView(Context context, AttributeSet attrs)115     public PhoneFavoriteListView(Context context, AttributeSet attrs) {
116         this(context, attrs, -1);
117     }
118 
PhoneFavoriteListView(Context context, AttributeSet attrs, int defStyle)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
onConfigurationChanged(Configuration newConfig)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
onInterceptTouchEvent(MotionEvent ev)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
onDragEvent(DragEvent event)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 
setDragShadowOverlay(ImageView overlay)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      */
getViewAtPosition(int x, int y)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 
ensureScrollHandler()219     private void ensureScrollHandler() {
220         if (mScrollHandler == null) {
221             mScrollHandler = getHandler();
222         }
223     }
224 
getDragDropController()225     public DragDropController getDragDropController() {
226         return mDragDropController;
227     }
228 
229     @Override
onDragStarted(int x, int y, PhoneFavoriteSquareTileView tileView)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
onDragHovered(int x, int y, PhoneFavoriteSquareTileView tileView)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
onDragFinished(int x, int y)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
onDroppedOnRemove()290     public void onDroppedOnRemove() {}
291 
createDraggedChildBitmap(View view)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
getViewForLocation(int x, int y)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