per-message zoom using JavaScript + CSS 3D transforms
When Auto-fit is off, disable built-in WebView zooming, which
is only capable of zooming an entire document (the entire
conversation).
Use ScaleGestureDetector to trigger a CSS 3D transform to zoom
in/out just a single message div.
During the gesture, the elements above and below the message
being scaled are untouched. This is ugly.
TODO: they should either move away (tricky) or at least fade
out.
When the gesture is complete, to avoid leaving overlapping
elements, we force-scale the height and reset the transform
origin to the top left.
TODO: auto-scroll to the correct place to counteract this
perceived jump.
Double-tap is not yet implemented. We'll have to do it
ourselves.
Bug: 7478834
Change-Id: I114e4977304c7060d499d116cc75bc0488967448
diff --git a/assets/script.js b/assets/script.js
index 4a01578..6ea24e8 100644
--- a/assets/script.js
+++ b/assets/script.js
@@ -21,6 +21,8 @@
// this is an Array, but we treat it like a Set and only insert unique items
var gImageLoadElements = [];
+var gScaleInfo;
+
/**
* Returns the page offset of an element.
*
@@ -59,6 +61,17 @@
return parent || null;
}
+function getCachedValue(div, property, attrName) {
+ var value;
+ if (div.hasAttribute(attrName)) {
+ value = div.getAttribute(attrName);
+ } else {
+ value = div[property];
+ div.setAttribute(attrName, value);
+ }
+ return value;
+}
+
function onToggleClick(e) {
toggleQuotedText(e.target);
measurePositions();
@@ -448,6 +461,113 @@
measurePositions();
}
+function onScaleBegin(screenX, screenY) {
+// console.log("JS got scaleBegin x/y=" + screenX + "/" + screenY);
+ var focusX = screenX + document.body.scrollLeft;
+ var focusY = screenY + document.body.scrollTop;
+ var i, len;
+ var msgDivs = document.getElementsByClassName("mail-message");
+ var msgDiv, msgBodyDiv;
+ var msgTop, msgDivTop, nextMsgTop;
+ var initialH;
+ var initialScale;
+ var scaledOriginX, scaledOriginY;
+ var translateX, translateY;
+ var origin;
+
+ gScaleInfo = undefined;
+
+ for (i = 0, len = msgDivs.length; i < len; i++) {
+ msgDiv = msgDivs[i];
+ msgTop = nextMsgTop ? nextMsgTop : getTotalOffset(msgDiv).top;
+ nextMsgTop = (i < len-1) ? getTotalOffset(msgDivs[i+1]).top : document.body.offsetHeight;
+ if (focusY >= msgTop && focusY < nextMsgTop) {
+ msgBodyDiv = msgDiv.children[1];
+ initialScale = msgBodyDiv.getAttribute("data-initial-scale") || 1.0;
+
+ msgDivTop = getTotalOffset(msgBodyDiv).top;
+
+ scaledOriginX = focusX;// / initialScale;
+ scaledOriginY = (focusY - msgDivTop);// / initialScale;
+
+ translateX = scaledOriginX * (initialScale - 1.0) / initialScale;
+ translateY = scaledOriginY * (initialScale - 1.0) / initialScale;
+
+ gScaleInfo = {
+ div: msgBodyDiv,
+ divTop: msgDivTop,
+ initialScale: initialScale,
+ initialX: focusX,
+ initialY: focusY,
+ translateX: translateX,
+ translateY: translateY,
+ initialH: getCachedValue(msgBodyDiv, "offsetHeight", "data-initial-height"),
+ minScale: Math.min(document.body.offsetWidth / msgBodyDiv.scrollWidth, 1.0),
+ currScale: initialScale
+ };
+
+ origin = scaledOriginX + "px " + scaledOriginY + "px";
+ msgBodyDiv.classList.add("zooming-focused");
+ msgBodyDiv.style.webkitTransformOrigin = origin;
+ msgBodyDiv.style.webkitTransform = "scale3d(" + initialScale + "," + initialScale
+ + ",1) translate3d(" + translateX + "px," + translateY + "px,0)";
+// console.log("scaleBegin, h=" + gScaleInfo.initialH + " origin='" + origin + "'");
+ break;
+ }
+ }
+}
+
+function onScaleEnd(screenX, screenY) {
+ var msgBodyDiv;
+ var scale;
+ var h;
+ if (!gScaleInfo) {
+ return;
+ }
+
+// console.log("JS got scaleEnd x/y=" + screenX + "/" + screenY);
+ msgBodyDiv = gScaleInfo.div;
+ scale = gScaleInfo.currScale;
+ msgBodyDiv.style.webkitTransformOrigin = "0 0";
+ // clear any translate
+ msgBodyDiv.style.webkitTransform = "scale3d(" + scale + "," + scale + ",1)";
+ // switching to a 2D transform here re-renders the fonts more clearly, but introduces
+ // texture upload lag to any subsequent scale operation
+ //msgBodyDiv.style.webkitTransform = "scale(" + gScaleInfo.currScale + ")";
+ h = gScaleInfo.initialH * scale;
+// console.log("onScaleEnd set h=" + h);
+ msgBodyDiv.style.height = h + "px";
+ msgBodyDiv.classList.remove("zooming-focused");
+ msgBodyDiv.setAttribute("data-initial-scale", scale);
+}
+
+function onScale(relativeScale, screenX, screenY) {
+ var focusX, focusY;
+ var scale;
+ var translateX, translateY;
+ var transform;
+
+ if (!gScaleInfo) {
+ return;
+ }
+ focusX = screenX + document.body.scrollLeft;
+ focusY = screenY + document.body.scrollTop;
+
+ scale = Math.max(gScaleInfo.initialScale * relativeScale, gScaleInfo.minScale);
+ if (scale > 4.0) {
+ scale = 4.0;
+ }
+ gScaleInfo.currScale = scale;
+ translateX = focusX - gScaleInfo.initialX;
+ translateY = focusY - gScaleInfo.initialY;
+ transform = "translate3d(" + translateX + "px," + translateY + "px,0) scale3d("
+ + scale + "," + scale + ",1) translate3d(" + gScaleInfo.translateX + "px,"
+ + gScaleInfo.translateY + "px,0)";
+ gScaleInfo.div.style.webkitTransform = transform;
+// console.log("JS got scale=" + relativeScale + " x/y=" + screenX + "/" + screenY
+// + " transform='" + transform + "'");
+}
+
// END Java->JavaScript handlers
// Do this first to ensure that the readiness signal comes through,
diff --git a/res/raw/template_message.html b/res/raw/template_message.html
index 9de1c64..8195f7d 100644
--- a/res/raw/template_message.html
+++ b/res/raw/template_message.html
@@ -1,5 +1,5 @@
<div id="%s" class="mail-message %s">
<div class="mail-message-header spacer" style="height: %spx;"></div>
- <div class="mail-message-content collapsible zoom-normal %s" style="display: %s; padding: 16px 0;">%s</div>
+ <div class="mail-message-content collapsible zoom-normal %s" style="display: %s; margin: 16px 0;">%s</div>
<div class="mail-message-footer spacer collapsible" style="display: %s; height: %spx;"></div>
</div>
diff --git a/src/com/android/mail/browse/ConversationWebView.java b/src/com/android/mail/browse/ConversationWebView.java
index 857db7a..f17ca3c 100644
--- a/src/com/android/mail/browse/ConversationWebView.java
+++ b/src/com/android/mail/browse/ConversationWebView.java
@@ -23,6 +23,8 @@
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.view.MotionEvent;
+import android.view.ScaleGestureDetector;
+import android.view.ScaleGestureDetector.OnScaleGestureListener;
import android.webkit.WebView;
import com.android.mail.R;
@@ -145,6 +147,8 @@
private ContentSizeChangeListener mSizeChangeListener;
+ private ScaleGestureDetector mScaleDetector;
+
private int mCachedContentHeight;
private final int mViewportWidth;
@@ -188,6 +192,13 @@
mSizeChangeListener = l;
}
+ public void setOnScaleGestureListener(OnScaleGestureListener l) {
+ if (l == null) {
+ mScaleDetector = null;
+ } else {
+ mScaleDetector = new ScaleGestureDetector(getContext(), l);
+ }
+ }
@Override
public int computeVerticalScrollRange() {
@@ -259,7 +270,13 @@
break;
}
- return super.onTouchEvent(ev);
+ final boolean handled = super.onTouchEvent(ev);
+
+ if (mScaleDetector != null) {
+ mScaleDetector.onTouchEvent(ev);
+ }
+
+ return handled;
}
public boolean isHandlingTouch() {
@@ -297,4 +314,5 @@
public float webPxToScreenPxError(int webPx) {
return webPx * getInitialScale() - webPxToScreenPx(webPx);
}
+
}
diff --git a/src/com/android/mail/ui/ConversationViewFragment.java b/src/com/android/mail/ui/ConversationViewFragment.java
index 28dd44f..c1ebd4a 100644
--- a/src/com/android/mail/ui/ConversationViewFragment.java
+++ b/src/com/android/mail/ui/ConversationViewFragment.java
@@ -30,7 +30,9 @@
import android.os.SystemClock;
import android.text.TextUtils;
import android.view.LayoutInflater;
+import android.view.ScaleGestureDetector;
import android.view.View;
+import android.view.ScaleGestureDetector.OnScaleGestureListener;
import android.view.View.OnLayoutChangeListener;
import android.view.ViewGroup;
import android.webkit.ConsoleMessage;
@@ -126,6 +128,8 @@
private final WebViewClient mWebViewClient = new ConversationWebViewClient();
+ private final ScaleInterceptor mScaleInterceptor = new ScaleInterceptor();
+
private ConversationViewAdapter mAdapter;
private boolean mViewsCreated;
@@ -143,6 +147,8 @@
private int mMaxAutoLoadMessages;
+ private int mSideMarginPx;
+
/**
* If this conversation fragment is not visible, and it's inappropriate to load up front,
* this is the reason we are waiting. This flag should be cleared once it's okay to load
@@ -264,6 +270,10 @@
mMaxAutoLoadMessages = getResources().getInteger(R.integer.max_auto_load_messages);
+ mSideMarginPx = getResources().getDimensionPixelOffset(
+ R.dimen.conversation_view_margin_side) + getResources().getDimensionPixelOffset(
+ R.dimen.conversation_message_content_margin_side);
+
mWebView.setOnCreateContextMenuListener(new WebViewContextMenu(getActivity()));
// set this up here instead of onCreateView to ensure the latest Account is loaded
@@ -596,11 +606,7 @@
final int convHeaderPos = mAdapter.addConversationHeader(mConversation);
final int convHeaderPx = measureOverlayHeight(convHeaderPos);
- final int sideMarginPx = getResources().getDimensionPixelOffset(
- R.dimen.conversation_view_margin_side) + getResources().getDimensionPixelOffset(
- R.dimen.conversation_message_content_margin_side);
-
- mTemplates.startConversation(mWebView.screenPxToWebPx(sideMarginPx),
+ mTemplates.startConversation(mWebView.screenPxToWebPx(mSideMarginPx),
mWebView.screenPxToWebPx(convHeaderPx));
int collapsedStart = -1;
@@ -926,14 +932,17 @@
}
private void setupOverviewMode() {
+ // for now, overview mode means use the built-in WebView zoom and disable custom scale
+ // gesture handling
final boolean overviewMode = isOverviewMode(mAccount);
final WebSettings settings = mWebView.getSettings();
settings.setUseWideViewPort(overviewMode);
settings.setSupportZoom(overviewMode);
+ settings.setBuiltInZoomControls(overviewMode);
if (overviewMode) {
- settings.setBuiltInZoomControls(true);
settings.setDisplayZoomControls(false);
}
+ mWebView.setOnScaleGestureListener(overviewMode ? null : mScaleInterceptor);
}
private class ConversationWebViewClient extends AbstractConversationWebViewClient {
@@ -1348,4 +1357,38 @@
int heightBefore) {
mDiff = (expanded ? 1 : -1) * Math.abs(i.getHeight() - heightBefore);
}
+
+ private class ScaleInterceptor implements OnScaleGestureListener {
+
+ private float getFocusXWebPx(ScaleGestureDetector detector) {
+ return (detector.getFocusX() - mSideMarginPx) / mWebView.getInitialScale();
+ }
+
+ private float getFocusYWebPx(ScaleGestureDetector detector) {
+ return detector.getFocusY() / mWebView.getInitialScale();
+ }
+
+ @Override
+ public boolean onScale(ScaleGestureDetector detector) {
+ mWebView.loadUrl(String.format("javascript:onScale(%s, %s, %s);",
+ detector.getScaleFactor(), getFocusXWebPx(detector),
+ getFocusYWebPx(detector)));
+ return false;
+ }
+
+ @Override
+ public boolean onScaleBegin(ScaleGestureDetector detector) {
+ mWebView.loadUrl(String.format("javascript:onScaleBegin(%s, %s);",
+ getFocusXWebPx(detector), getFocusYWebPx(detector)));
+ return true;
+ }
+
+ @Override
+ public void onScaleEnd(ScaleGestureDetector detector) {
+ mWebView.loadUrl(String.format("javascript:onScaleEnd(%s, %s);",
+ getFocusXWebPx(detector), getFocusYWebPx(detector)));
+ }
+
+ }
+
}