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