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