1 /* 2 * Copyright (C) 2015 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 com.android.documentsui.base; 18 19 import static com.android.documentsui.base.Shared.DEBUG; 20 21 import android.graphics.Point; 22 import android.support.v7.widget.RecyclerView; 23 import android.util.Log; 24 import android.util.Pools; 25 import android.view.KeyEvent; 26 import android.view.MotionEvent; 27 import android.view.View; 28 29 import com.android.documentsui.dirlist.DocumentDetails; 30 import com.android.documentsui.dirlist.DocumentHolder; 31 32 import javax.annotation.Nullable; 33 34 /** 35 * Utility code for dealing with MotionEvents. 36 */ 37 public final class Events { 38 39 /** 40 * Returns true if event was triggered by a mouse. 41 */ 42 public static boolean isMouseEvent(MotionEvent e) { 43 int toolType = e.getToolType(0); 44 return toolType == MotionEvent.TOOL_TYPE_MOUSE; 45 } 46 47 /** 48 * Returns true if event was triggered by a finger or stylus touch. 49 */ 50 public static boolean isActionDown(MotionEvent e) { 51 return e.getActionMasked() == MotionEvent.ACTION_DOWN; 52 } 53 54 /** 55 * Returns true if event was triggered by a finger or stylus touch. 56 */ 57 public static boolean isActionUp(MotionEvent e) { 58 return e.getActionMasked() == MotionEvent.ACTION_UP; 59 } 60 61 /** 62 * Returns true if the shift is pressed. 63 */ 64 public boolean isShiftPressed(MotionEvent e) { 65 return hasShiftBit(e.getMetaState()); 66 } 67 68 /** 69 * Returns true if the event is a mouse drag event. 70 * @param e 71 * @return 72 */ 73 public static boolean isMouseDragEvent(InputEvent e) { 74 return e.isMouseEvent() 75 && e.isActionMove() 76 && e.isPrimaryButtonPressed() 77 && e.isOverDragHotspot(); 78 } 79 80 /** 81 * Whether or not the given keyCode represents a navigation keystroke (e.g. up, down, home). 82 * 83 * @param keyCode 84 * @return 85 */ 86 public static boolean isNavigationKeyCode(int keyCode) { 87 switch (keyCode) { 88 case KeyEvent.KEYCODE_DPAD_UP: 89 case KeyEvent.KEYCODE_DPAD_DOWN: 90 case KeyEvent.KEYCODE_DPAD_LEFT: 91 case KeyEvent.KEYCODE_DPAD_RIGHT: 92 case KeyEvent.KEYCODE_MOVE_HOME: 93 case KeyEvent.KEYCODE_MOVE_END: 94 case KeyEvent.KEYCODE_PAGE_UP: 95 case KeyEvent.KEYCODE_PAGE_DOWN: 96 return true; 97 default: 98 return false; 99 } 100 } 101 102 103 /** 104 * Returns true if the "SHIFT" bit is set. 105 */ 106 public static boolean hasShiftBit(int metaState) { 107 return (metaState & KeyEvent.META_SHIFT_ON) != 0; 108 } 109 110 public static boolean hasCtrlBit(int metaState) { 111 return (metaState & KeyEvent.META_CTRL_ON) != 0; 112 } 113 114 public static boolean hasAltBit(int metaState) { 115 return (metaState & KeyEvent.META_ALT_ON) != 0; 116 } 117 118 /** 119 * A facade over MotionEvent primarily designed to permit for unit testing 120 * of related code. 121 */ 122 public interface InputEvent extends AutoCloseable { 123 boolean isMouseEvent(); 124 boolean isPrimaryButtonPressed(); 125 boolean isSecondaryButtonPressed(); 126 boolean isTertiaryButtonPressed(); 127 boolean isAltKeyDown(); 128 boolean isShiftKeyDown(); 129 boolean isCtrlKeyDown(); 130 131 /** Returns true if the action is the initial press of a mouse or touch. */ 132 boolean isActionDown(); 133 134 /** Returns true if the action is the final release of a mouse or touch. */ 135 boolean isActionUp(); 136 137 /** 138 * Returns true when the action is the initial press of a non-primary (ex. second finger) 139 * pointer. 140 * See {@link MotionEvent#ACTION_POINTER_DOWN}. 141 */ 142 boolean isMultiPointerActionDown(); 143 144 /** 145 * Returns true when the action is the final of a non-primary (ex. second finger) 146 * pointer. 147 * * See {@link MotionEvent#ACTION_POINTER_UP}. 148 */ 149 boolean isMultiPointerActionUp(); 150 151 /** Returns true if the action is neither the initial nor the final release of a mouse 152 * or touch. */ 153 boolean isActionMove(); 154 155 /** Returns true if the action is cancel. */ 156 boolean isActionCancel(); 157 158 // Eliminate the checked Exception from Autoclosable. 159 @Override 160 public void close(); 161 162 Point getOrigin(); 163 float getX(); 164 float getY(); 165 float getRawX(); 166 float getRawY(); 167 int getPointerCount(); 168 169 /** Returns true if there is an item under the finger/cursor. */ 170 boolean isOverItem(); 171 172 /** 173 * Returns true if there is a model backed item under the finger/cursor. 174 * Resulting calls on the event instance should never return a null 175 * DocumentDetails and DocumentDetails#hasModelId should always return true 176 */ 177 boolean isOverModelItem(); 178 179 /** 180 * Returns true if the event is over an area that can be dragged via touch. 181 * List items have a white area that is not draggable. 182 */ 183 boolean isOverDragHotspot(); 184 185 /** 186 * Returns true if the event is a two/three-finger scroll on touchpad. 187 */ 188 boolean isTouchpadScroll(); 189 190 /** Returns the adapter position of the item under the finger/cursor. */ 191 int getItemPosition(); 192 193 boolean isOverDocIcon(); 194 195 /** Returns the DocumentDetails for the item under the event, or null. */ 196 @Nullable DocumentDetails getDocumentDetails(); 197 } 198 199 public static final class MotionInputEvent implements InputEvent { 200 private static final String TAG = "MotionInputEvent"; 201 202 private static final int UNSET_POSITION = RecyclerView.NO_POSITION - 1; 203 204 private static final Pools.SimplePool<MotionInputEvent> sPool = new Pools.SimplePool<>(1); 205 206 private MotionEvent mEvent; 207 private @Nullable RecyclerView mRecView; 208 209 private int mPosition = UNSET_POSITION; 210 private @Nullable DocumentDetails mDocDetails; 211 212 private MotionInputEvent() { 213 if (DEBUG) Log.i(TAG, "Created a new instance."); 214 } 215 216 public static MotionInputEvent obtain(MotionEvent event, RecyclerView view) { 217 Shared.checkMainLoop(); 218 219 MotionInputEvent instance = sPool.acquire(); 220 instance = (instance != null ? instance : new MotionInputEvent()); 221 222 instance.mEvent = event; 223 instance.mRecView = view; 224 225 return instance; 226 } 227 228 public void recycle() { 229 Shared.checkMainLoop(); 230 231 mEvent = null; 232 mRecView = null; 233 mPosition = UNSET_POSITION; 234 mDocDetails = null; 235 236 boolean released = sPool.release(this); 237 // This assert is used to guarantee we won't generate too many instances that can't be 238 // held in the pool, which indicates our pool size is too small. 239 // 240 // Right now one instance is enough because we expect all instances are only used in 241 // main thread. 242 assert(released); 243 } 244 245 @Override 246 public void close() { 247 recycle(); 248 } 249 250 @Override 251 public boolean isMouseEvent() { 252 return Events.isMouseEvent(mEvent); 253 } 254 255 @Override 256 public boolean isPrimaryButtonPressed() { 257 return mEvent.isButtonPressed(MotionEvent.BUTTON_PRIMARY); 258 } 259 260 @Override 261 public boolean isSecondaryButtonPressed() { 262 return mEvent.isButtonPressed(MotionEvent.BUTTON_SECONDARY); 263 } 264 265 @Override 266 public boolean isTertiaryButtonPressed() { 267 return mEvent.isButtonPressed(MotionEvent.BUTTON_TERTIARY); 268 } 269 270 @Override 271 public boolean isAltKeyDown() { 272 return Events.hasAltBit(mEvent.getMetaState()); 273 } 274 275 @Override 276 public boolean isShiftKeyDown() { 277 return Events.hasShiftBit(mEvent.getMetaState()); 278 } 279 280 @Override 281 public boolean isCtrlKeyDown() { 282 return Events.hasCtrlBit(mEvent.getMetaState()); 283 } 284 285 @Override 286 public boolean isActionDown() { 287 return mEvent.getActionMasked() == MotionEvent.ACTION_DOWN; 288 } 289 290 @Override 291 public boolean isActionUp() { 292 return mEvent.getActionMasked() == MotionEvent.ACTION_UP; 293 } 294 295 @Override 296 public boolean isMultiPointerActionDown() { 297 return mEvent.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN; 298 } 299 300 @Override 301 public boolean isMultiPointerActionUp() { 302 return mEvent.getActionMasked() == MotionEvent.ACTION_POINTER_UP; 303 } 304 305 306 @Override 307 public boolean isActionMove() { 308 return mEvent.getActionMasked() == MotionEvent.ACTION_MOVE; 309 } 310 311 @Override 312 public boolean isActionCancel() { 313 return mEvent.getActionMasked() == MotionEvent.ACTION_CANCEL; 314 } 315 316 @Override 317 public Point getOrigin() { 318 return new Point((int) mEvent.getX(), (int) mEvent.getY()); 319 } 320 321 @Override 322 public float getX() { 323 return mEvent.getX(); 324 } 325 326 @Override 327 public float getY() { 328 return mEvent.getY(); 329 } 330 331 @Override 332 public float getRawX() { 333 return mEvent.getRawX(); 334 } 335 336 @Override 337 public float getRawY() { 338 return mEvent.getRawY(); 339 } 340 341 @Override 342 public int getPointerCount() { 343 return mEvent.getPointerCount(); 344 } 345 346 @Override 347 public boolean isTouchpadScroll() { 348 // Touchpad inputs are treated as mouse inputs, and when scrolling, there are no buttons 349 // returned. 350 return isMouseEvent() && isActionMove() && mEvent.getButtonState() == 0; 351 } 352 353 @Override 354 public boolean isOverDragHotspot() { 355 return isOverItem() && getDocumentDetails().isInDragHotspot(this); 356 } 357 358 @Override 359 public boolean isOverItem() { 360 return getItemPosition() != RecyclerView.NO_POSITION; 361 } 362 363 @Override 364 public boolean isOverDocIcon() { 365 return isOverItem() && getDocumentDetails().isOverDocIcon(this); 366 } 367 368 @Override 369 public boolean isOverModelItem() { 370 return isOverItem() && getDocumentDetails().hasModelId(); 371 } 372 373 @Override 374 public int getItemPosition() { 375 if (mPosition == UNSET_POSITION) { 376 View child = mRecView.findChildViewUnder(mEvent.getX(), mEvent.getY()); 377 mPosition = (child != null) 378 ? mRecView.getChildAdapterPosition(child) 379 : RecyclerView.NO_POSITION; 380 } 381 return mPosition; 382 } 383 384 @Override 385 public @Nullable DocumentDetails getDocumentDetails() { 386 if (mDocDetails == null) { 387 View childView = mRecView.findChildViewUnder(mEvent.getX(), mEvent.getY()); 388 mDocDetails = (childView != null) 389 ? (DocumentHolder) mRecView.getChildViewHolder(childView) 390 : null; 391 } 392 if (isOverItem()) { 393 assert(mDocDetails != null); 394 } 395 return mDocDetails; 396 } 397 398 @Override 399 public String toString() { 400 return new StringBuilder() 401 .append("MotionInputEvent {") 402 .append("isMouseEvent=").append(isMouseEvent()) 403 .append(" isPrimaryButtonPressed=").append(isPrimaryButtonPressed()) 404 .append(" isSecondaryButtonPressed=").append(isSecondaryButtonPressed()) 405 .append(" isShiftKeyDown=").append(isShiftKeyDown()) 406 .append(" isAltKeyDown=").append(isAltKeyDown()) 407 .append(" action(decoded)=").append( 408 MotionEvent.actionToString(mEvent.getActionMasked())) 409 .append(" getOrigin=").append(getOrigin()) 410 .append(" isOverItem=").append(isOverItem()) 411 .append(" getItemPosition=").append(getItemPosition()) 412 .append(" getDocumentDetails=").append(getDocumentDetails()) 413 .append(" getPointerCount=").append(getPointerCount()) 414 .append("}") 415 .toString(); 416 } 417 } 418 } 419