Home | History | Annotate | Download | only in base
      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