better handling of wide message content
Don't hide horizontal overflow, so content in extra-wide
messages is scrollable. (Downside is that an initial swipe on
such conversations will scroll the content rather than going to
ViewPager.)
Use default WebView text layout algorithm (NARROW_COLUMNS). It
doesn't kick in by default, possibly because of the method we
use to shrink wide messages, but at least double-tap after a
manual zoom-in will trigger line re-wrapping.
NARROW_COLUMNS reflow can change the HTML height, so we need to
remeasure and reposition headers when that happens. Added code
to listen for WebView mContentHeight changes. This is done
circuitously (via invalidate()) because I don't know of a more
natural way to to know when this happens. Although invalidate()
happens all the time, this should be pretty cheap because no
work is done unless the DOM height changes.
Clean up screen-pixel -> HTML-pixel conversion code. This will
be handy when trying to make the page initially wide to further
improve line wrapping behavior and reduce the frequency of
the extra-wide case. Initial-wide-mode presents a host of
side effects to be addressed in a future CL.
Bug: 6389819
Bug: 6318848
Change-Id: I3ad2bd1ca6c1f6c0859af1a10056578ea4faf073
diff --git a/assets/script.js b/assets/script.js
index bc75e60..284dabd 100644
--- a/assets/script.js
+++ b/assets/script.js
@@ -63,7 +63,7 @@
}
}
-function shrinkWideMessages() {
+function normalizeMessageWidths() {
var i;
var elements = document.getElementsByClassName("mail-message-content");
var messageElement;
@@ -71,21 +71,7 @@
var scale;
for (i = 0; i < elements.length; i++) {
messageElement = elements[i];
- if (messageElement.scrollWidth > documentWidth) {
- scale = documentWidth / messageElement.scrollWidth;
-
- // TODO: 'zoom' is nice because it does a proper layout, but WebView seems to clamp the
- // minimum 'zoom' level.
- if (false) {
- // TODO: this alternative works well in Chrome but doesn't work in WebView.
- messageElement.style.webkitTransformOrigin = "left top";
- messageElement.style.webkitTransform = "scale(" + scale + ")";
- messageElement.style.height = (messageElement.offsetHeight * scale) + "px";
- messageElement.style.overflowX = "visible";
- } else {
- messageElement.style.zoom = documentWidth / messageElement.scrollWidth;
- }
- }
+ messageElement.style.zoom = documentWidth / messageElement.scrollWidth;
}
}
@@ -129,6 +115,12 @@
}
}
+function setWideViewport() {
+ var metaViewport = document.getElementById('meta-viewport');
+ metaViewport.setAttribute('content', 'width=' + WIDE_VIEWPORT_WIDTH);
+}
+
+// BEGIN Java->JavaScript handlers
function measurePositions() {
var overlayBottoms;
var h;
@@ -149,7 +141,6 @@
window.mail.onWebContentGeometryChange(overlayBottoms);
}
-// BEGIN Java->JavaScript handlers
function unblockImages(messageDomId) {
var i, images, imgCount, image, blockedSrc;
var msg = document.getElementById(messageDomId);
@@ -217,6 +208,7 @@
collapseQuotedText();
hideUnsafeImages();
-shrinkWideMessages();
+normalizeMessageWidths();
+//setWideViewport();
measurePositions();
diff --git a/res/raw/template_conversation_lower.html b/res/raw/template_conversation_lower.html
index 9118068..9f69deb 100644
--- a/res/raw/template_conversation_lower.html
+++ b/res/raw/template_conversation_lower.html
@@ -3,7 +3,8 @@
var MSG_HIDE_ELIDED = '%s';
var MSG_SHOW_ELIDED = '%s';
var ACCOUNT_URI = '%s';
- var VIEW_WIDTH = %s;
+ var VIEW_WIDTH = %d;
+ var WIDE_VIEWPORT_WIDTH = %d;
</script>
<script type="text/javascript" src="file:///android_asset/script.js"></script>
</html>
diff --git a/res/raw/template_conversation_upper.html b/res/raw/template_conversation_upper.html
index 99c9e68..32b884d 100644
--- a/res/raw/template_conversation_upper.html
+++ b/res/raw/template_conversation_upper.html
@@ -1,7 +1,7 @@
<!DOCTYPE html>
<html>
<head>
- <meta name="viewport" content="width=device-width"/>
+ <meta id="meta-viewport" name="viewport" content="width=device-width"/>
<style>
.elided-text {
display: none;
@@ -16,7 +16,6 @@
margin: 12px 0 6px;
}
.mail-message-content {
- overflow-x: hidden;
}
body {
font-size: 80%%;
diff --git a/res/values/constants.xml b/res/values/constants.xml
index 4f072e3..18255bd 100644
--- a/res/values/constants.xml
+++ b/res/values/constants.xml
@@ -51,6 +51,7 @@
<integer name="conversation_desired_font_size_px">14</integer>
<!-- matches 'font-size' style in template_conversation_upper.html -->
<integer name="conversation_unstyled_font_size_px">13</integer>
+ <integer name="conversation_webview_viewport_px">980</integer>
<!-- Whether the list is collapsed in conversation view mode -->
<bool name="list_collapsed">true</bool>
diff --git a/src/com/android/mail/browse/ConversationContainer.java b/src/com/android/mail/browse/ConversationContainer.java
index 7f13a9a..f556929 100644
--- a/src/com/android/mail/browse/ConversationContainer.java
+++ b/src/com/android/mail/browse/ConversationContainer.java
@@ -66,9 +66,8 @@
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.
+ * Current document zoom scale per {@link WebView#getScale()}. This is the ratio of actual
+ * screen pixels to logical WebView HTML pixels. We use it to convert from one to the other.
*/
private float mScale;
/**
@@ -126,8 +125,6 @@
*/
private final SparseArray<OverlayView> mOverlayViews;
- private final float mDensity;
-
private int mWidthMeasureSpec;
private boolean mDisableLayoutTracing;
@@ -166,9 +163,6 @@
mOverlayViews = new SparseArray<OverlayView>();
- mDensity = c.getResources().getDisplayMetrics().density;
- mScale = mDensity;
-
mTouchSlop = ViewConfiguration.get(c).getScaledTouchSlop();
// Disabling event splitting fixes pinch-zoom when the first pointer goes down on the
@@ -304,12 +298,14 @@
/*
* The scale value that WebView reports is inaccurate when measured during WebView
* initialization. This bug is present in ICS, so to work around it, we ignore all
- * reported values and use the density (expected value) instead. Only when the user
- * actually begins to touch the view (to, say, begin a zoom) do we begin to pay attention
- * to WebView-reported scale values.
+ * reported values and use a calculated expected value from ConversationWebView instead.
+ * Only when the user actually begins to touch the view (to, say, begin a zoom) do we begin
+ * to pay attention to WebView-reported scale values.
*/
if (mTouchInitialized) {
mScale = mWebView.getScale();
+ } else if (mScale == 0) {
+ mScale = mWebView.getInitialScale();
}
traceLayout("in positionOverlays, raw scale=%f, effective scale=%f", mWebView.getScale(),
mScale);
diff --git a/src/com/android/mail/browse/ConversationWebView.java b/src/com/android/mail/browse/ConversationWebView.java
index 3632f45..26302b9 100644
--- a/src/com/android/mail/browse/ConversationWebView.java
+++ b/src/com/android/mail/browse/ConversationWebView.java
@@ -22,6 +22,7 @@
import android.view.MotionEvent;
import android.webkit.WebView;
+import com.android.mail.R;
import com.android.mail.utils.LogTag;
import com.android.mail.utils.LogUtils;
@@ -30,6 +31,18 @@
public class ConversationWebView extends WebView implements ScrollNotifier {
+ // NARROW_COLUMNS reflow can trigger the document to change size, so notify interested parties.
+ public interface ContentSizeChangeListener {
+ void onHeightChange(int h);
+ }
+
+ private ContentSizeChangeListener mSizeChangeListener;
+
+ private int mCachedContentHeight;
+
+ private final int mViewportWidth;
+ private final float mDensity;
+
private final Set<ScrollListener> mScrollListeners =
new CopyOnWriteArraySet<ScrollListener>();
@@ -47,6 +60,9 @@
public ConversationWebView(Context c, AttributeSet attrs) {
super(c, attrs);
+
+ mViewportWidth = getResources().getInteger(R.integer.conversation_webview_viewport_px);
+ mDensity = getResources().getDisplayMetrics().density;
}
@Override
@@ -59,6 +75,10 @@
mScrollListeners.remove(l);
}
+ public void setContentSizeChangeListener(ContentSizeChangeListener l) {
+ mSizeChangeListener = l;
+ }
+
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
super.onScrollChanged(l, t, oldl, oldt);
@@ -69,6 +89,19 @@
}
@Override
+ public void invalidate() {
+ super.invalidate();
+
+ if (mSizeChangeListener != null) {
+ final int contentHeight = getContentHeight();
+ if (contentHeight != mCachedContentHeight) {
+ mCachedContentHeight = contentHeight;
+ mSizeChangeListener.onHeightChange(contentHeight);
+ }
+ }
+ }
+
+ @Override
public boolean onTouchEvent(MotionEvent ev) {
final int action = ev.getActionMasked();
@@ -93,4 +126,28 @@
return mHandlingTouch;
}
+ public int getViewportWidth() {
+ return mViewportWidth;
+ }
+
+ /**
+ * Similar to {@link #getScale()}, except that it returns the initially expected scale, as
+ * determined by the ratio of actual screen pixels to logical HTML pixels.
+ * <p>This assumes that we are able to control the logical HTML viewport with a meta-viewport
+ * tag.
+ */
+ public float getInitialScale() {
+ // an HTML meta-viewport width of "device-width" and unspecified (medium) density means
+ // that the default scale is effectively the screen density.
+ return mDensity;
+ }
+
+ public int screenPxToWebPx(int screenPx) {
+ return (int) (screenPx / getInitialScale());
+ }
+
+ public int webPxToScreenPx(int webPx) {
+ return (int) (webPx * getInitialScale());
+ }
+
}
diff --git a/src/com/android/mail/ui/ConversationViewFragment.java b/src/com/android/mail/ui/ConversationViewFragment.java
index 6a37e14..a5262f5 100644
--- a/src/com/android/mail/ui/ConversationViewFragment.java
+++ b/src/com/android/mail/ui/ConversationViewFragment.java
@@ -118,8 +118,6 @@
private MenuItem mChangeFoldersMenuItem;
- private float mDensity;
-
/**
* Folder is used to help determine valid menu actions for this conversation.
*/
@@ -202,8 +200,6 @@
getLoaderManager(), this, this, this, mAddressCache);
mConversationContainer.setOverlayAdapter(mAdapter);
- mDensity = getResources().getDisplayMetrics().density;
-
mMaxAutoLoadMessages = getResources().getInteger(R.integer.max_auto_load_messages);
showConversation();
@@ -242,13 +238,22 @@
return true;
}
});
+ mWebView.setContentSizeChangeListener(new ConversationWebView.ContentSizeChangeListener() {
+ @Override
+ public void onHeightChange(int h) {
+ // When WebKit says the DOM height has changed, re-measure bodies and re-position
+ // their headers.
+ // This is separate from the typical JavaScript DOM change listeners because
+ // cases like NARROW_COLUMNS text reflow do not trigger DOM events.
+ mWebView.loadUrl("javascript:measurePositions();");
+ }
+ });
final WebSettings settings = mWebView.getSettings();
settings.setJavaScriptEnabled(true);
settings.setUseWideViewPort(true);
-
- settings.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.NORMAL);
+ settings.setLoadWithOverviewMode(true);
settings.setSupportZoom(true);
settings.setBuiltInZoomControls(true);
@@ -472,9 +477,9 @@
// add a single conversation header item
final int convHeaderPos = mAdapter.addConversationHeader(mConversation);
- final int convHeaderDp = measureOverlayHeight(convHeaderPos);
+ final int convHeaderPx = measureOverlayHeight(convHeaderPos);
- mTemplates.startConversation(convHeaderDp);
+ mTemplates.startConversation(mWebView.screenPxToWebPx(convHeaderPx));
int collapsedStart = -1;
Message prevCollapsedMsg = null;
@@ -521,13 +526,13 @@
mWebView.getSettings().setBlockNetworkImage(!allowNetworkImages);
- return mTemplates.endConversation(mBaseUri, 320);
+ return mTemplates.endConversation(mBaseUri, 320, mWebView.getViewportWidth());
}
private void renderSuperCollapsedBlock(int start, int end) {
final int blockPos = mAdapter.addSuperCollapsedBlock(start, end);
- final int blockDp = measureOverlayHeight(blockPos);
- mTemplates.appendSuperCollapsedHtml(start, blockDp);
+ final int blockPx = measureOverlayHeight(blockPos);
+ mTemplates.appendSuperCollapsedHtml(start, mWebView.screenPxToWebPx(blockPx));
}
private void renderMessage(Message msg, boolean expanded, boolean safeForImages) {
@@ -539,11 +544,11 @@
// Measure item header and footer heights to allocate spacers in HTML
// But since the views themselves don't exist yet, render each item temporarily into
// a host view for measurement.
- final int headerDp = measureOverlayHeight(headerPos);
- final int footerDp = measureOverlayHeight(footerPos);
+ final int headerPx = measureOverlayHeight(headerPos);
+ final int footerPx = measureOverlayHeight(footerPos);
- mTemplates.appendMessageHtml(msg, expanded, safeForImages, 1.0f, headerDp,
- footerDp);
+ mTemplates.appendMessageHtml(msg, expanded, safeForImages, 1.0f,
+ mWebView.screenPxToWebPx(headerPx), mWebView.screenPxToWebPx(footerPx));
}
private String renderCollapsedHeaders(MessageCursor cursor,
@@ -559,11 +564,11 @@
false /* expanded */);
final MessageFooterItem footer = mAdapter.newMessageFooterItem(header);
- final int headerDp = measureOverlayHeight(header);
- final int footerDp = measureOverlayHeight(footer);
+ final int headerPx = measureOverlayHeight(header);
+ final int footerPx = measureOverlayHeight(footer);
mTemplates.appendMessageHtml(msg, false /* expanded */, msg.alwaysShowImages, 1.0f,
- headerDp, footerDp);
+ mWebView.screenPxToWebPx(headerPx), mWebView.screenPxToWebPx(footerPx));
replacements.add(header);
replacements.add(footer);
}
@@ -587,7 +592,7 @@
* {@link ConversationOverlayItem} for later use in overlay positioning.
*
* @param convItem adapter item with data to render and measure
- * @return height in dp of the rendered view
+ * @return height of the rendered view in screen px
*/
private int measureOverlayHeight(ConversationOverlayItem convItem) {
final int type = convItem.getType();
@@ -602,7 +607,7 @@
convItem.setHeight(heightPx);
convItem.markMeasurementValid();
- return (int) (heightPx / mDensity);
+ return heightPx;
}
private void onConversationSeen() {
@@ -661,10 +666,11 @@
mConversationContainer.invalidateSpacerGeometry();
// update message HTML spacer height
- LogUtils.i(LAYOUT_TAG, "setting HTML spacer h=%dpx", newSpacerHeightPx);
- final int heightDp = (int) (newSpacerHeightPx / mDensity);
+ final int h = mWebView.screenPxToWebPx(newSpacerHeightPx);
+ LogUtils.i(LAYOUT_TAG, "setting HTML spacer h=%dwebPx (%dscreenPx)", h,
+ newSpacerHeightPx);
mWebView.loadUrl(String.format("javascript:setMessageHeaderSpacerHeight('%s', %d);",
- mTemplates.getMessageDomId(item.message), heightDp));
+ mTemplates.getMessageDomId(item.message), h));
}
@Override
@@ -672,11 +678,11 @@
mConversationContainer.invalidateSpacerGeometry();
// show/hide the HTML message body and update the spacer height
- LogUtils.i(LAYOUT_TAG, "setting HTML spacer expanded=%s h=%dpx", item.isExpanded(),
- newSpacerHeightPx);
- final int heightDp = (int) (newSpacerHeightPx / mDensity);
+ final int h = mWebView.screenPxToWebPx(newSpacerHeightPx);
+ LogUtils.i(LAYOUT_TAG, "setting HTML spacer expanded=%s h=%dwebPx (%dscreenPx)",
+ item.isExpanded(), h, newSpacerHeightPx);
mWebView.loadUrl(String.format("javascript:setMessageBodyVisible('%s', %s, %d);",
- mTemplates.getMessageDomId(item.message), item.isExpanded(), heightDp));
+ mTemplates.getMessageDomId(item.message), item.isExpanded(), h));
}
@Override
diff --git a/src/com/android/mail/ui/HtmlConversationTemplates.java b/src/com/android/mail/ui/HtmlConversationTemplates.java
index a440004..002bed4 100644
--- a/src/com/android/mail/ui/HtmlConversationTemplates.java
+++ b/src/com/android/mail/ui/HtmlConversationTemplates.java
@@ -187,13 +187,13 @@
mInProgress = true;
}
- public String endConversation(String baseUri, int viewWidth) {
+ public String endConversation(String baseUri, int viewWidth, int viewportWidth) {
if (!mInProgress) {
throw new IllegalStateException("must call startConversation first");
}
append(sConversationLower, mContext.getString(R.string.hide_elided),
- mContext.getString(R.string.show_elided), baseUri, viewWidth);
+ mContext.getString(R.string.show_elided), baseUri, viewWidth, viewportWidth);
mInProgress = false;