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