Home | History | Annotate | Download | only in widget
      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 android.widget;
     18 
     19 import android.os.SystemClock;
     20 import android.view.MotionEvent;
     21 import android.view.View;
     22 import android.view.ViewConfiguration;
     23 import android.view.ViewParent;
     24 
     25 import com.android.internal.view.menu.ShowableListMenu;
     26 
     27 /**
     28  * Abstract class that forwards touch events to a {@link ShowableListMenu}.
     29  *
     30  * @hide
     31  */
     32 public abstract class ForwardingListener
     33         implements View.OnTouchListener, View.OnAttachStateChangeListener {
     34 
     35     /** Scaled touch slop, used for detecting movement outside bounds. */
     36     private final float mScaledTouchSlop;
     37 
     38     /** Timeout before disallowing intercept on the source's parent. */
     39     private final int mTapTimeout;
     40 
     41     /** Timeout before accepting a long-press to start forwarding. */
     42     private final int mLongPressTimeout;
     43 
     44     /** Source view from which events are forwarded. */
     45     private final View mSrc;
     46 
     47     /** Runnable used to prevent conflicts with scrolling parents. */
     48     private Runnable mDisallowIntercept;
     49 
     50     /** Runnable used to trigger forwarding on long-press. */
     51     private Runnable mTriggerLongPress;
     52 
     53     /** Whether this listener is currently forwarding touch events. */
     54     private boolean mForwarding;
     55 
     56     /** The id of the first pointer down in the current event stream. */
     57     private int mActivePointerId;
     58 
     59     public ForwardingListener(View src) {
     60         mSrc = src;
     61         src.setLongClickable(true);
     62         src.addOnAttachStateChangeListener(this);
     63 
     64         mScaledTouchSlop = ViewConfiguration.get(src.getContext()).getScaledTouchSlop();
     65         mTapTimeout = ViewConfiguration.getTapTimeout();
     66 
     67         // Use a medium-press timeout. Halfway between tap and long-press.
     68         mLongPressTimeout = (mTapTimeout + ViewConfiguration.getLongPressTimeout()) / 2;
     69     }
     70 
     71     /**
     72      * Returns the popup to which this listener is forwarding events.
     73      * <p>
     74      * Override this to return the correct popup. If the popup is displayed
     75      * asynchronously, you may also need to override
     76      * {@link #onForwardingStopped} to prevent premature cancellation of
     77      * forwarding.
     78      *
     79      * @return the popup to which this listener is forwarding events
     80      */
     81     public abstract ShowableListMenu getPopup();
     82 
     83     @Override
     84     public boolean onTouch(View v, MotionEvent event) {
     85         final boolean wasForwarding = mForwarding;
     86         final boolean forwarding;
     87         if (wasForwarding) {
     88             forwarding = onTouchForwarded(event) || !onForwardingStopped();
     89         } else {
     90             forwarding = onTouchObserved(event) && onForwardingStarted();
     91 
     92             if (forwarding) {
     93                 // Make sure we cancel any ongoing source event stream.
     94                 final long now = SystemClock.uptimeMillis();
     95                 final MotionEvent e = MotionEvent.obtain(now, now, MotionEvent.ACTION_CANCEL,
     96                         0.0f, 0.0f, 0);
     97                 mSrc.onTouchEvent(e);
     98                 e.recycle();
     99             }
    100         }
    101 
    102         mForwarding = forwarding;
    103         return forwarding || wasForwarding;
    104     }
    105 
    106     @Override
    107     public void onViewAttachedToWindow(View v) {
    108     }
    109 
    110     @Override
    111     public void onViewDetachedFromWindow(View v) {
    112         mForwarding = false;
    113         mActivePointerId = MotionEvent.INVALID_POINTER_ID;
    114 
    115         if (mDisallowIntercept != null) {
    116             mSrc.removeCallbacks(mDisallowIntercept);
    117         }
    118     }
    119 
    120     /**
    121      * Called when forwarding would like to start.
    122      * <p>
    123      * By default, this will show the popup returned by {@link #getPopup()}.
    124      * It may be overridden to perform another action, like clicking the
    125      * source view or preparing the popup before showing it.
    126      *
    127      * @return true to start forwarding, false otherwise
    128      */
    129     protected boolean onForwardingStarted() {
    130         final ShowableListMenu popup = getPopup();
    131         if (popup != null && !popup.isShowing()) {
    132             popup.show();
    133         }
    134         return true;
    135     }
    136 
    137     /**
    138      * Called when forwarding would like to stop.
    139      * <p>
    140      * By default, this will dismiss the popup returned by
    141      * {@link #getPopup()}. It may be overridden to perform some other
    142      * action.
    143      *
    144      * @return true to stop forwarding, false otherwise
    145      */
    146     protected boolean onForwardingStopped() {
    147         final ShowableListMenu popup = getPopup();
    148         if (popup != null && popup.isShowing()) {
    149             popup.dismiss();
    150         }
    151         return true;
    152     }
    153 
    154     /**
    155      * Observes motion events and determines when to start forwarding.
    156      *
    157      * @param srcEvent motion event in source view coordinates
    158      * @return true to start forwarding motion events, false otherwise
    159      */
    160     private boolean onTouchObserved(MotionEvent srcEvent) {
    161         final View src = mSrc;
    162         if (!src.isEnabled()) {
    163             return false;
    164         }
    165 
    166         final int actionMasked = srcEvent.getActionMasked();
    167         switch (actionMasked) {
    168             case MotionEvent.ACTION_DOWN:
    169                 mActivePointerId = srcEvent.getPointerId(0);
    170 
    171                 if (mDisallowIntercept == null) {
    172                     mDisallowIntercept = new DisallowIntercept();
    173                 }
    174                 src.postDelayed(mDisallowIntercept, mTapTimeout);
    175 
    176                 if (mTriggerLongPress == null) {
    177                     mTriggerLongPress = new TriggerLongPress();
    178                 }
    179                 src.postDelayed(mTriggerLongPress, mLongPressTimeout);
    180                 break;
    181             case MotionEvent.ACTION_MOVE:
    182                 final int activePointerIndex = srcEvent.findPointerIndex(mActivePointerId);
    183                 if (activePointerIndex >= 0) {
    184                     final float x = srcEvent.getX(activePointerIndex);
    185                     final float y = srcEvent.getY(activePointerIndex);
    186 
    187                     // Has the pointer moved outside of the view?
    188                     if (!src.pointInView(x, y, mScaledTouchSlop)) {
    189                         clearCallbacks();
    190 
    191                         // Don't let the parent intercept our events.
    192                         src.getParent().requestDisallowInterceptTouchEvent(true);
    193                         return true;
    194                     }
    195                 }
    196                 break;
    197             case MotionEvent.ACTION_CANCEL:
    198             case MotionEvent.ACTION_UP:
    199                 clearCallbacks();
    200                 break;
    201         }
    202 
    203         return false;
    204     }
    205 
    206     private void clearCallbacks() {
    207         if (mTriggerLongPress != null) {
    208             mSrc.removeCallbacks(mTriggerLongPress);
    209         }
    210 
    211         if (mDisallowIntercept != null) {
    212             mSrc.removeCallbacks(mDisallowIntercept);
    213         }
    214     }
    215 
    216     private void onLongPress() {
    217         clearCallbacks();
    218 
    219         final View src = mSrc;
    220         if (!src.isEnabled() || src.isLongClickable()) {
    221             // Ignore long-press if the view is disabled or has its own
    222             // handler.
    223             return;
    224         }
    225 
    226         if (!onForwardingStarted()) {
    227             return;
    228         }
    229 
    230         // Don't let the parent intercept our events.
    231         src.getParent().requestDisallowInterceptTouchEvent(true);
    232 
    233         // Make sure we cancel any ongoing source event stream.
    234         final long now = SystemClock.uptimeMillis();
    235         final MotionEvent e = MotionEvent.obtain(now, now, MotionEvent.ACTION_CANCEL, 0, 0, 0);
    236         src.onTouchEvent(e);
    237         e.recycle();
    238 
    239         mForwarding = true;
    240     }
    241 
    242     /**
    243      * Handles forwarded motion events and determines when to stop
    244      * forwarding.
    245      *
    246      * @param srcEvent motion event in source view coordinates
    247      * @return true to continue forwarding motion events, false to cancel
    248      */
    249     private boolean onTouchForwarded(MotionEvent srcEvent) {
    250         final View src = mSrc;
    251         final ShowableListMenu popup = getPopup();
    252         if (popup == null || !popup.isShowing()) {
    253             return false;
    254         }
    255 
    256         final DropDownListView dst = (DropDownListView) popup.getListView();
    257         if (dst == null || !dst.isShown()) {
    258             return false;
    259         }
    260 
    261         // Convert event to destination-local coordinates.
    262         final MotionEvent dstEvent = MotionEvent.obtainNoHistory(srcEvent);
    263         src.toGlobalMotionEvent(dstEvent);
    264         dst.toLocalMotionEvent(dstEvent);
    265 
    266         // Forward converted event to destination view, then recycle it.
    267         final boolean handled = dst.onForwardedEvent(dstEvent, mActivePointerId);
    268         dstEvent.recycle();
    269 
    270         // Always cancel forwarding when the touch stream ends.
    271         final int action = srcEvent.getActionMasked();
    272         final boolean keepForwarding = action != MotionEvent.ACTION_UP
    273                 && action != MotionEvent.ACTION_CANCEL;
    274 
    275         return handled && keepForwarding;
    276     }
    277 
    278     private class DisallowIntercept implements Runnable {
    279         @Override
    280         public void run() {
    281             final ViewParent parent = mSrc.getParent();
    282             if (parent != null) {
    283                 parent.requestDisallowInterceptTouchEvent(true);
    284             }
    285         }
    286     }
    287 
    288     private class TriggerLongPress implements Runnable {
    289         @Override
    290         public void run() {
    291             onLongPress();
    292         }
    293     }
    294 }
    295