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