blob: d3182ae05d8c9079b918c3c4d9cdd0c4288436b0 [file] [log] [blame]
/*
* Copyright (C) 2013 Google Inc.
* Licensed to The Android Open Source Project.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.mail.browse;
import android.content.Context;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.ViewConfiguration;
import android.widget.ScrollView;
import com.android.mail.utils.LogUtils;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
/**
* A container that tries to play nice with an internally scrollable {@link Touchable} child view.
* The assumption is that the child view can scroll horizontally, but not vertically, so any
* touch events on that child view should ALSO be sent here so it can simultaneously vertically
* scroll (not the standard either/or behavior).
* <p>
* Touch events on any other child of this ScrollView are intercepted in the standard fashion.
*/
public class MessageScrollView extends ScrollView implements ScrollNotifier,
ScaleGestureDetector.OnScaleGestureListener, GestureDetector.OnDoubleTapListener {
/**
* A View that reports whether onTouchEvent() was recently called.
*/
public interface Touchable {
boolean wasTouched();
void clearTouched();
boolean zoomIn();
boolean zoomOut();
}
/**
* True when performing "special" interception.
*/
private boolean mWantToIntercept;
/**
* Whether to perform the standard touch interception procedure. This is set to true when we
* want to intercept a touch stream from any child OTHER than {@link #mTouchableChild}.
*/
private boolean mInterceptNormally;
/**
* The special child that we want to NOT intercept from in the normal way. Instead, this child
* will continue to receive the touch event stream (so it can handle the horizontal component)
* while this parent will additionally handle the events to perform vertical scrolling.
*/
private Touchable mTouchableChild;
/**
* We want to detect the scale gesture so that we don't try to scroll instead, but we don't
* care about actually interpreting it because the webview does that by itself when it handles
* the touch events.
*
* This might lead to really weird interactions if the two gesture detectors' implementations
* drift...
*/
private ScaleGestureDetector mScaleDetector;
private boolean mInScaleGesture;
/**
* We also want to detect double-tap gestures, but in a way that doesn't conflict with
* tap-tap-drag gestures
*/
private GestureDetector mGestureDetector;
private boolean mDoubleTapOccurred;
private boolean mZoomedIn;
/**
* Touch slop used to determine if this double tap is valid for starting a scale or should be
* ignored.
*/
private int mTouchSlopSquared;
/**
* X and Y coordinates for the current down event. Since mDoubleTapOccurred only contains the
* information that there was a double tap event, use these to get the secondary tap
* information to determine if a user has moved beyond touch slop.
*/
private float mDownFocusX;
private float mDownFocusY;
private final Set<ScrollListener> mScrollListeners =
new CopyOnWriteArraySet<ScrollListener>();
public static final String LOG_TAG = "MsgScroller";
public MessageScrollView(Context c) {
this(c, null);
}
public MessageScrollView(Context c, AttributeSet attrs) {
super(c, attrs);
final int touchSlop = ViewConfiguration.get(c).getScaledTouchSlop();
mTouchSlopSquared = touchSlop * touchSlop;
mScaleDetector = new ScaleGestureDetector(c, this);
mGestureDetector = new GestureDetector(c, new GestureDetector.SimpleOnGestureListener());
mGestureDetector.setOnDoubleTapListener(this);
}
public void setInnerScrollableView(Touchable child) {
mTouchableChild = child;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (mInterceptNormally) {
LogUtils.d(LOG_TAG, "IN ScrollView.onIntercept, NOW stealing. ev=%s", ev);
return true;
} else if (mWantToIntercept) {
LogUtils.d(LOG_TAG, "IN ScrollView.onIntercept, already stealing. ev=%s", ev);
return false;
}
mWantToIntercept = super.onInterceptTouchEvent(ev);
LogUtils.d(LOG_TAG, "OUT ScrollView.onIntercept, steal=%s ev=%s", mWantToIntercept, ev);
return false;
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
final int action = ev.getActionMasked();
switch (action) {
case MotionEvent.ACTION_DOWN:
LogUtils.d(LOG_TAG, "IN ScrollView.dispatchTouch, clearing flags");
mWantToIntercept = false;
mInterceptNormally = false;
break;
}
if (mTouchableChild != null) {
mTouchableChild.clearTouched();
}
mScaleDetector.onTouchEvent(ev);
mGestureDetector.onTouchEvent(ev);
final boolean handled = super.dispatchTouchEvent(ev);
LogUtils.d(LOG_TAG, "OUT ScrollView.dispatchTouch, handled=%s ev=%s", handled, ev);
if (mWantToIntercept && !mInScaleGesture) {
final boolean touchedChild = (mTouchableChild != null && mTouchableChild.wasTouched());
if (touchedChild) {
// also give the event to this scroll view if the WebView got the event
// and didn't stop any parent interception
LogUtils.d(LOG_TAG, "IN extra ScrollView.onTouch, ev=%s", ev);
onTouchEvent(ev);
} else {
mInterceptNormally = true;
mWantToIntercept = false;
}
}
return handled;
}
@Override
public boolean onScale(ScaleGestureDetector detector) {
return true;
}
@Override
public boolean onScaleBegin(ScaleGestureDetector detector) {
LogUtils.d(LOG_TAG, "Begin scale gesture");
mInScaleGesture = true;
return true;
}
@Override
public void onScaleEnd(ScaleGestureDetector detector) {
LogUtils.d(LOG_TAG, "End scale gesture");
mInScaleGesture = false;
}
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
return false;
}
@Override
public boolean onDoubleTap(MotionEvent e) {
mDoubleTapOccurred = true;
return false;
}
@Override
public boolean onDoubleTapEvent(MotionEvent e) {
final int action = e.getAction();
boolean handled = false;
switch (action) {
case MotionEvent.ACTION_DOWN:
mDownFocusX = e.getX();
mDownFocusY = e.getY();
break;
case MotionEvent.ACTION_UP:
handled = triggerZoom();
break;
case MotionEvent.ACTION_MOVE:
final int deltaX = (int) (e.getX() - mDownFocusX);
final int deltaY = (int) (e.getY() - mDownFocusY);
int distance = (deltaX * deltaX) + (deltaY * deltaY);
if (distance > mTouchSlopSquared) {
mDoubleTapOccurred = false;
}
break;
}
return handled;
}
private boolean triggerZoom() {
boolean handled = false;
if (mDoubleTapOccurred) {
if (mZoomedIn) {
mTouchableChild.zoomOut();
} else {
mTouchableChild.zoomIn();
}
mZoomedIn = !mZoomedIn;
LogUtils.d(LogUtils.TAG, "Trigger Zoom!");
handled = true;
}
mDoubleTapOccurred = false;
return handled;
}
@Override
public void addScrollListener(ScrollListener l) {
mScrollListeners.add(l);
}
@Override
public void removeScrollListener(ScrollListener l) {
mScrollListeners.remove(l);
}
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
super.onScrollChanged(l, t, oldl, oldt);
for (ScrollListener listener : mScrollListeners) {
listener.onNotifierScroll(l, t);
}
}
@Override
public int computeVerticalScrollRange() {
return super.computeVerticalScrollRange();
}
@Override
public int computeVerticalScrollOffset() {
return super.computeVerticalScrollOffset();
}
@Override
public int computeVerticalScrollExtent() {
return super.computeVerticalScrollExtent();
}
@Override
public int computeHorizontalScrollRange() {
return super.computeHorizontalScrollRange();
}
@Override
public int computeHorizontalScrollOffset() {
return super.computeHorizontalScrollOffset();
}
@Override
public int computeHorizontalScrollExtent() {
return super.computeHorizontalScrollExtent();
}
}