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 */ isMouseEvent(MotionEvent e)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 */ isActionDown(MotionEvent e)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 */ isActionUp(MotionEvent e)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 */ isShiftPressed(MotionEvent e)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 */ isMouseDragEvent(InputEvent e)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 */ isNavigationKeyCode(int keyCode)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 */ hasShiftBit(int metaState)106 public static boolean hasShiftBit(int metaState) { 107 return (metaState & KeyEvent.META_SHIFT_ON) != 0; 108 } 109 hasCtrlBit(int metaState)110 public static boolean hasCtrlBit(int metaState) { 111 return (metaState & KeyEvent.META_CTRL_ON) != 0; 112 } 113 hasAltBit(int metaState)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 { isMouseEvent()123 boolean isMouseEvent(); isPrimaryButtonPressed()124 boolean isPrimaryButtonPressed(); isSecondaryButtonPressed()125 boolean isSecondaryButtonPressed(); isTertiaryButtonPressed()126 boolean isTertiaryButtonPressed(); isAltKeyDown()127 boolean isAltKeyDown(); isShiftKeyDown()128 boolean isShiftKeyDown(); isCtrlKeyDown()129 boolean isCtrlKeyDown(); 130 131 /** Returns true if the action is the initial press of a mouse or touch. */ isActionDown()132 boolean isActionDown(); 133 134 /** Returns true if the action is the final release of a mouse or touch. */ isActionUp()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 */ isMultiPointerActionDown()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 */ isMultiPointerActionUp()149 boolean isMultiPointerActionUp(); 150 151 /** Returns true if the action is neither the initial nor the final release of a mouse 152 * or touch. */ isActionMove()153 boolean isActionMove(); 154 155 /** Returns true if the action is cancel. */ isActionCancel()156 boolean isActionCancel(); 157 158 // Eliminate the checked Exception from Autoclosable. 159 @Override close()160 public void close(); 161 getOrigin()162 Point getOrigin(); getX()163 float getX(); getY()164 float getY(); getRawX()165 float getRawX(); getRawY()166 float getRawY(); getPointerCount()167 int getPointerCount(); 168 169 /** Returns true if there is an item under the finger/cursor. */ isOverItem()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 */ isOverModelItem()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 */ isOverDragHotspot()183 boolean isOverDragHotspot(); 184 185 /** 186 * Returns true if the event is a two/three-finger scroll on touchpad. 187 */ isTouchpadScroll()188 boolean isTouchpadScroll(); 189 190 /** Returns the adapter position of the item under the finger/cursor. */ getItemPosition()191 int getItemPosition(); 192 193 /** Returns the DocumentDetails for the item under the event, or null. */ getDocumentDetails()194 @Nullable DocumentDetails getDocumentDetails(); 195 } 196 197 public static final class MotionInputEvent implements InputEvent { 198 private static final String TAG = "MotionInputEvent"; 199 200 private static final int UNSET_POSITION = RecyclerView.NO_POSITION - 1; 201 202 private static final Pools.SimplePool<MotionInputEvent> sPool = new Pools.SimplePool<>(1); 203 204 private MotionEvent mEvent; 205 private @Nullable RecyclerView mRecView; 206 207 private int mPosition = UNSET_POSITION; 208 private @Nullable DocumentDetails mDocDetails; 209 MotionInputEvent()210 private MotionInputEvent() { 211 if (DEBUG) Log.i(TAG, "Created a new instance."); 212 } 213 obtain(MotionEvent event, RecyclerView view)214 public static MotionInputEvent obtain(MotionEvent event, RecyclerView view) { 215 Shared.checkMainLoop(); 216 217 MotionInputEvent instance = sPool.acquire(); 218 instance = (instance != null ? instance : new MotionInputEvent()); 219 220 instance.mEvent = event; 221 instance.mRecView = view; 222 223 return instance; 224 } 225 recycle()226 public void recycle() { 227 Shared.checkMainLoop(); 228 229 mEvent = null; 230 mRecView = null; 231 mPosition = UNSET_POSITION; 232 mDocDetails = null; 233 234 boolean released = sPool.release(this); 235 // This assert is used to guarantee we won't generate too many instances that can't be 236 // held in the pool, which indicates our pool size is too small. 237 // 238 // Right now one instance is enough because we expect all instances are only used in 239 // main thread. 240 assert(released); 241 } 242 243 @Override close()244 public void close() { 245 recycle(); 246 } 247 248 @Override isMouseEvent()249 public boolean isMouseEvent() { 250 return Events.isMouseEvent(mEvent); 251 } 252 253 @Override isPrimaryButtonPressed()254 public boolean isPrimaryButtonPressed() { 255 return mEvent.isButtonPressed(MotionEvent.BUTTON_PRIMARY); 256 } 257 258 @Override isSecondaryButtonPressed()259 public boolean isSecondaryButtonPressed() { 260 return mEvent.isButtonPressed(MotionEvent.BUTTON_SECONDARY); 261 } 262 263 @Override isTertiaryButtonPressed()264 public boolean isTertiaryButtonPressed() { 265 return mEvent.isButtonPressed(MotionEvent.BUTTON_TERTIARY); 266 } 267 268 @Override isAltKeyDown()269 public boolean isAltKeyDown() { 270 return Events.hasAltBit(mEvent.getMetaState()); 271 } 272 273 @Override isShiftKeyDown()274 public boolean isShiftKeyDown() { 275 return Events.hasShiftBit(mEvent.getMetaState()); 276 } 277 278 @Override isCtrlKeyDown()279 public boolean isCtrlKeyDown() { 280 return Events.hasCtrlBit(mEvent.getMetaState()); 281 } 282 283 @Override isActionDown()284 public boolean isActionDown() { 285 return mEvent.getActionMasked() == MotionEvent.ACTION_DOWN; 286 } 287 288 @Override isActionUp()289 public boolean isActionUp() { 290 return mEvent.getActionMasked() == MotionEvent.ACTION_UP; 291 } 292 293 @Override isMultiPointerActionDown()294 public boolean isMultiPointerActionDown() { 295 return mEvent.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN; 296 } 297 298 @Override isMultiPointerActionUp()299 public boolean isMultiPointerActionUp() { 300 return mEvent.getActionMasked() == MotionEvent.ACTION_POINTER_UP; 301 } 302 303 304 @Override isActionMove()305 public boolean isActionMove() { 306 return mEvent.getActionMasked() == MotionEvent.ACTION_MOVE; 307 } 308 309 @Override isActionCancel()310 public boolean isActionCancel() { 311 return mEvent.getActionMasked() == MotionEvent.ACTION_CANCEL; 312 } 313 314 @Override getOrigin()315 public Point getOrigin() { 316 return new Point((int) mEvent.getX(), (int) mEvent.getY()); 317 } 318 319 @Override getX()320 public float getX() { 321 return mEvent.getX(); 322 } 323 324 @Override getY()325 public float getY() { 326 return mEvent.getY(); 327 } 328 329 @Override getRawX()330 public float getRawX() { 331 return mEvent.getRawX(); 332 } 333 334 @Override getRawY()335 public float getRawY() { 336 return mEvent.getRawY(); 337 } 338 339 @Override getPointerCount()340 public int getPointerCount() { 341 return mEvent.getPointerCount(); 342 } 343 344 @Override isTouchpadScroll()345 public boolean isTouchpadScroll() { 346 // Touchpad inputs are treated as mouse inputs, and when scrolling, there are no buttons 347 // returned. 348 return isMouseEvent() && isActionMove() && mEvent.getButtonState() == 0; 349 } 350 351 @Override isOverDragHotspot()352 public boolean isOverDragHotspot() { 353 return isOverItem() ? getDocumentDetails().isInDragHotspot(this) : false; 354 } 355 356 @Override isOverItem()357 public boolean isOverItem() { 358 return getItemPosition() != RecyclerView.NO_POSITION; 359 } 360 361 @Override isOverModelItem()362 public boolean isOverModelItem() { 363 return isOverItem() && getDocumentDetails().hasModelId(); 364 } 365 366 @Override getItemPosition()367 public int getItemPosition() { 368 if (mPosition == UNSET_POSITION) { 369 View child = mRecView.findChildViewUnder(mEvent.getX(), mEvent.getY()); 370 mPosition = (child != null) 371 ? mRecView.getChildAdapterPosition(child) 372 : RecyclerView.NO_POSITION; 373 } 374 return mPosition; 375 } 376 377 @Override getDocumentDetails()378 public @Nullable DocumentDetails getDocumentDetails() { 379 if (mDocDetails == null) { 380 View childView = mRecView.findChildViewUnder(mEvent.getX(), mEvent.getY()); 381 mDocDetails = (childView != null) 382 ? (DocumentHolder) mRecView.getChildViewHolder(childView) 383 : null; 384 } 385 if (isOverItem()) { 386 assert(mDocDetails != null); 387 } 388 return mDocDetails; 389 } 390 391 @Override toString()392 public String toString() { 393 return new StringBuilder() 394 .append("MotionInputEvent {") 395 .append("isMouseEvent=").append(isMouseEvent()) 396 .append(" isPrimaryButtonPressed=").append(isPrimaryButtonPressed()) 397 .append(" isSecondaryButtonPressed=").append(isSecondaryButtonPressed()) 398 .append(" isShiftKeyDown=").append(isShiftKeyDown()) 399 .append(" isAltKeyDown=").append(isAltKeyDown()) 400 .append(" action(decoded)=").append( 401 MotionEvent.actionToString(mEvent.getActionMasked())) 402 .append(" getOrigin=").append(getOrigin()) 403 .append(" isOverItem=").append(isOverItem()) 404 .append(" getItemPosition=").append(getItemPosition()) 405 .append(" getDocumentDetails=").append(getDocumentDetails()) 406 .append(" getPointerCount=").append(getPointerCount()) 407 .append("}") 408 .toString(); 409 } 410 } 411 } 412