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