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 {
wasTouched()48         boolean wasTouched();
clearTouched()49         void clearTouched();
zoomIn()50         boolean zoomIn();
zoomOut()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 
MessageScrollView(Context c)108     public MessageScrollView(Context c) {
109         this(c, null);
110     }
111 
MessageScrollView(Context c, AttributeSet attrs)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 
setInnerScrollableView(Touchable child)121     public void setInnerScrollableView(Touchable child) {
122         mTouchableChild = child;
123     }
124 
125     @Override
onInterceptTouchEvent(MotionEvent ev)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
dispatchTouchEvent(MotionEvent ev)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
onScale(ScaleGestureDetector detector)177     public boolean onScale(ScaleGestureDetector detector) {
178         return true;
179     }
180 
181     @Override
onScaleBegin(ScaleGestureDetector detector)182     public boolean onScaleBegin(ScaleGestureDetector detector) {
183         LogUtils.d(LOG_TAG, "Begin scale gesture");
184         mInScaleGesture = true;
185         return true;
186     }
187 
188     @Override
onScaleEnd(ScaleGestureDetector detector)189     public void onScaleEnd(ScaleGestureDetector detector) {
190         LogUtils.d(LOG_TAG, "End scale gesture");
191         mInScaleGesture = false;
192     }
193 
194     @Override
onSingleTapConfirmed(MotionEvent e)195     public boolean onSingleTapConfirmed(MotionEvent e) {
196         return false;
197     }
198 
199     @Override
onDoubleTap(MotionEvent e)200     public boolean onDoubleTap(MotionEvent e) {
201         mDoubleTapOccurred = true;
202         return false;
203     }
204 
205     @Override
onDoubleTapEvent(MotionEvent e)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 
triggerZoom()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
addScrollListener(ScrollListener l)248     public void addScrollListener(ScrollListener l) {
249         mScrollListeners.add(l);
250     }
251 
252     @Override
removeScrollListener(ScrollListener l)253     public void removeScrollListener(ScrollListener l) {
254         mScrollListeners.remove(l);
255     }
256 
257     @Override
onScrollChanged(int l, int t, int oldl, int oldt)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
computeVerticalScrollRange()266     public int computeVerticalScrollRange() {
267         return super.computeVerticalScrollRange();
268     }
269 
270     @Override
computeVerticalScrollOffset()271     public int computeVerticalScrollOffset() {
272         return super.computeVerticalScrollOffset();
273     }
274 
275     @Override
computeVerticalScrollExtent()276     public int computeVerticalScrollExtent() {
277         return super.computeVerticalScrollExtent();
278     }
279 
280     @Override
computeHorizontalScrollRange()281     public int computeHorizontalScrollRange() {
282         return super.computeHorizontalScrollRange();
283     }
284 
285     @Override
computeHorizontalScrollOffset()286     public int computeHorizontalScrollOffset() {
287         return super.computeHorizontalScrollOffset();
288     }
289 
290     @Override
computeHorizontalScrollExtent()291     public int computeHorizontalScrollExtent() {
292         return super.computeHorizontalScrollExtent();
293     }
294 }
295