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