• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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