1 /* 2 * Copyright (C) 2023 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.car.carlauncher.recyclerview; 18 19 import static android.view.View.DRAG_FLAG_GLOBAL; 20 import static android.view.View.DRAG_FLAG_OPAQUE; 21 22 import static com.android.car.carlauncher.AppGridConstants.AppItemBoundDirection; 23 import static com.android.car.carlauncher.AppGridConstants.PageOrientation; 24 import static com.android.car.carlauncher.AppGridConstants.isHorizontal; 25 import static com.android.car.hidden.apis.HiddenApiAccess.DRAG_FLAG_REQUEST_SURFACE_FOR_RETURN_ANIMATION; 26 27 import android.content.ClipData; 28 import android.content.ComponentName; 29 import android.content.Context; 30 import android.graphics.Point; 31 import android.graphics.Rect; 32 import android.graphics.drawable.Drawable; 33 import android.graphics.drawable.TransitionDrawable; 34 import android.os.Handler; 35 import android.os.Looper; 36 import android.util.Pair; 37 import android.view.DragEvent; 38 import android.view.MotionEvent; 39 import android.view.View; 40 import android.view.ViewPropertyAnimator; 41 import android.view.ViewTreeObserver.OnGlobalLayoutListener; 42 import android.widget.ImageView; 43 import android.widget.LinearLayout; 44 import android.widget.TextView; 45 46 import androidx.annotation.NonNull; 47 import androidx.annotation.Nullable; 48 import androidx.recyclerview.widget.RecyclerView; 49 50 import com.android.car.carlauncher.AppGridActivity; 51 import com.android.car.carlauncher.AppGridPageSnapper.AppGridPageSnapCallback; 52 import com.android.car.carlauncher.AppItemDragShadowBuilder; 53 import com.android.car.carlauncher.AppMetaData; 54 import com.android.car.carlauncher.R; 55 import com.android.car.carlaunchercommon.toasts.NonDrivingOptimizedLaunchFailedToast; 56 57 /** 58 * App item view holder that contains the app icon and name. 59 */ 60 public class AppItemViewHolder extends RecyclerView.ViewHolder { 61 private static final String APP_ITEM_DRAG_TAG = "com.android.car.launcher.APP_ITEM_DRAG_TAG"; 62 private final long mReleaseAnimationDurationMs; 63 private final long mLongPressAnimationDurationMs; 64 private final long mDropAnimationDelayMs; 65 private final int mHighlightTransitionDurationMs; 66 private final int mIconSize; 67 private final int mIconScaledSize; 68 private final Context mContext; 69 private final LinearLayout mAppItemView; 70 private final ImageView mAppIcon; 71 private final TextView mAppName; 72 private final AppItemDragCallback mDragCallback; 73 private final AppGridPageSnapCallback mSnapCallback; 74 private final boolean mConfigReorderAllowed; 75 private final int mThresholdToStartDragDrop; 76 private Rect mPageBound; 77 78 @PageOrientation 79 private int mPageOrientation; 80 @AppItemBoundDirection 81 private int mDragExitDirection; 82 83 private boolean mHasAppMetadata; 84 private ComponentName mComponentName; 85 private Point mAppIconCenter; 86 private TransitionDrawable mBackgroundHighlight; 87 private int mAppItemWidth; 88 private int mAppItemHeight; 89 private boolean mIsTargeted; 90 private boolean mCanStartDragAction; 91 92 /** 93 * Information describing state of the recyclerview when this view holder was last rebinded. 94 * 95 * {@param isDistractionOptimizationRequired} true if driving restriction should be required. 96 * {@param pageBound} the bounds of the recyclerview containing this view holder. 97 */ 98 public static class BindInfo { 99 private final boolean mIsDistractionOptimizationRequired; 100 private final Rect mPageBound; 101 private final AppGridActivity.Mode mMode; 102 BindInfo(boolean isDistractionOptimizationRequired, Rect pageBound, AppGridActivity.Mode mode)103 public BindInfo(boolean isDistractionOptimizationRequired, 104 Rect pageBound, 105 AppGridActivity.Mode mode) { 106 this.mIsDistractionOptimizationRequired = isDistractionOptimizationRequired; 107 this.mPageBound = pageBound; 108 this.mMode = mode; 109 } 110 BindInfo(boolean isDistractionOptimizationRequired, Rect pageBound)111 public BindInfo(boolean isDistractionOptimizationRequired, Rect pageBound) { 112 this(isDistractionOptimizationRequired, pageBound, AppGridActivity.Mode.ALL_APPS); 113 } 114 } 115 AppItemViewHolder(View view, Context context, AppItemDragCallback dragCallback, AppGridPageSnapCallback snapCallback)116 public AppItemViewHolder(View view, Context context, AppItemDragCallback dragCallback, 117 AppGridPageSnapCallback snapCallback) { 118 super(view); 119 mContext = context; 120 mAppItemView = view.findViewById(R.id.app_item); 121 mAppIcon = mAppItemView.findViewById(R.id.app_icon); 122 mAppName = mAppItemView.findViewById(R.id.app_name); 123 mDragCallback = dragCallback; 124 mSnapCallback = snapCallback; 125 126 mIconSize = context.getResources().getDimensionPixelSize(R.dimen.app_icon_size); 127 mConfigReorderAllowed = context.getResources().getBoolean(R.bool.config_allow_reordering); 128 // distance that users must drag (hold and attempt to move the app icon) to initiate 129 // reordering, measured in pixels on screen. 130 mThresholdToStartDragDrop = context.getResources().getDimensionPixelSize( 131 R.dimen.threshold_to_start_drag_drop); 132 mPageOrientation = context.getResources().getBoolean(R.bool.use_vertical_app_grid) 133 ? PageOrientation.VERTICAL : PageOrientation.HORIZONTAL; 134 135 mIconScaledSize = context.getResources().getDimensionPixelSize( 136 R.dimen.app_icon_scaled_size); 137 // duration for animating the resizing of app icon on long press 138 mLongPressAnimationDurationMs = context.getResources().getInteger( 139 R.integer.ms_long_press_animation_duration); 140 // duration for animating the resizing after long press is released 141 mReleaseAnimationDurationMs = context.getResources().getInteger( 142 R.integer.ms_release_animation_duration); 143 // duration to animate the highlighting of view holder when it is targeted during drag drop 144 mHighlightTransitionDurationMs = context.getResources().getInteger( 145 R.integer.ms_background_highlight_duration); 146 // delay before animating the drop animation when a valid drop event has been received 147 mDropAnimationDelayMs = context.getResources().getInteger( 148 R.integer.ms_drop_animation_delay); 149 } 150 151 /** 152 * Binds the grid app item view with the app metadata. 153 * 154 * @param app AppMetaData to be displayed. Pass {@code null} will empty out the viewHolder. 155 */ bind(@ullable AppMetaData app, @NonNull BindInfo bindInfo)156 public void bind(@Nullable AppMetaData app, @NonNull BindInfo bindInfo) { 157 resetViewHolder(); 158 if (app == null) { 159 return; 160 } 161 boolean isDistractionOptimizationRequired = bindInfo.mIsDistractionOptimizationRequired; 162 mPageBound = bindInfo.mPageBound; 163 AppGridActivity.Mode mode = bindInfo.mMode; 164 165 mHasAppMetadata = true; 166 mAppItemView.setFocusable(true); 167 mAppName.setText(app.getDisplayName()); 168 mAppIcon.setImageDrawable(app.getIcon()); 169 mAppIcon.setAlpha(1.f); 170 mComponentName = app.getComponentName(); 171 172 Drawable highlightedLayer = mContext.getDrawable(R.drawable.app_item_highlight); 173 Drawable emptyLayer = mContext.getDrawable(R.drawable.app_item_highlight); 174 emptyLayer.setAlpha(0); 175 mBackgroundHighlight = new TransitionDrawable(new Drawable[]{emptyLayer, highlightedLayer}); 176 mBackgroundHighlight.resetTransition(); 177 mAppItemView.setBackground(mBackgroundHighlight); 178 179 // app icon's relative location within view holders are only measurable after it is drawn 180 181 // during a drag and drop operation, the user could scroll to another page and return to the 182 // previous page, so we need to rebind the app with the correct visibility. 183 setStateSelected(mComponentName.equals(mDragCallback.mSelectedComponent)); 184 185 boolean isLaunchableDistractionOptimized = !isDistractionOptimizationRequired 186 || app.getIsDistractionOptimized(); 187 boolean isDisabledByTos = app.getIsDisabledByTos(); 188 boolean isLaunchable = isLaunchableDistractionOptimized || isDisabledByTos; 189 190 int opacity = getOpacity(isLaunchable, isDisabledByTos); 191 mAppIcon.setAlpha(mContext.getResources().getFloat(opacity)); 192 193 if (isLaunchable) { 194 View.OnClickListener appLaunchListener = new View.OnClickListener() { 195 @Override 196 public void onClick(View v) { 197 app.getLaunchCallback().accept(mContext); 198 mSnapCallback.notifySnapToPosition(getAbsoluteAdapterPosition()); 199 } 200 }; 201 mAppItemView.setOnClickListener(appLaunchListener); 202 mAppIcon.setOnClickListener(appLaunchListener); 203 // long click actions should not be enabled when driving 204 if (!isDistractionOptimizationRequired) { 205 View.OnLongClickListener longPressListener = new View.OnLongClickListener() { 206 @Override 207 public boolean onLongClick(View v) { 208 // display set shortcut pop-up for force stop 209 app.getAlternateLaunchCallback().accept(Pair.create(mContext, v)); 210 // drag and drop should only start after long click animation is complete 211 mDragCallback.notifyItemLongPressed(true); 212 mDragCallback.scheduleDragTask(new Runnable() { 213 @Override 214 public void run() { 215 mCanStartDragAction = true; 216 } 217 }, mLongPressAnimationDurationMs); 218 animateIconResize(/* scale */ ((float) mIconScaledSize / mIconSize), 219 /* duration */ mLongPressAnimationDurationMs); 220 return true; 221 } 222 }; 223 mAppIcon.setLongClickable(true); 224 mAppIcon.setOnLongClickListener(longPressListener); 225 mAppIcon.setOnTouchListener(new View.OnTouchListener() { 226 private float mActionDownX; 227 private float mActionDownY; 228 229 @Override 230 public boolean onTouch(View v, MotionEvent event) { 231 int action = event.getAction(); 232 if (action == MotionEvent.ACTION_DOWN) { 233 mActionDownX = event.getX(); 234 mActionDownY = event.getY(); 235 mCanStartDragAction = false; 236 } else if (action == MotionEvent.ACTION_MOVE 237 && shouldStartDragAndDrop(event, 238 mActionDownX, 239 mActionDownY, 240 mode)) { 241 startDragAndDrop(app, event.getX(), event.getY()); 242 mCanStartDragAction = false; 243 } else if (action == MotionEvent.ACTION_UP 244 || action == MotionEvent.ACTION_CANCEL) { 245 animateIconResize(/* scale */ 1.f, 246 /* duration */ mReleaseAnimationDurationMs); 247 mDragCallback.cancelDragTasks(); 248 mDragCallback.notifyItemLongPressed(false); 249 mCanStartDragAction = false; 250 } 251 return false; 252 } 253 }); 254 } 255 } else { 256 View.OnClickListener appLaunchListener = v -> 257 NonDrivingOptimizedLaunchFailedToast.Companion.showToast( 258 mContext, app.getDisplayName()); 259 mAppItemView.setOnClickListener(appLaunchListener); 260 mAppIcon.setOnClickListener(appLaunchListener); 261 262 mAppIcon.setLongClickable(false); 263 mAppIcon.setOnLongClickListener(null); 264 mAppIcon.setOnTouchListener(null); 265 } 266 } 267 animateIconResize(float scale, long duration)268 void animateIconResize(float scale, long duration) { 269 mAppIcon.animate().setDuration(duration).scaleX(scale); 270 mAppIcon.animate().setDuration(duration).scaleY(scale); 271 } 272 273 /** 274 * Transforms the app icon into the drop shadow's drop location in preparation for animateDrop, 275 * which should be dispatched by AppGridItemAnimator shortly after prepareForDropAnimation. 276 */ prepareForDropAnimation()277 public void prepareForDropAnimation() { 278 // dragOffset is the offset between dragged icon center and users finger touch point 279 int dragOffsetX = mDragCallback.mDragPoint.x - mIconScaledSize / 2; 280 int dragOffsetY = mDragCallback.mDragPoint.y - mIconScaledSize / 2; 281 // draggedIconCenter is the center of the dropped app icon, after the user finger touch 282 // point offset is subtracted to another 283 int draggedIconCenterX = mDragCallback.mDropPoint.x - dragOffsetX; 284 int draggedIconCenterY = mDragCallback.mDropPoint.y - dragOffsetY; 285 // dx and dx are the offset to translate between the dragged icon and dropped location 286 int dx = draggedIconCenterX - mDragCallback.mDropDestination.x; 287 int dy = draggedIconCenterY - mDragCallback.mDropDestination.y; 288 mAppIcon.setScaleX((float) mIconScaledSize / mIconSize); 289 mAppIcon.setScaleY((float) mIconScaledSize / mIconSize); 290 mAppIcon.setAlpha(1.f); 291 mAppIcon.setTranslationX(dx); 292 mAppIcon.setTranslationY(dy); 293 mAppItemView.setTranslationZ(.5f); 294 mAppName.setTranslationZ(.5f); 295 mAppIcon.setTranslationZ(1.f); 296 } 297 298 /** 299 * Resets Z axis translation of all views contained by the view holder. 300 */ resetTranslationZ()301 public void resetTranslationZ() { 302 mAppItemView.setTranslationZ(0.f); 303 mAppIcon.setTranslationZ(0.f); 304 mAppName.setTranslationZ(0.f); 305 } 306 307 /** 308 * Animates the drop transition back to the original app icon location. 309 */ getDropAnimation()310 public ViewPropertyAnimator getDropAnimation() { 311 return mAppIcon.animate() 312 .translationX(0).translationY(0) 313 .scaleX(1.f).scaleY(1.f) 314 .setStartDelay(mDropAnimationDelayMs); 315 } 316 resetViewHolder()317 private void resetViewHolder() { 318 // TODO: Create a different item for empty app item. 319 mHasAppMetadata = false; 320 321 mAppItemView.setOnDragListener(new AppItemOnDragListener()); 322 mAppItemView.setFocusable(false); 323 mAppItemView.setOnClickListener(null); 324 325 mAppIcon.setLongClickable(false); 326 mAppIcon.setOnLongClickListener(null); 327 mAppIcon.setOnTouchListener(null); 328 mAppIcon.setAlpha(0.f); 329 mAppIcon.setOutlineProvider(null); 330 331 mAppIcon.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() { 332 @Override 333 public void onGlobalLayout() { 334 // remove listener since icon only need to be measured once 335 mAppIcon.getViewTreeObserver().removeOnGlobalLayoutListener(this); 336 Rect appIconBound = new Rect(); 337 mAppIcon.getDrawingRect(appIconBound); 338 mAppItemView.offsetDescendantRectToMyCoords(mAppIcon, appIconBound); 339 mAppIconCenter = new Point(/* x */ (appIconBound.right + appIconBound.left) / 2, 340 /* y */ (appIconBound.bottom + appIconBound.top) / 2); 341 mAppItemWidth = mAppItemView.getWidth(); 342 mAppItemHeight = mAppItemView.getHeight(); 343 } 344 }); 345 346 mAppItemView.setBackground(null); 347 mAppIcon.setImageDrawable(null); 348 mAppName.setText(null); 349 350 mDragExitDirection = AppItemBoundDirection.NONE; 351 } 352 setStateTargeted(boolean targeted)353 private void setStateTargeted(boolean targeted) { 354 if (mIsTargeted == targeted) return; 355 mIsTargeted = targeted; 356 if (targeted) { 357 mDragCallback.notifyItemTargeted(AppItemViewHolder.this); 358 mBackgroundHighlight.startTransition(mHighlightTransitionDurationMs); 359 return; 360 } 361 mDragCallback.notifyItemTargeted(null); 362 mBackgroundHighlight.resetTransition(); 363 } 364 setStateSelected(boolean selected)365 private void setStateSelected(boolean selected) { 366 if (selected) { 367 mAppIcon.setAlpha(0.f); 368 return; 369 } 370 if (mHasAppMetadata) { 371 mAppIcon.setAlpha(1.f); 372 } 373 } 374 375 shouldStartDragAndDrop(MotionEvent event, float actionDownX, float actionDownY, AppGridActivity.Mode mode)376 private boolean shouldStartDragAndDrop(MotionEvent event, float actionDownX, 377 float actionDownY, AppGridActivity.Mode mode) { 378 // If App Grid is not in all apps mode, we should not allow drag and drop 379 if (mode != AppGridActivity.Mode.ALL_APPS) { 380 return false; 381 } 382 // the move event should be with in the bounds of the app icon 383 boolean isEventWithinIcon = event.getX() >= 0 && event.getY() >= 0 384 && event.getX() < mIconScaledSize && event.getY() < mIconScaledSize; 385 // the move event should be further by more than mThresholdToStartDragDrop pixels 386 // away from the initial touch input. 387 boolean isDistancePastThreshold = Math.hypot(/* dx */ Math.abs(event.getX() - actionDownX), 388 /* dy */ event.getY() - actionDownY) > mThresholdToStartDragDrop; 389 return mConfigReorderAllowed && mCanStartDragAction && isEventWithinIcon 390 && isDistancePastThreshold; 391 } 392 393 private void startDragAndDrop(AppMetaData app, float eventX, float eventY) { 394 ClipData clipData = ClipData.newPlainText(/* label= */ APP_ITEM_DRAG_TAG, 395 /* text= */ app.getComponentName().flattenToString()); 396 397 // since the app icon is scaled, the touch point that users should be holding when drag 398 // shadow is deployed should also be scaled 399 Point dragPoint = new Point(/* x */ (int) (eventX / mIconSize * mIconScaledSize), 400 /* y */ (int) (eventY / mIconSize * mIconScaledSize)); 401 402 Drawable appIcon = app.getIcon(); 403 if (appIcon.getConstantState() == null) return; 404 AppItemDragShadowBuilder dragShadowBuilder = new AppItemDragShadowBuilder( 405 appIcon.getConstantState().newDrawable().mutate(), 406 /* touchPointX */ dragPoint.x, /* touchPointX */ dragPoint.y, 407 /* scaledSize */ mIconScaledSize); 408 mAppIcon.startDragAndDrop(clipData, /* dragShadowBuilder */ dragShadowBuilder, 409 /* myLocalState */ null, /* flags */ DRAG_FLAG_OPAQUE | DRAG_FLAG_GLOBAL 410 | DRAG_FLAG_REQUEST_SURFACE_FOR_RETURN_ANIMATION); 411 412 mDragCallback.notifyItemSelected(AppItemViewHolder.this, dragPoint); 413 } 414 415 class AppItemOnDragListener implements View.OnDragListener { 416 @Override 417 public boolean onDrag(View view, DragEvent event) { 418 int action = event.getAction(); 419 if (mHasAppMetadata) { 420 if (action == DragEvent.ACTION_DRAG_STARTED) { 421 if (isSelectedViewHolder()) { 422 setStateSelected(true); 423 } 424 } else if (action == DragEvent.ACTION_DRAG_LOCATION && inScrollStateIdle()) { 425 boolean shouldTargetViewHolder = isTargetIconVisible() 426 && isDraggedIconInBound(event) 427 && mDragCallback.mSelectedComponent != null; 428 setStateTargeted(shouldTargetViewHolder); 429 } else if (action == DragEvent.ACTION_DRAG_EXITED && inScrollStateIdle()) { 430 setStateTargeted(false); 431 } else if (action == DragEvent.ACTION_DROP) { 432 if (isTargetedViewHolder()) { 433 Point dropPoint = new Point(/* x */ (int) event.getX(), 434 /* y */ (int) event.getY()); 435 mDragCallback.notifyItemDropped(dropPoint); 436 } 437 setStateTargeted(false); 438 } 439 } 440 if (action == DragEvent.ACTION_DRAG_ENTERED && inScrollStateIdle()) { 441 mDragCallback.notifyItemDragged(); 442 } 443 if (action == DragEvent.ACTION_DRAG_LOCATION && inScrollStateIdle()) { 444 mDragExitDirection = getClosestBoundDirection(event.getX(), event.getY()); 445 mDragCallback.notifyItemDragged(); 446 } 447 if (action == DragEvent.ACTION_DRAG_EXITED && inScrollStateIdle()) { 448 mDragCallback.notifyDragExited(AppItemViewHolder.this, mDragExitDirection); 449 mDragExitDirection = AppItemBoundDirection.NONE; 450 } 451 if (event.getAction() == DragEvent.ACTION_DRAG_ENDED) { 452 mDragExitDirection = AppItemBoundDirection.NONE; 453 setStateSelected(false); 454 } 455 if (action == DragEvent.ACTION_DROP) { 456 return false; 457 } 458 return true; 459 } 460 } 461 462 private boolean isSelectedViewHolder() { 463 return mComponentName != null && mComponentName.equals(mDragCallback.mSelectedComponent); 464 } 465 466 private boolean isTargetedViewHolder() { 467 return mComponentName != null && mComponentName.equals(mDragCallback.mTargetedComponent); 468 } 469 470 private boolean inScrollStateIdle() { 471 return mSnapCallback.getScrollState() == RecyclerView.SCROLL_STATE_IDLE; 472 } 473 474 /** 475 * Returns whether this view holder's icon is visible to the user. 476 * 477 * Since the edge of the view holder from the previous/next may also receive drop events, a 478 * valid drop target should have its app icon be visible to the user. 479 */ 480 private boolean isTargetIconVisible() { 481 if (mAppIcon == null || mAppIcon.getMeasuredWidth() == 0) { 482 return false; 483 } 484 final Rect bound = new Rect(); 485 mAppIcon.getGlobalVisibleRect(bound); 486 return bound.intersect(mPageBound); 487 } 488 489 private boolean isDraggedIconInBound(DragEvent event) { 490 int iconLeft = (int) event.getX() - mDragCallback.mDragPoint.x; 491 int iconTop = (int) event.getY() - mDragCallback.mDragPoint.y; 492 return iconLeft >= 0 && iconTop >= 0 && (iconLeft + mIconScaledSize) < mAppItemWidth 493 && (iconTop + mIconScaledSize) < mAppItemHeight; 494 } 495 496 @AppItemBoundDirection 497 int getClosestBoundDirection(float eventX, float eventY) { 498 float cutoffThreshold = .25f; 499 if (isHorizontal(mPageOrientation)) { 500 float horizontalPosition = eventX / mAppItemWidth; 501 if (horizontalPosition < cutoffThreshold) { 502 return AppItemBoundDirection.LEFT; 503 } else if (horizontalPosition > (1 - cutoffThreshold)) { 504 return AppItemBoundDirection.RIGHT; 505 } 506 return AppItemBoundDirection.NONE; 507 } 508 float verticalPosition = eventY / mAppItemHeight; 509 if (verticalPosition < .5f) { 510 return AppItemBoundDirection.TOP; 511 } else if (verticalPosition > (1 - cutoffThreshold)) { 512 return AppItemBoundDirection.BOTTOM; 513 } 514 return AppItemBoundDirection.NONE; 515 } 516 517 public boolean isMostRecentlySelected() { 518 return mComponentName != null 519 && mComponentName.equals(mDragCallback.getPreviousSelectedComponent()); 520 } 521 522 /** 523 * A Callback contract between AppItemViewHolders and its listener. There are multiple view 524 * holders updating the callback but there should only be one listener. 525 * 526 * Drag drop operations will be started and listened to by each AppItemViewHolder, so all 527 * visual elements should be handled directly by the AppItemViewHolder. This class should only 528 * be used to communicate adapter data position changes. 529 */ 530 public static class AppItemDragCallback { 531 private static final int NONE = -1; 532 private final AppItemDragListener mDragListener; 533 private final Handler mHandler; 534 private ComponentName mPreviousSelectedComponent; 535 private ComponentName mSelectedComponent; 536 private ComponentName mTargetedComponent; 537 private AppItemViewHolder mTargetedViewHolder; 538 private int mSelectedGridIndex = NONE; 539 private int mTargetedGridIndex = NONE; 540 // x y coordinate within the source app icon that the user finger is holding 541 private Point mDragPoint; 542 // x y coordinate within the viewHolder the drop event was registered 543 private Point mDropPoint; 544 // x y coordinate within the viewHolder which the drop animation should translate to 545 private Point mDropDestination; 546 547 public AppItemDragCallback(AppItemDragListener listener) { 548 mDragListener = listener; 549 mHandler = new Handler(Looper.getMainLooper()); 550 } 551 552 /** 553 * The preparation step of drag drop process. Called when a long press gesture has been 554 * inputted or cancelled by the user. 555 */ 556 public void notifyItemLongPressed(boolean isLongPressed) { 557 mDragListener.onItemLongPressed(isLongPressed); 558 } 559 560 /** 561 * The initial step of the drag drop process. Called when the drag shadow of an app icon has 562 * been created, and should be immediately set as the drag source. 563 */ 564 public void notifyItemSelected(AppItemViewHolder viewHolder, Point dragPoint) { 565 mDragPoint = new Point(dragPoint); 566 mDropDestination = new Point(viewHolder.mAppIconCenter); 567 mSelectedComponent = viewHolder.mComponentName; 568 mSelectedGridIndex = viewHolder.getAbsoluteAdapterPosition(); 569 mDragListener.onItemSelected(mSelectedGridIndex); 570 } 571 572 /** 573 * The second step of the drag drop process. Called when a drag shadow enters the bounds of 574 * a view holder (including the view holder containing the dragged icon itself). 575 */ 576 public void notifyItemTargeted(@Nullable AppItemViewHolder viewHolder) { 577 if (mTargetedViewHolder != null && !mTargetedViewHolder.equals(viewHolder)) { 578 mTargetedViewHolder.setStateTargeted(false); 579 } 580 if (viewHolder == null) { 581 mTargetedComponent = null; 582 mTargetedViewHolder = null; 583 mTargetedGridIndex = NONE; 584 return; 585 } 586 mTargetedComponent = viewHolder.mComponentName; 587 mTargetedViewHolder = viewHolder; 588 mTargetedGridIndex = viewHolder.getAbsoluteAdapterPosition(); 589 } 590 591 /** 592 * An intermediary step of the drag drop process. Called the drag shadow enters the 593 * view holder. 594 */ 595 public void notifyItemDragged() { 596 mDragListener.onItemDragged(); 597 } 598 599 /** 600 * An intermediary step of the drag drop process. Called the drag shadow is dragged outside 601 * the view holder. 602 */ 603 public void notifyDragExited(@NonNull AppItemViewHolder viewHolder, 604 @AppItemBoundDirection int exitDirection) { 605 int gridPosition = viewHolder.getAbsoluteAdapterPosition(); 606 mDragListener.onDragExited(gridPosition, exitDirection); 607 } 608 609 /** 610 * The last step of drag and drop. Called when a ACTION_DROP event has been received by a 611 * view holder. 612 * 613 * Note that this event may never be called if the ACTION_DROP event was consumed by 614 * another onDragListener. 615 */ 616 public void notifyItemDropped(Point dropPoint) { 617 mDropPoint = new Point(dropPoint); 618 if (mSelectedGridIndex != NONE && mTargetedGridIndex != NONE) { 619 mDragListener.onItemDropped(mSelectedGridIndex, mTargetedGridIndex); 620 resetCallbackState(); 621 } 622 } 623 624 /** Returns the previously selected component. */ 625 public ComponentName getPreviousSelectedComponent() { 626 return mPreviousSelectedComponent; 627 } 628 629 /** Reset component and callback state after a drag drop event has concluded */ 630 public void resetCallbackState() { 631 if (mSelectedComponent != null) { 632 mPreviousSelectedComponent = mSelectedComponent; 633 } 634 mSelectedComponent = mTargetedComponent = null; 635 mSelectedGridIndex = mTargetedGridIndex = NONE; 636 } 637 638 /** Schedules a delayed task that enables drag and drop to start */ 639 public void scheduleDragTask(Runnable runnable, long delay) { 640 mHandler.postDelayed(runnable, delay); 641 } 642 643 /** Cancels all schedules tasks (i.e cancels intent to start drag drop) */ 644 public void cancelDragTasks() { 645 mHandler.removeCallbacksAndMessages(null); 646 } 647 } 648 649 /** 650 * Listener class that should be implemented by AppGridActivity. 651 */ 652 public interface AppItemDragListener { 653 /** Listener method called during AppItemDragCallback.notifyLongPressed */ 654 void onItemLongPressed(boolean longPressed); 655 656 /** Listener method called during AppItemDragCallback.notifyItemSelected */ 657 void onItemSelected(int gridPositionFrom); 658 659 /** Listener method called during AppItemDragCallback.notifyDragEntered */ 660 void onItemDragged(); 661 662 /** Listener method called during AppItemDragCallback.notifyDragExited */ 663 void onDragExited(int gridPosition, @AppItemBoundDirection int exitDirection); 664 665 /** Listener method called during AppItemDragCallback.notifyItemDropped */ 666 void onItemDropped(int gridPositionFrom, int gridPositionTo); 667 } 668 669 private int getOpacity(boolean isLaunchable, boolean isDisabledByTos) { 670 if (isDisabledByTos) { 671 return R.dimen.app_icon_opacity_tos_disabled; 672 } 673 if (isLaunchable) { 674 return R.dimen.app_icon_opacity; 675 } 676 return R.dimen.app_icon_opacity_unavailable; 677 } 678 } 679