MouseInputHandler.java revision ac5fe7c617c66850fff75a9fce9979c6e5674b0f
1/* 2 * Copyright 2017 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 17package androidx.recyclerview.selection; 18 19import static androidx.core.util.Preconditions.checkArgument; 20import static androidx.core.util.Preconditions.checkState; 21 22import static androidx.recyclerview.selection.Shared.DEBUG; 23import static androidx.recyclerview.selection.Shared.VERBOSE; 24 25import androidx.annotation.NonNull; 26import androidx.annotation.Nullable; 27import androidx.recyclerview.widget.RecyclerView; 28import android.util.Log; 29import android.view.MotionEvent; 30 31import androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails; 32 33/** 34 * A MotionInputHandler that provides the high-level glue for mouse driven selection. This 35 * class works with {@link RecyclerView}, {@link GestureRouter}, and {@link GestureSelectionHelper} 36 * to implement the primary policies around mouse input. 37 */ 38final class MouseInputHandler<K> extends MotionInputHandler<K> { 39 40 private static final String TAG = "MouseInputDelegate"; 41 42 private final ItemDetailsLookup<K> mDetailsLookup; 43 private final OnContextClickListener mOnContextClickListener; 44 private final OnItemActivatedListener<K> mOnItemActivatedListener; 45 private final FocusDelegate<K> mFocusDelegate; 46 47 // The event has been handled in onSingleTapUp 48 private boolean mHandledTapUp; 49 // true when the previous event has consumed a right click motion event 50 private boolean mHandledOnDown; 51 52 MouseInputHandler( 53 @NonNull SelectionTracker<K> selectionTracker, 54 @NonNull ItemKeyProvider<K> keyProvider, 55 @NonNull ItemDetailsLookup<K> detailsLookup, 56 @NonNull OnContextClickListener onContextClickListener, 57 @NonNull OnItemActivatedListener<K> onItemActivatedListener, 58 @NonNull FocusDelegate<K> focusDelegate) { 59 60 super(selectionTracker, keyProvider, focusDelegate); 61 62 checkArgument(detailsLookup != null); 63 checkArgument(onContextClickListener != null); 64 checkArgument(onItemActivatedListener != null); 65 66 mDetailsLookup = detailsLookup; 67 mOnContextClickListener = onContextClickListener; 68 mOnItemActivatedListener = onItemActivatedListener; 69 mFocusDelegate = focusDelegate; 70 } 71 72 @Override 73 public boolean onDown(@NonNull MotionEvent e) { 74 if (VERBOSE) Log.v(TAG, "Delegated onDown event."); 75 if ((MotionEvents.isAltKeyPressed(e) && MotionEvents.isPrimaryMouseButtonPressed(e)) 76 || MotionEvents.isSecondaryMouseButtonPressed(e)) { 77 mHandledOnDown = true; 78 return onRightClick(e); 79 } 80 81 return false; 82 } 83 84 @Override 85 public boolean onScroll(@NonNull MotionEvent e1, @NonNull MotionEvent e2, 86 float distanceX, float distanceY) { 87 // Don't scroll content window in response to mouse drag 88 // If it's two-finger trackpad scrolling, we want to scroll 89 return !MotionEvents.isTouchpadScroll(e2); 90 } 91 92 @Override 93 public boolean onSingleTapUp(@NonNull MotionEvent e) { 94 // See b/27377794. Since we don't get a button state back from UP events, we have to 95 // explicitly save this state to know whether something was previously handled by 96 // DOWN events or not. 97 if (mHandledOnDown) { 98 if (VERBOSE) Log.v(TAG, "Ignoring onSingleTapUp, previously handled in onDown."); 99 mHandledOnDown = false; 100 return false; 101 } 102 103 if (!mDetailsLookup.overItemWithSelectionKey(e)) { 104 if (DEBUG) Log.d(TAG, "Tap not associated w/ model item. Clearing selection."); 105 mSelectionTracker.clearSelection(); 106 mFocusDelegate.clearFocus(); 107 return false; 108 } 109 110 if (MotionEvents.isTertiaryMouseButtonPressed(e)) { 111 if (DEBUG) Log.d(TAG, "Ignoring middle click"); 112 return false; 113 } 114 115 if (mSelectionTracker.hasSelection()) { 116 onItemClick(e, mDetailsLookup.getItemDetails(e)); 117 mHandledTapUp = true; 118 return true; 119 } 120 121 return false; 122 } 123 124 // tap on an item when there is an existing selection. We could extend 125 // a selection, we could clear selection (then launch) 126 private void onItemClick(@NonNull MotionEvent e, @NonNull ItemDetails<K> item) { 127 checkState(mSelectionTracker.hasSelection()); 128 checkArgument(item != null); 129 130 if (isRangeExtension(e)) { 131 extendSelectionRange(item); 132 } else { 133 if (shouldClearSelection(e, item)) { 134 mSelectionTracker.clearSelection(); 135 } 136 if (mSelectionTracker.isSelected(item.getSelectionKey())) { 137 if (mSelectionTracker.deselect(item.getSelectionKey())) { 138 mFocusDelegate.clearFocus(); 139 } 140 } else { 141 selectOrFocusItem(item, e); 142 } 143 } 144 } 145 146 @Override 147 public boolean onSingleTapConfirmed(@NonNull MotionEvent e) { 148 if (mHandledTapUp) { 149 if (VERBOSE) { 150 Log.v(TAG, 151 "Ignoring onSingleTapConfirmed, previously handled in onSingleTapUp."); 152 } 153 mHandledTapUp = false; 154 return false; 155 } 156 157 if (mSelectionTracker.hasSelection()) { 158 return false; // should have been handled by onSingleTapUp. 159 } 160 161 if (!mDetailsLookup.overItem(e)) { 162 if (DEBUG) Log.d(TAG, "Ignoring Confirmed Tap on non-item."); 163 return false; 164 } 165 166 if (MotionEvents.isTertiaryMouseButtonPressed(e)) { 167 if (DEBUG) Log.d(TAG, "Ignoring middle click"); 168 return false; 169 } 170 171 @Nullable ItemDetails<K> item = mDetailsLookup.getItemDetails(e); 172 if (item == null || !item.hasSelectionKey()) { 173 return false; 174 } 175 176 if (mFocusDelegate.hasFocusedItem() && MotionEvents.isShiftKeyPressed(e)) { 177 mSelectionTracker.startRange(mFocusDelegate.getFocusedPosition()); 178 mSelectionTracker.extendRange(item.getPosition()); 179 } else { 180 selectOrFocusItem(item, e); 181 } 182 return true; 183 } 184 185 @Override 186 public boolean onDoubleTap(@NonNull MotionEvent e) { 187 mHandledTapUp = false; 188 189 if (!mDetailsLookup.overItemWithSelectionKey(e)) { 190 if (DEBUG) Log.d(TAG, "Ignoring DoubleTap on non-model-backed item."); 191 return false; 192 } 193 194 if (MotionEvents.isTertiaryMouseButtonPressed(e)) { 195 if (DEBUG) Log.d(TAG, "Ignoring middle click"); 196 return false; 197 } 198 199 ItemDetails<K> item = mDetailsLookup.getItemDetails(e); 200 return (item != null) && mOnItemActivatedListener.onItemActivated(item, e); 201 } 202 203 private boolean onRightClick(@NonNull MotionEvent e) { 204 if (mDetailsLookup.overItemWithSelectionKey(e)) { 205 @Nullable ItemDetails<K> item = mDetailsLookup.getItemDetails(e); 206 if (item != null && !mSelectionTracker.isSelected(item.getSelectionKey())) { 207 mSelectionTracker.clearSelection(); 208 selectItem(item); 209 } 210 } 211 212 // We always delegate final handling of the event, 213 // since the handler might want to show a context menu 214 // in an empty area or some other weirdo view. 215 return mOnContextClickListener.onContextClick(e); 216 } 217 218 private void selectOrFocusItem(@NonNull ItemDetails<K> item, @NonNull MotionEvent e) { 219 if (item.inSelectionHotspot(e) || MotionEvents.isCtrlKeyPressed(e)) { 220 selectItem(item); 221 } else { 222 focusItem(item); 223 } 224 } 225} 226