1 /*
2  * Copyright (C) 2016 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 androidx.core.view;
18 
19 
20 import android.graphics.Point;
21 import android.view.MotionEvent;
22 import android.view.View;
23 
24 import org.jspecify.annotations.NonNull;
25 
26 /**
27  * DragStartHelper is a utility class for implementing drag and drop support.
28  * <p>
29  * It detects gestures commonly used to start drag (long click for any input source,
30  * click and drag for mouse).
31  * <p>
32  * It also keeps track of the screen location where the drag started, and helps determining
33  * the hot spot position for a drag shadow.
34  * <p>
35  * Implement {@link DragStartHelper.OnDragStartListener} to start the drag operation:
36  * <pre>
37  * DragStartHelper.OnDragStartListener listener = new DragStartHelper.OnDragStartListener {
38  *     protected void onDragStart(View view, DragStartHelper helper) {
39  *         View.DragShadowBuilder shadowBuilder = new View.DragShadowBuilder(view) {
40  *             public void onProvideShadowMetrics(Point shadowSize, Point shadowTouchPoint) {
41  *                 super.onProvideShadowMetrics(shadowSize, shadowTouchPoint);
42  *                 helper.getTouchPosition(shadowTouchPoint);
43  *             }
44  *         };
45  *         view.startDrag(mClipData, shadowBuilder, mLocalState, mDragFlags);
46  *     }
47  * };
48  * mDragStartHelper = new DragStartHelper(mDraggableView, listener);
49  * </pre>
50  * Once created, DragStartHelper can be attached to a view (this will replace existing long click
51  * and touch listeners):
52  * <pre>
53  * mDragStartHelper.attach();
54  * </pre>
55  * It may also be used in combination with existing listeners:
56  * <pre>
57  * public boolean onTouch(View view, MotionEvent event) {
58  *     if (mDragStartHelper.onTouch(view, event)) {
59  *         return true;
60  *     }
61  *     return handleTouchEvent(view, event);
62  * }
63  * public boolean onLongClick(View view) {
64  *     if (mDragStartHelper.onLongClick(view)) {
65  *         return true;
66  *     }
67  *     return handleLongClickEvent(view);
68  * }
69  * </pre>
70  */
71 public class DragStartHelper {
72     private final View mView;
73     private final OnDragStartListener mListener;
74 
75     private int mLastTouchX, mLastTouchY;
76     private boolean mDragging;
77 
78     /**
79      * Interface definition for a callback to be invoked when a drag start gesture is detected.
80      */
81     public interface OnDragStartListener {
82         /**
83          * Called when a drag start gesture has been detected.
84          *
85          * @param v The view over which the drag start gesture has been detected.
86          * @param helper The DragStartHelper object which detected the gesture.
87          * @return True if the listener has started the drag operation, false otherwise.
88          */
onDragStart(@onNull View v, @NonNull DragStartHelper helper)89         boolean onDragStart(@NonNull View v, @NonNull DragStartHelper helper);
90     }
91 
92     /**
93      * Create a DragStartHelper associated with the specified view.
94      * The newly created helper is not initially attached to the view, {@link #attach} must be
95      * called explicitly.
96      * @param view A View
97      * @param listener listener for the drag events.
98      */
DragStartHelper(@onNull View view, @NonNull OnDragStartListener listener)99     public DragStartHelper(@NonNull View view, @NonNull OnDragStartListener listener) {
100         mView = view;
101         mListener = listener;
102     }
103 
104     /**
105      * Attach the helper to the view.
106      * <p>
107      * This will replace previously existing touch and long click listeners.
108      */
attach()109     public void attach() {
110         mView.setOnLongClickListener(mLongClickListener);
111         mView.setOnTouchListener(mTouchListener);
112     }
113 
114     /**
115      * Detach the helper from the view.
116      * <p>
117      * This will reset touch and long click listeners to {@code null}.
118      */
detach()119     public void detach() {
120         mView.setOnLongClickListener(null);
121         mView.setOnTouchListener(null);
122     }
123 
124     /**
125      * Handle a touch event.
126      * @param v The view the touch event has been dispatched to.
127      * @param event The MotionEvent object containing full information about
128      *        the event.
129      * @return True if the listener has consumed the event, false otherwise.
130      */
onTouch(@onNull View v, @NonNull MotionEvent event)131     public boolean onTouch(@NonNull View v, @NonNull MotionEvent event) {
132         final int x = (int) event.getX();
133         final int y = (int) event.getY();
134         switch (event.getAction()) {
135             case MotionEvent.ACTION_DOWN:
136                 mLastTouchX = x;
137                 mLastTouchY = y;
138                 break;
139 
140             case MotionEvent.ACTION_MOVE:
141                 if (!MotionEventCompat.isFromSource(event, InputDeviceCompat.SOURCE_MOUSE)
142                         || (event.getButtonState()
143                                 & MotionEvent.BUTTON_PRIMARY) == 0) {
144                     break;
145                 }
146                 if (mDragging) {
147                     // Ignore ACTION_MOVE events once the drag operation is in progress.
148                     break;
149                 }
150                 if (mLastTouchX == x && mLastTouchY == y) {
151                     // Do not call the listener unless the pointer position has actually changed.
152                     break;
153                 }
154                 mLastTouchX = x;
155                 mLastTouchY = y;
156                 mDragging = mListener.onDragStart(v, this);
157                 return mDragging;
158 
159             case MotionEvent.ACTION_UP:
160             case MotionEvent.ACTION_CANCEL:
161                 mDragging = false;
162                 break;
163         }
164         return false;
165     }
166 
167     /**
168      * Handle a long click event.
169      * @param v The view that was clicked and held.
170      * @return true if the callback consumed the long click, false otherwise.
171      */
onLongClick(@onNull View v)172     public boolean onLongClick(@NonNull View v) {
173         if (mDragging) {
174             // Ignore long click once the drag operation is in progress.
175             return true;
176         }
177         mDragging = mListener.onDragStart(v, this);
178         return mDragging;
179     }
180 
181     /**
182      * Compute the position of the touch event that started the drag operation.
183      * @param point The position of the touch event that started the drag operation.
184      */
getTouchPosition(@onNull Point point)185     public void getTouchPosition(@NonNull Point point) {
186         point.set(mLastTouchX, mLastTouchY);
187     }
188 
189     private final View.OnLongClickListener mLongClickListener =
190             DragStartHelper.this::onLongClick;
191 
192     private final View.OnTouchListener mTouchListener = DragStartHelper.this::onTouch;
193 }
194 
195