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.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.ImageView; 33 import android.widget.ListView; 34 35 import com.android.dialer.R; 36 import com.android.dialer.list.PhoneFavoritesTileAdapter.ContactTileRow; 37 import com.android.dialer.list.SwipeHelper.OnItemGestureListener; 38 import com.android.dialer.list.SwipeHelper.SwipeHelperCallback; 39 40 /** 41 * The ListView composed of {@link ContactTileRow}. 42 * This ListView handles both 43 * - Swiping, which is borrowed from packages/apps/UnifiedEmail (com.android.mail.ui.Swipeable) 44 * - Drag and drop 45 */ 46 public class PhoneFavoriteListView extends ListView implements SwipeHelperCallback, 47 OnDragDropListener { 48 49 public static final String LOG_TAG = PhoneFavoriteListView.class.getSimpleName(); 50 51 private SwipeHelper mSwipeHelper; 52 private boolean mEnableSwipe = true; 53 54 private OnItemGestureListener mOnItemGestureListener; 55 56 private float mDensityScale; 57 private float mTouchSlop; 58 59 private int mTopScrollBound; 60 private int mBottomScrollBound; 61 private int mLastDragY; 62 63 private Handler mScrollHandler; 64 private final long SCROLL_HANDLER_DELAY_MILLIS = 5; 65 private final int DRAG_SCROLL_PX_UNIT = 25; 66 67 private boolean mIsDragScrollerRunning = false; 68 private int mTouchDownForDragStartX; 69 private int mTouchDownForDragStartY; 70 71 private Bitmap mDragShadowBitmap; 72 private ImageView mDragShadowOverlay; 73 private int mAnimationDuration; 74 75 // X and Y offsets inside the item from where the user grabbed to the 76 // child's left coordinate. This is used to aid in the drawing of the drag shadow. 77 private int mTouchOffsetToChildLeft; 78 private int mTouchOffsetToChildTop; 79 80 private int mDragShadowLeft; 81 private int mDragShadowTop; 82 83 private DragDropController mDragDropController = new DragDropController(); 84 85 private final float DRAG_SHADOW_ALPHA = 0.7f; 86 87 /** 88 * {@link #mTopScrollBound} and {@link mBottomScrollBound} will be 89 * offseted to the top / bottom by {@link #getHeight} * {@link #BOUND_GAP_RATIO} pixels. 90 */ 91 private final float BOUND_GAP_RATIO = 0.2f; 92 93 private final Runnable mDragScroller = new Runnable() { 94 @Override 95 public void run() { 96 if (mLastDragY <= mTopScrollBound) { 97 smoothScrollBy(-DRAG_SCROLL_PX_UNIT, (int) SCROLL_HANDLER_DELAY_MILLIS); 98 } else if (mLastDragY >= mBottomScrollBound) { 99 smoothScrollBy(DRAG_SCROLL_PX_UNIT, (int) SCROLL_HANDLER_DELAY_MILLIS); 100 } 101 mScrollHandler.postDelayed(this, SCROLL_HANDLER_DELAY_MILLIS); 102 } 103 }; 104 105 private final AnimatorListenerAdapter mDragShadowOverAnimatorListener = 106 new AnimatorListenerAdapter() { 107 @Override 108 public void onAnimationEnd(Animator animation) { 109 if (mDragShadowBitmap != null) { 110 mDragShadowBitmap.recycle(); 111 mDragShadowBitmap = null; 112 } 113 mDragShadowOverlay.setVisibility(GONE); 114 mDragShadowOverlay.setImageBitmap(null); 115 } 116 }; 117 118 public PhoneFavoriteListView(Context context) { 119 this(context, null); 120 } 121 122 public PhoneFavoriteListView(Context context, AttributeSet attrs) { 123 this(context, attrs, -1); 124 } 125 126 public PhoneFavoriteListView(Context context, AttributeSet attrs, int defStyle) { 127 super(context, attrs, defStyle); 128 mAnimationDuration = context.getResources().getInteger(R.integer.fade_duration); 129 mDensityScale = getResources().getDisplayMetrics().density; 130 mTouchSlop = ViewConfiguration.get(context).getScaledPagingTouchSlop(); 131 mSwipeHelper = new SwipeHelper(context, SwipeHelper.X, this, 132 mDensityScale, mTouchSlop); 133 setItemsCanFocus(true); 134 mDragDropController.addOnDragDropListener(this); 135 } 136 137 @Override 138 protected void onConfigurationChanged(Configuration newConfig) { 139 super.onConfigurationChanged(newConfig); 140 mDensityScale= getResources().getDisplayMetrics().density; 141 mTouchSlop = ViewConfiguration.get(getContext()).getScaledPagingTouchSlop(); 142 mSwipeHelper.setDensityScale(mDensityScale); 143 mSwipeHelper.setPagingTouchSlop(mTouchSlop); 144 } 145 146 /** 147 * Enable swipe gestures. 148 */ 149 public void enableSwipe(boolean enable) { 150 mEnableSwipe = enable; 151 } 152 153 public boolean isSwipeEnabled() { 154 return mEnableSwipe && mOnItemGestureListener.isSwipeEnabled(); 155 } 156 157 public void setOnItemSwipeListener(OnItemGestureListener listener) { 158 mOnItemGestureListener = listener; 159 } 160 161 /** 162 * TODO: This is all swipe to remove code (nothing to do with drag to remove). This should 163 * be cleaned up and removed once drag to remove becomes the only way to remove contacts. 164 */ 165 @Override 166 public boolean onInterceptTouchEvent(MotionEvent ev) { 167 if (ev.getAction() == MotionEvent.ACTION_DOWN) { 168 mTouchDownForDragStartX = (int) ev.getX(); 169 mTouchDownForDragStartY = (int) ev.getY(); 170 } 171 if (isSwipeEnabled()) { 172 return mSwipeHelper.onInterceptTouchEvent(ev) || super.onInterceptTouchEvent(ev); 173 } else { 174 return super.onInterceptTouchEvent(ev); 175 } 176 } 177 178 @Override 179 public boolean onTouchEvent(MotionEvent ev) { 180 if (mOnItemGestureListener != null) { 181 mOnItemGestureListener.onTouch(); 182 } 183 if (isSwipeEnabled()) { 184 return mSwipeHelper.onTouchEvent(ev) || super.onTouchEvent(ev); 185 } else { 186 return super.onTouchEvent(ev); 187 } 188 } 189 190 @Override 191 public View getChildAtPosition(MotionEvent ev) { 192 final View view = getViewAtPosition((int) ev.getX(), (int) ev.getY()); 193 if (view != null && 194 SwipeHelper.isSwipeable(view) && 195 view.getVisibility() != GONE) { 196 // If this view is swipable in this listview, then return it. Otherwise 197 // return a null view, which will simply be ignored by the swipe helper. 198 return view; 199 } 200 return null; 201 } 202 203 @Override 204 public View getChildContentView(View view) { 205 return view.findViewById(R.id.contact_favorite_card); 206 } 207 208 @Override 209 public void onScroll() {} 210 211 @Override 212 public boolean canChildBeDismissed(View v) { 213 return SwipeHelper.isSwipeable(v); 214 } 215 216 @Override 217 public void onChildDismissed(final View v) { 218 if (v != null) { 219 if (mOnItemGestureListener != null) { 220 mOnItemGestureListener.onSwipe(v); 221 } 222 } 223 } 224 225 @Override 226 public void onDragCancelled(View v) {} 227 228 @Override 229 public void onBeginDrag(View v) { 230 final View tileRow = (View) v.getParent(); 231 232 // We do this so the underlying ScrollView knows that it won't get 233 // the chance to intercept events anymore 234 requestDisallowInterceptTouchEvent(true); 235 } 236 237 /** 238 * End of swipe-to-remove code 239 */ 240 241 @Override 242 public boolean dispatchDragEvent(DragEvent event) { 243 final int action = event.getAction(); 244 final int eX = (int) event.getX(); 245 final int eY = (int) event.getY(); 246 switch (action) { 247 case DragEvent.ACTION_DRAG_STARTED: 248 final int[] coordinates = new int[2]; 249 getLocationOnScreen(coordinates); 250 // Calculate the X and Y coordinates of the drag event relative to the view 251 final int viewX = eX - coordinates[0]; 252 final int viewY = eY - coordinates[1]; 253 final View child = getViewAtPosition(viewX, viewY); 254 255 if (!(child instanceof ContactTileRow)) { 256 // Bail early. 257 return false; 258 } 259 260 final ContactTileRow tile = (ContactTileRow) child; 261 262 // Disable drag and drop if there is a contact that has been swiped and is currently 263 // in the pending remove state 264 if (tile.getTileAdapter().hasPotentialRemoveEntryIndex()) { 265 return false; 266 } 267 268 if (!mDragDropController.handleDragStarted(viewX, viewY, tile)) { 269 return false; 270 } 271 break; 272 case DragEvent.ACTION_DRAG_LOCATION: 273 mLastDragY = eY; 274 final View view = getViewAtPosition(eX, eY); 275 mDragDropController.handleDragHovered(eX, eY, view); 276 // Kick off {@link #mScrollHandler} if it's not started yet. 277 if (!mIsDragScrollerRunning && 278 // And if the distance traveled while dragging exceeds the touch slop 279 (Math.abs(mLastDragY - mTouchDownForDragStartY) >= 4 * mTouchSlop)) { 280 mIsDragScrollerRunning = true; 281 ensureScrollHandler(); 282 mScrollHandler.postDelayed(mDragScroller, SCROLL_HANDLER_DELAY_MILLIS); 283 } 284 break; 285 case DragEvent.ACTION_DRAG_ENTERED: 286 final int boundGap = (int) (getHeight() * BOUND_GAP_RATIO); 287 mTopScrollBound = (getTop() + boundGap); 288 mBottomScrollBound = (getBottom() - boundGap); 289 break; 290 case DragEvent.ACTION_DRAG_EXITED: 291 case DragEvent.ACTION_DRAG_ENDED: 292 case DragEvent.ACTION_DROP: 293 ensureScrollHandler(); 294 mScrollHandler.removeCallbacks(mDragScroller); 295 mIsDragScrollerRunning = false; 296 // Either a successful drop or it's ended with out drop. 297 if (action == DragEvent.ACTION_DROP || action == DragEvent.ACTION_DRAG_ENDED) { 298 mDragDropController.handleDragFinished(eX, eY, false); 299 } 300 break; 301 default: 302 break; 303 } 304 // This ListView will consume the drag events on behalf of its children. 305 return true; 306 } 307 308 public void setDragShadowOverlay(ImageView overlay) { 309 mDragShadowOverlay = overlay; 310 } 311 312 /** 313 * Find the view under the pointer. 314 */ 315 private View getViewAtPosition(int x, int y) { 316 final int count = getChildCount(); 317 View child; 318 for (int childIdx = 0; childIdx < count; childIdx++) { 319 child = getChildAt(childIdx); 320 if (y >= child.getTop() && y <= child.getBottom()) { 321 return child; 322 } 323 } 324 return null; 325 } 326 327 private void ensureScrollHandler() { 328 if (mScrollHandler == null) { 329 mScrollHandler = getHandler(); 330 } 331 } 332 333 public DragDropController getDragDropController() { 334 return mDragDropController; 335 } 336 337 @Override 338 public void onDragStarted(int itemIndex, int x, int y, PhoneFavoriteTileView tileView) { 339 if (mDragShadowOverlay == null) { 340 return; 341 } 342 343 mDragShadowOverlay.clearAnimation(); 344 mDragShadowBitmap = createDraggedChildBitmap(tileView); 345 if (mDragShadowBitmap == null) { 346 return; 347 } 348 349 if (tileView instanceof PhoneFavoriteRegularRowView) { 350 mDragShadowLeft = tileView.getParentRow().getLeft(); 351 mDragShadowTop = tileView.getParentRow().getTop(); 352 } else { 353 // Square tile is relative to the contact tile, 354 // and contact tile is relative to this list view. 355 mDragShadowLeft = tileView.getLeft() + tileView.getParentRow().getLeft(); 356 mDragShadowTop = tileView.getTop() + tileView.getParentRow().getTop(); 357 } 358 359 mDragShadowOverlay.setImageBitmap(mDragShadowBitmap); 360 mDragShadowOverlay.setVisibility(VISIBLE); 361 mDragShadowOverlay.setAlpha(DRAG_SHADOW_ALPHA); 362 363 mDragShadowOverlay.setX(mDragShadowLeft); 364 mDragShadowOverlay.setY(mDragShadowTop); 365 366 // x and y passed in are the coordinates of where the user has touched down, 367 // calculate the offset to the top left coordinate of the dragged child. This 368 // will be used for drawing the drag shadow. 369 mTouchOffsetToChildLeft = x - mDragShadowLeft; 370 mTouchOffsetToChildTop = y - mDragShadowTop; 371 } 372 373 @Override 374 public void onDragHovered(int itemIndex, int x, int y) { 375 // Update the drag shadow location. 376 mDragShadowLeft = x - mTouchOffsetToChildLeft; 377 mDragShadowTop = y - mTouchOffsetToChildTop; 378 // Draw the drag shadow at its last known location if the drag shadow exists. 379 if (mDragShadowOverlay != null) { 380 mDragShadowOverlay.setX(mDragShadowLeft); 381 mDragShadowOverlay.setY(mDragShadowTop); 382 } 383 } 384 385 @Override 386 public void onDragFinished(int x, int y) { 387 // Update the drag shadow location. 388 mDragShadowLeft = x - mTouchOffsetToChildLeft; 389 mDragShadowTop = y - mTouchOffsetToChildTop; 390 391 if (mDragShadowOverlay != null) { 392 mDragShadowOverlay.clearAnimation(); 393 mDragShadowOverlay.animate().alpha(0.0f) 394 .setDuration(mAnimationDuration) 395 .setListener(mDragShadowOverAnimatorListener) 396 .start(); 397 } 398 } 399 400 @Override 401 public void onDroppedOnRemove() {} 402 403 private Bitmap createDraggedChildBitmap(View view) { 404 view.setDrawingCacheEnabled(true); 405 final Bitmap cache = view.getDrawingCache(); 406 407 Bitmap bitmap = null; 408 if (cache != null) { 409 try { 410 bitmap = cache.copy(Bitmap.Config.ARGB_8888, false); 411 } catch (final OutOfMemoryError e) { 412 Log.w(LOG_TAG, "Failed to copy bitmap from Drawing cache", e); 413 bitmap = null; 414 } 415 } 416 417 view.destroyDrawingCache(); 418 view.setDrawingCacheEnabled(false); 419 420 return bitmap; 421 } 422 } 423