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