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