Merge "Conversation view touch event shenanigans"
diff --git a/src/com/android/mail/browse/MessageHeaderView.java b/src/com/android/mail/browse/MessageHeaderView.java
index 1466bb0..ed07493 100644
--- a/src/com/android/mail/browse/MessageHeaderView.java
+++ b/src/com/android/mail/browse/MessageHeaderView.java
@@ -16,6 +16,8 @@
package com.android.mail.browse;
+import com.google.common.annotations.VisibleForTesting;
+
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Typeface;
@@ -28,18 +30,20 @@
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
-import android.view.ViewGroup;
import android.view.View.OnClickListener;
+import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.PopupMenu;
+import android.widget.PopupMenu.OnMenuItemClickListener;
import android.widget.QuickContactBadge;
import android.widget.TextView;
import android.widget.Toast;
-import android.widget.PopupMenu.OnMenuItemClickListener;
import com.android.mail.ContactInfoSource;
import com.android.mail.FormattedDateBuilder;
+import com.android.mail.R;
+import com.android.mail.SenderInfoLoader.ContactInfo;
import com.android.mail.compose.ComposeActivity;
import com.android.mail.perf.Timer;
import com.android.mail.providers.Account;
@@ -47,11 +51,8 @@
import com.android.mail.providers.Attachment;
import com.android.mail.providers.Message;
import com.android.mail.providers.UIProvider;
-import com.android.mail.R;
-import com.android.mail.SenderInfoLoader.ContactInfo;
import com.android.mail.utils.LogUtils;
import com.android.mail.utils.Utils;
-import com.google.common.annotations.VisibleForTesting;
import java.io.IOException;
import java.io.StringReader;
@@ -1179,4 +1180,5 @@
t.pause(MEASURE_TAG);
}
}
+
}
diff --git a/src/com/android/mail/browse/MessageWebView.java b/src/com/android/mail/browse/MessageWebView.java
deleted file mode 100644
index d2fbcd3..0000000
--- a/src/com/android/mail/browse/MessageWebView.java
+++ /dev/null
@@ -1,156 +0,0 @@
-/*
- * Copyright (C) 2011 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.ViewGroup;
-import android.view.ViewParent;
-import android.webkit.WebView;
-
-import com.android.mail.utils.LogUtils;
-
-/**
- * A WebView for HTML messages with custom zoom and scroll behavior.
- *
- */
-public class MessageWebView extends WebView {
-
- private float mMaxScale = 0;
- /**
- * WebView with height=content_wrap doesn't shrink its height when zooming out.
- * So to force this behavior, when zooming out (scale has shrunken) trick the measurement
- * of WebView so as to make it think that it *should* fit the height to its bound AND
- * that its view height is now zero. WebView will then attempt to fit its content into
- * a zero-height view, and then resize the view height to the 'natural' content height.
- *
- * The measurement trick should be turned off on following onLayout to allow WebKit to grow its
- * height again.
- */
- private boolean mShrinkMeasuredHeight;
- /**
- * When tricking the WebView to be zero height, views below it will shift up when drawing,
- * and then shift back down when the WebView does its corrective layout pass.
- * To avoid this shifting, force the WebView's parent view to keep its height fixed until
- * the corrective layout pass is over, at which point we can restore the normal WebView height
- * and the views below will draw in their correct final positions.
- */
- private int mParentLayoutHeight;
- private boolean mCheckedWidePage;
-
- private static final boolean ENABLE_WIDE_VIEWPORT_MODE = false;
- private static final String LOG_TAG = new LogUtils().getLogTag();
-
- public MessageWebView(Context context) {
- this(context, null);
- }
-
- public MessageWebView(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
- /**
- * Set the view parent's height and trigger a layout.
- *
- * @param measure if true, set the height to the current measured height and ignore the height
- * parameter
- * @param height a height to set it to. ignored if measure is true
- * @return the original height
- */
- private int setParentHeight(boolean measure, int height) {
- int originalHeight = 0;
- ViewParent parent = getParent();
- if (parent instanceof ViewGroup) {
- ViewGroup parentGroup = (ViewGroup) getParent();
- ViewGroup.LayoutParams parentLayoutParams = parentGroup.getLayoutParams();
- originalHeight = parentLayoutParams.height;
- parentLayoutParams.height = (measure) ? parentGroup.getHeight() : height;
- parentGroup.setLayoutParams(parentLayoutParams);
- }
- return originalHeight;
- }
-
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- int heightMode = MeasureSpec.getMode(heightMeasureSpec);
- int heightSize = MeasureSpec.getSize(heightMeasureSpec);
- int widthMode = MeasureSpec.getMode(widthMeasureSpec);
- int widthSize = MeasureSpec.getSize(widthMeasureSpec);
-
- // Allow WebView to measure as it normally would, which sets the HeightCanMeasure flag to
- // trigger a WebKit layout. Afterwards, override the measured height with zero so that
- // WebKit layout uses a desired height of zero, which shrinks the view to true content
- // height.
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
-
- if (mShrinkMeasuredHeight && heightMode != MeasureSpec.EXACTLY) {
- setMeasuredDimension(getMeasuredWidthAndState(), 0);
- }
- }
-
- @Override
- protected void onLayout(boolean changed, int l, int t, int r, int b) {
- LogUtils.d(LOG_TAG, "IN %d onLayout, changed=%b w/h=%d/%d l/t/r/b=%d/%d/%d/%d z=%f",
- (hashCode() % 1000), changed, getWidth(), getHeight(), l, t, r, b , getScale());
- super.onLayout(changed, l, t, r, b);
-
- float scale = getScale();
-
- if (mShrinkMeasuredHeight) {
- // restore normal measurement behavior on the first layout after overriding height
- // also restore parent container height
- setParentHeight(false, mParentLayoutHeight);
- mShrinkMeasuredHeight = false;
-
- } else if (getHeight() > 0) {
- if (scale < mMaxScale && !getSettings().getUseWideViewPort()) {
- // fix the parent's height to whatever it is now.
- // (normally the parent would be recalculated as header + body height)
- // this will prevent messages below from shifting up
- // and then when it's all done, set it back to the original value
- // so the below messages just finally adjust now that the height is settled.
-
- // we expect that WebView will trigger another layout anyway,
- // and we have to inject this new sizing in before then.
- LogUtils.d(LOG_TAG, "*** shrinking height of webview=" + (hashCode() % 1000));
-
- mParentLayoutHeight = setParentHeight(true, 0);
- mMaxScale = 0;
- // force all measurements from now until the next layout to claim a zero height
- mShrinkMeasuredHeight = true;
-
- } else if (scale > mMaxScale) {
- mMaxScale = getScale();
-
- if (ENABLE_WIDE_VIEWPORT_MODE) {
- if (!mCheckedWidePage) {
- if ((getMeasuredWidthAndState() & MEASURED_STATE_TOO_SMALL) != 0) {
- LogUtils.i(LOG_TAG, "*** setting wide page mode for webview=%d",
- (hashCode() % 1000));
- getSettings().setUseWideViewPort(true);
- // FIXME: wide viewport mode does not grow/shrink height as expected
- // maybe override height at this point and turn off reflow so that
- // scaling is linear, and we can manually maintain view height.
- }
- mCheckedWidePage = true;
- }
- }
- }
- }
- }
-
-}
diff --git a/src/com/android/mail/ui/ConversationContainer.java b/src/com/android/mail/ui/ConversationContainer.java
index 0f7ab3c..d57ed68 100644
--- a/src/com/android/mail/ui/ConversationContainer.java
+++ b/src/com/android/mail/ui/ConversationContainer.java
@@ -19,10 +19,13 @@
import android.content.Context;
import android.util.AttributeSet;
+import android.view.MotionEvent;
import android.view.View;
+import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.webkit.WebView;
import android.widget.Adapter;
+import android.widget.ScrollView;
import com.android.mail.ui.ScrollNotifier.ScrollListener;
import com.android.mail.utils.LogUtils;
@@ -33,20 +36,69 @@
*/
public class ConversationContainer extends ViewGroup implements ScrollListener {
- private Adapter mOverlayAdapter;
- private int[] mOverlayTops;
-
private static final String TAG = new LogUtils().getLogTag();
- private int mOffsetY;
+ private Adapter mOverlayAdapter;
+ private int[] mOverlayTops;
+ private ConversationWebView mWebView;
+
+ /**
+ * Current document zoom scale per {@link WebView#getScale()}. It does not already account for
+ * display density, but by a happy coincidence, this makes the arithmetic for overlay placement
+ * easier.
+ */
private float mScale;
+ /**
+ * System touch-slop distance per {@link ViewConfiguration#getScaledTouchSlop()}.
+ */
+ private final int mTouchSlop;
+ /**
+ * Current scroll position, as dictated by the background {@link WebView}.
+ */
+ private int mOffsetY;
+ /**
+ * Original pointer Y for slop calculation.
+ */
+ private float mLastMotionY;
+ /**
+ * Original pointer ID for slop calculation.
+ */
+ private int mActivePointerId;
+ /**
+ * Track pointer up/down state to know whether to send a make-up DOWN event to WebView.
+ * WebView internal logic requires that a stream of {@link MotionEvent#ACTION_MOVE} events be
+ * preceded by a {@link MotionEvent#ACTION_DOWN} event.
+ */
+ private boolean mTouchIsDown = false;
+ /**
+ * Remember if touch interception was triggered on a {@link MotionEvent#ACTION_POINTER_DOWN},
+ * so we can send a make-up event in {@link #onTouchEvent(MotionEvent)}.
+ */
+ private boolean mMissedPointerDown;
+
public ConversationContainer(Context c) {
this(c, null);
}
public ConversationContainer(Context c, AttributeSet attrs) {
super(c, attrs);
+
+ mTouchSlop = ViewConfiguration.get(c).getScaledTouchSlop();
+
+ // Disabling event splitting fixes pinch-zoom when the first pointer goes down on the
+ // WebView and the second pointer goes down on an overlay view.
+ // Intercepting ACTION_POINTER_DOWN events allows pinch-zoom to work when the first pointer
+ // goes down on an overlay view.
+ setMotionEventSplittingEnabled(false);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+
+ mWebView = (ConversationWebView) getChildAt(0);
+ mWebView.addScrollListener(this);
}
public void setOverlayAdapter(Adapter a) {
@@ -61,14 +113,77 @@
return getChildAt(i + 1);
}
- private WebView getBackgroundView() {
- return (WebView) getChildAt(0);
+ private void forwardFakeMotionEvent(MotionEvent original, int newAction) {
+ MotionEvent newEvent = MotionEvent.obtain(original);
+ newEvent.setAction(newAction);
+ mWebView.onTouchEvent(newEvent);
+ LogUtils.v(TAG, "in Container.OnTouch. fake: action=%d x/y=%f/%f pointers=%d",
+ newEvent.getActionMasked(), newEvent.getX(), newEvent.getY(),
+ newEvent.getPointerCount());
+ }
+
+ /**
+ * Touch slop code was copied from {@link ScrollView#onInterceptTouchEvent(MotionEvent)}.
+ */
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ boolean intercept = false;
+ switch (ev.getActionMasked()) {
+ case MotionEvent.ACTION_POINTER_DOWN:
+ intercept = true;
+ mMissedPointerDown = true;
+ break;
+
+ case MotionEvent.ACTION_DOWN:
+ mLastMotionY = ev.getY();
+ mActivePointerId = ev.getPointerId(0);
+ break;
+
+ case MotionEvent.ACTION_MOVE:
+ final int pointerIndex = ev.findPointerIndex(mActivePointerId);
+ final float y = ev.getY(pointerIndex);
+ final int yDiff = (int) Math.abs(y - mLastMotionY);
+ if (yDiff > mTouchSlop) {
+ mLastMotionY = y;
+ intercept = true;
+ }
+ break;
+ }
+
+ LogUtils.v(TAG, "in Container.InterceptTouch. action=%d x/y=%f/%f pointers=%d result=%s",
+ ev.getActionMasked(), ev.getX(), ev.getY(), ev.getPointerCount(), intercept);
+ return intercept;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ final int action = ev.getActionMasked();
+
+ if (action == MotionEvent.ACTION_UP) {
+ mTouchIsDown = false;
+ } else if (!mTouchIsDown &&
+ (action == MotionEvent.ACTION_MOVE || action == MotionEvent.ACTION_POINTER_DOWN)) {
+
+ forwardFakeMotionEvent(ev, MotionEvent.ACTION_DOWN);
+ if (mMissedPointerDown) {
+ forwardFakeMotionEvent(ev, MotionEvent.ACTION_POINTER_DOWN);
+ mMissedPointerDown = false;
+ }
+
+ mTouchIsDown = true;
+ }
+
+ final boolean webViewResult = mWebView.onTouchEvent(ev);
+
+ LogUtils.v(TAG, "in Container.OnTouch. action=%d x/y=%f/%f pointers=%d",
+ ev.getActionMasked(), ev.getX(), ev.getY(), ev.getPointerCount());
+ return webViewResult;
}
@Override
public void onNotifierScroll(int x, int y) {
mOffsetY = y;
- mScale = getBackgroundView().getScale();
+ mScale = mWebView.getScale();
LogUtils.v(TAG, "*** IN on scroll, x/y=%d/%d zoom=%f", x, y, mScale);
layoutOverlays();
@@ -77,8 +192,9 @@
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- LogUtils.d(TAG, "*** IN header container onMeasure");
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ LogUtils.d(TAG, "*** IN header container onMeasure spec for w/h=%d/%d", widthMeasureSpec,
+ heightMeasureSpec);
measureChildren(widthMeasureSpec, heightMeasureSpec);
}
@@ -86,9 +202,8 @@
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
LogUtils.d(TAG, "*** IN header container onLayout");
- final View backgroundView = getBackgroundView();
- backgroundView.layout(0, 0, backgroundView.getMeasuredWidth(),
- backgroundView.getMeasuredHeight());
+ mWebView.layout(0, 0, mWebView.getMeasuredWidth(),
+ mWebView.getMeasuredHeight());
layoutOverlays();
}
@@ -111,7 +226,7 @@
}
}
- // TODO: add margin support for children that want it (e.g. tablet headers)
+ // TODO: add margin support for children that want it (e.g. tablet headers?)
public void onGeometryChange(int[] messageTops) {
LogUtils.d(TAG, "*** got message tops:");
@@ -132,7 +247,7 @@
// to position bottom-anchored content like attachments
}
- mScale = getBackgroundView().getScale();
+ mScale = mWebView.getScale();
}
diff --git a/src/com/android/mail/ui/ConversationViewFragment.java b/src/com/android/mail/ui/ConversationViewFragment.java
index a64ce29..4e173e4 100644
--- a/src/com/android/mail/ui/ConversationViewFragment.java
+++ b/src/com/android/mail/ui/ConversationViewFragment.java
@@ -148,8 +148,6 @@
.findViewById(R.id.conversation_container);
mWebView = (ConversationWebView) rootView.findViewById(R.id.webview);
- mWebView.addScrollListener(mConversationContainer);
-
mWebView.addJavascriptInterface(mJsBridge, "mail");
mWebView.setWebChromeClient(new WebChromeClient() {
diff --git a/src/com/android/mail/ui/ConversationWebView.java b/src/com/android/mail/ui/ConversationWebView.java
index edf337b..72852dd 100644
--- a/src/com/android/mail/ui/ConversationWebView.java
+++ b/src/com/android/mail/ui/ConversationWebView.java
@@ -19,6 +19,7 @@
import android.content.Context;
import android.util.AttributeSet;
+import android.view.MotionEvent;
import android.webkit.WebView;
import java.util.Set;
@@ -56,5 +57,16 @@
}
}
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ boolean result = super.onTouchEvent(ev);
+
+ if (result) {
+ // Events handled by the WebView should not be monkeyed with by any overlay interceptor
+ requestDisallowInterceptTouchEvent(true);
+ }
+
+ return result;
+ }
}