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