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[] mLocationOnScreen = 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 #mTopScrollBound} and {@link mBottomScrollBound} 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 mTouchSlop; 52 private int mTopScrollBound; 53 private int mBottomScrollBound; 54 private int mLastDragY; 55 private Handler mScrollHandler; 56 private final Runnable mDragScroller = 57 new Runnable() { 58 @Override 59 public void run() { 60 if (mLastDragY <= mTopScrollBound) { 61 smoothScrollBy(-DRAG_SCROLL_PX_UNIT, (int) SCROLL_HANDLER_DELAY_MILLIS); 62 } else if (mLastDragY >= mBottomScrollBound) { 63 smoothScrollBy(DRAG_SCROLL_PX_UNIT, (int) SCROLL_HANDLER_DELAY_MILLIS); 64 } 65 mScrollHandler.postDelayed(this, SCROLL_HANDLER_DELAY_MILLIS); 66 } 67 }; 68 private boolean mIsDragScrollerRunning = false; 69 private int mTouchDownForDragStartY; 70 private Bitmap mDragShadowBitmap; 71 private ImageView mDragShadowOverlay; 72 private final AnimatorListenerAdapter mDragShadowOverAnimatorListener = 73 new AnimatorListenerAdapter() { 74 @Override 75 public void onAnimationEnd(Animator animation) { 76 if (mDragShadowBitmap != null) { 77 mDragShadowBitmap.recycle(); 78 mDragShadowBitmap = null; 79 } 80 mDragShadowOverlay.setVisibility(GONE); 81 mDragShadowOverlay.setImageBitmap(null); 82 } 83 }; 84 private View mDragShadowParent; 85 private int mAnimationDuration; 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 mTouchOffsetToChildLeft; 89 private int mTouchOffsetToChildTop; 90 private int mDragShadowLeft; 91 private int mDragShadowTop; 92 private DragDropController mDragDropController = new DragDropController(this); 93 94 public PhoneFavoriteListView(Context context) { 95 this(context, null); 96 } 97 98 public PhoneFavoriteListView(Context context, AttributeSet attrs) { 99 this(context, attrs, 0); 100 } 101 102 public PhoneFavoriteListView(Context context, AttributeSet attrs, int defStyle) { 103 super(context, attrs, defStyle); 104 mAnimationDuration = context.getResources().getInteger(R.integer.fade_duration); 105 mTouchSlop = ViewConfiguration.get(context).getScaledPagingTouchSlop(); 106 mDragDropController.addOnDragDropListener(this); 107 } 108 109 @Override 110 protected void onConfigurationChanged(Configuration newConfig) { 111 super.onConfigurationChanged(newConfig); 112 mTouchSlop = 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 120 public boolean onInterceptTouchEvent(MotionEvent ev) { 121 if (ev.getAction() == MotionEvent.ACTION_DOWN) { 122 mTouchDownForDragStartY = (int) ev.getY(); 123 } 124 125 return super.onInterceptTouchEvent(ev); 126 } 127 128 @Override 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 (!mDragDropController.handleDragStarted(this, eX, eY)) { 142 return false; 143 } 144 break; 145 } 146 case DragEvent.ACTION_DRAG_LOCATION: 147 mLastDragY = eY; 148 mDragDropController.handleDragHovered(this, eX, eY); 149 // Kick off {@link #mScrollHandler} if it's not started yet. 150 if (!mIsDragScrollerRunning 151 && 152 // And if the distance traveled while dragging exceeds the touch slop 153 (Math.abs(mLastDragY - mTouchDownForDragStartY) >= 4 * mTouchSlop)) { 154 mIsDragScrollerRunning = true; 155 ensureScrollHandler(); 156 mScrollHandler.postDelayed(mDragScroller, SCROLL_HANDLER_DELAY_MILLIS); 157 } 158 break; 159 case DragEvent.ACTION_DRAG_ENTERED: 160 final int boundGap = (int) (getHeight() * BOUND_GAP_RATIO); 161 mTopScrollBound = (getTop() + boundGap); 162 mBottomScrollBound = (getBottom() - boundGap); 163 break; 164 case DragEvent.ACTION_DRAG_EXITED: 165 case DragEvent.ACTION_DRAG_ENDED: 166 case DragEvent.ACTION_DROP: 167 ensureScrollHandler(); 168 mScrollHandler.removeCallbacks(mDragScroller); 169 mIsDragScrollerRunning = 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 mDragDropController.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 182 public void setDragShadowOverlay(ImageView overlay) { 183 mDragShadowOverlay = overlay; 184 mDragShadowParent = (View) mDragShadowOverlay.getParent(); 185 } 186 187 /** Find the view under the pointer. */ 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 203 private void ensureScrollHandler() { 204 if (mScrollHandler == null) { 205 mScrollHandler = getHandler(); 206 } 207 } 208 209 public DragDropController getDragDropController() { 210 return mDragDropController; 211 } 212 213 @Override 214 public void onDragStarted(int x, int y, PhoneFavoriteSquareTileView tileView) { 215 if (mDragShadowOverlay == null) { 216 return; 217 } 218 219 mDragShadowOverlay.clearAnimation(); 220 mDragShadowBitmap = createDraggedChildBitmap(tileView); 221 if (mDragShadowBitmap == null) { 222 return; 223 } 224 225 tileView.getLocationOnScreen(mLocationOnScreen); 226 mDragShadowLeft = mLocationOnScreen[0]; 227 mDragShadowTop = mLocationOnScreen[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 mTouchOffsetToChildLeft = x - mDragShadowLeft; 235 mTouchOffsetToChildTop = y - mDragShadowTop; 236 237 mDragShadowParent.getLocationOnScreen(mLocationOnScreen); 238 mDragShadowLeft -= mLocationOnScreen[0]; 239 mDragShadowTop -= mLocationOnScreen[1]; 240 241 mDragShadowOverlay.setImageBitmap(mDragShadowBitmap); 242 mDragShadowOverlay.setVisibility(VISIBLE); 243 mDragShadowOverlay.setAlpha(DRAG_SHADOW_ALPHA); 244 245 mDragShadowOverlay.setX(mDragShadowLeft); 246 mDragShadowOverlay.setY(mDragShadowTop); 247 } 248 249 @Override 250 public void onDragHovered(int x, int y, PhoneFavoriteSquareTileView tileView) { 251 // Update the drag shadow location. 252 mDragShadowParent.getLocationOnScreen(mLocationOnScreen); 253 mDragShadowLeft = x - mTouchOffsetToChildLeft - mLocationOnScreen[0]; 254 mDragShadowTop = y - mTouchOffsetToChildTop - mLocationOnScreen[1]; 255 // Draw the drag shadow at its last known location if the drag shadow exists. 256 if (mDragShadowOverlay != null) { 257 mDragShadowOverlay.setX(mDragShadowLeft); 258 mDragShadowOverlay.setY(mDragShadowTop); 259 } 260 } 261 262 @Override 263 public void onDragFinished(int x, int y) { 264 if (mDragShadowOverlay != null) { 265 mDragShadowOverlay.clearAnimation(); 266 mDragShadowOverlay 267 .animate() 268 .alpha(0.0f) 269 .setDuration(mAnimationDuration) 270 .setListener(mDragShadowOverAnimatorListener) 271 .start(); 272 } 273 } 274 275 @Override 276 public void onDroppedOnRemove() {} 277 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 299 public PhoneFavoriteSquareTileView getViewForLocation(int x, int y) { 300 getLocationOnScreen(mLocationOnScreen); 301 // Calculate the X and Y coordinates of the drag event relative to the view 302 final int viewX = x - mLocationOnScreen[0]; 303 final int viewY = y - mLocationOnScreen[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