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