Home | History | Annotate | Download | only in browse
      1 /*
      2  * Copyright (C) 2013 Google Inc.
      3  * Licensed to The Android Open Source Project.
      4  *
      5  * Licensed under the Apache License, Version 2.0 (the "License");
      6  * you may not use this file except in compliance with the License.
      7  * You may obtain a copy of the License at
      8  *
      9  *      http://www.apache.org/licenses/LICENSE-2.0
     10  *
     11  * Unless required by applicable law or agreed to in writing, software
     12  * distributed under the License is distributed on an "AS IS" BASIS,
     13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     14  * See the License for the specific language governing permissions and
     15  * limitations under the License.
     16  */
     17 
     18 package com.android.mail.browse;
     19 
     20 import android.content.Context;
     21 import android.util.AttributeSet;
     22 import android.view.GestureDetector;
     23 import android.view.MotionEvent;
     24 import android.view.ScaleGestureDetector;
     25 import android.view.ViewConfiguration;
     26 import android.widget.ScrollView;
     27 
     28 import com.android.mail.utils.LogUtils;
     29 
     30 import java.util.Set;
     31 import java.util.concurrent.CopyOnWriteArraySet;
     32 
     33 /**
     34  * A container that tries to play nice with an internally scrollable {@link Touchable} child view.
     35  * The assumption is that the child view can scroll horizontally, but not vertically, so any
     36  * touch events on that child view should ALSO be sent here so it can simultaneously vertically
     37  * scroll (not the standard either/or behavior).
     38  * <p>
     39  * Touch events on any other child of this ScrollView are intercepted in the standard fashion.
     40  */
     41 public class MessageScrollView extends ScrollView implements ScrollNotifier,
     42         ScaleGestureDetector.OnScaleGestureListener, GestureDetector.OnDoubleTapListener {
     43 
     44     /**
     45      * A View that reports whether onTouchEvent() was recently called.
     46      */
     47     public interface Touchable {
     48         boolean wasTouched();
     49         void clearTouched();
     50         boolean zoomIn();
     51         boolean zoomOut();
     52     }
     53 
     54     /**
     55      * True when performing "special" interception.
     56      */
     57     private boolean mWantToIntercept;
     58     /**
     59      * Whether to perform the standard touch interception procedure. This is set to true when we
     60      * want to intercept a touch stream from any child OTHER than {@link #mTouchableChild}.
     61      */
     62     private boolean mInterceptNormally;
     63     /**
     64      * The special child that we want to NOT intercept from in the normal way. Instead, this child
     65      * will continue to receive the touch event stream (so it can handle the horizontal component)
     66      * while this parent will additionally handle the events to perform vertical scrolling.
     67      */
     68     private Touchable mTouchableChild;
     69 
     70     /**
     71      * We want to detect the scale gesture so that we don't try to scroll instead, but we don't
     72      * care about actually interpreting it because the webview does that by itself when it handles
     73      * the touch events.
     74      *
     75      * This might lead to really weird interactions if the two gesture detectors' implementations
     76      * drift...
     77      */
     78     private ScaleGestureDetector mScaleDetector;
     79     private boolean mInScaleGesture;
     80 
     81     /**
     82      * We also want to detect double-tap gestures, but in a way that doesn't conflict with
     83      * tap-tap-drag gestures
     84      */
     85     private GestureDetector mGestureDetector;
     86     private boolean mDoubleTapOccurred;
     87     private boolean mZoomedIn;
     88 
     89     /**
     90      * Touch slop used to determine if this double tap is valid for starting a scale or should be
     91      * ignored.
     92      */
     93     private int mTouchSlopSquared;
     94 
     95     /**
     96      * X and Y coordinates for the current down event. Since mDoubleTapOccurred only contains the
     97      * information that there was a double tap event, use these to get the secondary tap
     98      * information to determine if a user has moved beyond touch slop.
     99      */
    100     private float mDownFocusX;
    101     private float mDownFocusY;
    102 
    103     private final Set<ScrollListener> mScrollListeners =
    104             new CopyOnWriteArraySet<ScrollListener>();
    105 
    106     public static final String LOG_TAG = "MsgScroller";
    107 
    108     public MessageScrollView(Context c) {
    109         this(c, null);
    110     }
    111 
    112     public MessageScrollView(Context c, AttributeSet attrs) {
    113         super(c, attrs);
    114         final int touchSlop = ViewConfiguration.get(c).getScaledTouchSlop();
    115         mTouchSlopSquared = touchSlop * touchSlop;
    116         mScaleDetector = new ScaleGestureDetector(c, this);
    117         mGestureDetector = new GestureDetector(c, new GestureDetector.SimpleOnGestureListener());
    118         mGestureDetector.setOnDoubleTapListener(this);
    119     }
    120 
    121     public void setInnerScrollableView(Touchable child) {
    122         mTouchableChild = child;
    123     }
    124 
    125     @Override
    126     public boolean onInterceptTouchEvent(MotionEvent ev) {
    127         if (mInterceptNormally) {
    128             LogUtils.d(LOG_TAG, "IN ScrollView.onIntercept, NOW stealing. ev=%s", ev);
    129             return true;
    130         } else if (mWantToIntercept) {
    131             LogUtils.d(LOG_TAG, "IN ScrollView.onIntercept, already stealing. ev=%s", ev);
    132             return false;
    133         }
    134 
    135         mWantToIntercept = super.onInterceptTouchEvent(ev);
    136         LogUtils.d(LOG_TAG, "OUT ScrollView.onIntercept, steal=%s ev=%s", mWantToIntercept, ev);
    137         return false;
    138     }
    139 
    140     @Override
    141     public boolean dispatchTouchEvent(MotionEvent ev) {
    142         final int action = ev.getActionMasked();
    143         switch (action) {
    144             case MotionEvent.ACTION_DOWN:
    145                 LogUtils.d(LOG_TAG, "IN ScrollView.dispatchTouch, clearing flags");
    146                 mWantToIntercept = false;
    147                 mInterceptNormally = false;
    148                 break;
    149         }
    150         if (mTouchableChild != null) {
    151             mTouchableChild.clearTouched();
    152         }
    153 
    154         mScaleDetector.onTouchEvent(ev);
    155         mGestureDetector.onTouchEvent(ev);
    156 
    157         final boolean handled = super.dispatchTouchEvent(ev);
    158         LogUtils.d(LOG_TAG, "OUT ScrollView.dispatchTouch, handled=%s ev=%s", handled, ev);
    159 
    160         if (mWantToIntercept && !mInScaleGesture) {
    161             final boolean touchedChild = (mTouchableChild != null && mTouchableChild.wasTouched());
    162             if (touchedChild) {
    163                 // also give the event to this scroll view if the WebView got the event
    164                 // and didn't stop any parent interception
    165                 LogUtils.d(LOG_TAG, "IN extra ScrollView.onTouch, ev=%s", ev);
    166                 onTouchEvent(ev);
    167             } else {
    168                 mInterceptNormally = true;
    169                 mWantToIntercept = false;
    170             }
    171         }
    172 
    173         return handled;
    174     }
    175 
    176     @Override
    177     public boolean onScale(ScaleGestureDetector detector) {
    178         return true;
    179     }
    180 
    181     @Override
    182     public boolean onScaleBegin(ScaleGestureDetector detector) {
    183         LogUtils.d(LOG_TAG, "Begin scale gesture");
    184         mInScaleGesture = true;
    185         return true;
    186     }
    187 
    188     @Override
    189     public void onScaleEnd(ScaleGestureDetector detector) {
    190         LogUtils.d(LOG_TAG, "End scale gesture");
    191         mInScaleGesture = false;
    192     }
    193 
    194     @Override
    195     public boolean onSingleTapConfirmed(MotionEvent e) {
    196         return false;
    197     }
    198 
    199     @Override
    200     public boolean onDoubleTap(MotionEvent e) {
    201         mDoubleTapOccurred = true;
    202         return false;
    203     }
    204 
    205     @Override
    206     public boolean onDoubleTapEvent(MotionEvent e) {
    207         final int action = e.getAction();
    208         boolean handled = false;
    209 
    210         switch (action) {
    211             case MotionEvent.ACTION_DOWN:
    212                 mDownFocusX = e.getX();
    213                 mDownFocusY = e.getY();
    214                 break;
    215             case MotionEvent.ACTION_UP:
    216                 handled = triggerZoom();
    217                 break;
    218             case MotionEvent.ACTION_MOVE:
    219                 final int deltaX = (int) (e.getX() - mDownFocusX);
    220                 final int deltaY = (int) (e.getY() - mDownFocusY);
    221                 int distance = (deltaX * deltaX) + (deltaY * deltaY);
    222                 if (distance > mTouchSlopSquared) {
    223                     mDoubleTapOccurred = false;
    224                 }
    225                 break;
    226 
    227         }
    228         return handled;
    229     }
    230 
    231     private boolean triggerZoom() {
    232         boolean handled = false;
    233         if (mDoubleTapOccurred) {
    234             if (mZoomedIn) {
    235                 mTouchableChild.zoomOut();
    236             } else {
    237                 mTouchableChild.zoomIn();
    238             }
    239             mZoomedIn = !mZoomedIn;
    240             LogUtils.d(LogUtils.TAG, "Trigger Zoom!");
    241             handled = true;
    242         }
    243         mDoubleTapOccurred = false;
    244         return handled;
    245     }
    246 
    247     @Override
    248     public void addScrollListener(ScrollListener l) {
    249         mScrollListeners.add(l);
    250     }
    251 
    252     @Override
    253     public void removeScrollListener(ScrollListener l) {
    254         mScrollListeners.remove(l);
    255     }
    256 
    257     @Override
    258     protected void onScrollChanged(int l, int t, int oldl, int oldt) {
    259         super.onScrollChanged(l, t, oldl, oldt);
    260         for (ScrollListener listener : mScrollListeners) {
    261             listener.onNotifierScroll(t);
    262         }
    263     }
    264 
    265     @Override
    266     public int computeVerticalScrollRange() {
    267         return super.computeVerticalScrollRange();
    268     }
    269 
    270     @Override
    271     public int computeVerticalScrollOffset() {
    272         return super.computeVerticalScrollOffset();
    273     }
    274 
    275     @Override
    276     public int computeVerticalScrollExtent() {
    277         return super.computeVerticalScrollExtent();
    278     }
    279 
    280     @Override
    281     public int computeHorizontalScrollRange() {
    282         return super.computeHorizontalScrollRange();
    283     }
    284 
    285     @Override
    286     public int computeHorizontalScrollOffset() {
    287         return super.computeHorizontalScrollOffset();
    288     }
    289 
    290     @Override
    291     public int computeHorizontalScrollExtent() {
    292         return super.computeHorizontalScrollExtent();
    293     }
    294 }
    295