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(this, 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