Commit 6349c066 authored by wychen's avatar wychen Committed by Commit bot

Changing font size with pinch gesture in Reader Mode

When users pinch in Reader Mode, the page would zoom in or out as if it
is a normal web page allowing user-zoom. At the end of pinch gesture, the
page would do text reflow. These pinch-to-zoom and text reflow effects
are not native, but are emulated using CSS and JavaScript.

In order to achieve near-native zooming and panning frame rate, fake 3D
transform is used so that the layer doesn't repaint for each frame.

After the text reflow, the web content shown in the viewport should
roughly be the same paragraph before zooming.

The control point of font size is the html element, so that both "em" and
"rem" are adjusted. Accordingly, font size of body is no longer specified
in CSS in unit of pixel.

Some CSS styles and animations are updated to fix issues specific to
resizing.

BUG=445632

Review URL: https://codereview.chromium.org/1009703002

Cr-Commit-Position: refs/heads/master@{#326945}
parent 73dfc1c5
...@@ -56,6 +56,7 @@ ...@@ -56,6 +56,7 @@
'variables': { 'variables': {
'files': [ 'files': [
'test/data/', 'test/data/',
'dom_distiller/core/javascript/',
'../third_party/dom_distiller_js/dist/test/data/', '../third_party/dom_distiller_js/dist/test/data/',
], ],
}, },
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
#include "base/memory/weak_ptr.h" #include "base/memory/weak_ptr.h"
#include "base/path_service.h" #include "base/path_service.h"
#include "base/run_loop.h" #include "base/run_loop.h"
#include "base/strings/utf_string_conversions.h"
#include "base/values.h" #include "base/values.h"
#include "components/dom_distiller/content/distiller_page_web_contents.h" #include "components/dom_distiller/content/distiller_page_web_contents.h"
#include "components/dom_distiller/content/web_contents_main_frame_observer.h" #include "components/dom_distiller/content/web_contents_main_frame_observer.h"
...@@ -100,6 +101,11 @@ class DistillerPageWebContentsTest : public ContentBrowserTest { ...@@ -100,6 +101,11 @@ class DistillerPageWebContentsTest : public ContentBrowserTest {
quit_closure_.Run(); quit_closure_.Run();
} }
void OnJsExecutionDone(base::Closure callback, const base::Value* value) {
js_result_.reset(value->DeepCopy());
callback.Run();
}
private: private:
void AddComponentsResources() { void AddComponentsResources() {
base::FilePath pak_file; base::FilePath pak_file;
...@@ -119,8 +125,10 @@ class DistillerPageWebContentsTest : public ContentBrowserTest { ...@@ -119,8 +125,10 @@ class DistillerPageWebContentsTest : public ContentBrowserTest {
void SetUpTestServer() { void SetUpTestServer() {
base::FilePath path; base::FilePath path;
PathService::Get(base::DIR_SOURCE_ROOT, &path); PathService::Get(base::DIR_SOURCE_ROOT, &path);
path = path.AppendASCII("components/test/data/dom_distiller"); embedded_test_server()->ServeFilesFromDirectory(
embedded_test_server()->ServeFilesFromDirectory(path); path.AppendASCII("components/test/data/dom_distiller"));
embedded_test_server()->ServeFilesFromDirectory(
path.AppendASCII("components/dom_distiller/core/javascript"));
ASSERT_TRUE(embedded_test_server()->InitializeAndWaitUntilReady()); ASSERT_TRUE(embedded_test_server()->InitializeAndWaitUntilReady());
} }
...@@ -133,6 +141,7 @@ class DistillerPageWebContentsTest : public ContentBrowserTest { ...@@ -133,6 +141,7 @@ class DistillerPageWebContentsTest : public ContentBrowserTest {
DistillerPageWebContents* distiller_page_; DistillerPageWebContents* distiller_page_;
base::Closure quit_closure_; base::Closure quit_closure_;
scoped_ptr<proto::DomDistillerResult> distiller_result_; scoped_ptr<proto::DomDistillerResult> distiller_result_;
scoped_ptr<base::Value> js_result_;
}; };
// Use this class to be able to leak the WebContents, which is needed for when // Use this class to be able to leak the WebContents, which is needed for when
...@@ -458,4 +467,40 @@ IN_PROC_BROWSER_TEST_F(DistillerPageWebContentsTest, TestTitleNeverEmpty) { ...@@ -458,4 +467,40 @@ IN_PROC_BROWSER_TEST_F(DistillerPageWebContentsTest, TestTitleNeverEmpty) {
} }
} }
IN_PROC_BROWSER_TEST_F(DistillerPageWebContentsTest,
TestPinch) {
// Load the test file in content shell and wait until it has fully loaded.
content::WebContents* web_contents = shell()->web_contents();
dom_distiller::WebContentsMainFrameObserver::CreateForWebContents(
web_contents);
base::RunLoop url_loaded_runner;
WebContentsMainFrameHelper main_frame_loaded(web_contents,
url_loaded_runner.QuitClosure(),
true);
web_contents->GetController().LoadURL(
embedded_test_server()->GetURL("/pinch_tester.html"),
content::Referrer(),
ui::PAGE_TRANSITION_TYPED,
std::string());
url_loaded_runner.Run();
// Execute the JS to run the tests, and wait until it has finished.
base::RunLoop run_loop;
web_contents->GetMainFrame()->ExecuteJavaScript(
base::UTF8ToUTF16("(function() {return pinchtest.run();})();"),
base::Bind(&DistillerPageWebContentsTest::OnJsExecutionDone,
base::Unretained(this), run_loop.QuitClosure()));
run_loop.Run();
// Convert to dictionary and parse the results.
const base::DictionaryValue* dict;
ASSERT_TRUE(js_result_);
ASSERT_TRUE(js_result_->GetAsDictionary(&dict));
ASSERT_TRUE(dict->HasKey("success"));
bool success;
ASSERT_TRUE(dict->GetBoolean("success", &success));
EXPECT_TRUE(success);
}
} // namespace dom_distiller } // namespace dom_distiller
...@@ -73,9 +73,11 @@ th { ...@@ -73,9 +73,11 @@ th {
/* Base typography. */ /* Base typography. */
body,
html { html {
font-size: 14px; font-size: 14px;
}
body {
height: 100%; height: 100%;
line-height: 1.4; line-height: 1.4;
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;
...@@ -154,11 +156,11 @@ h6 { ...@@ -154,11 +156,11 @@ h6 {
/* Margins for Show Original link. */ /* Margins for Show Original link. */
#showOriginal { #showOriginal {
margin: auto 1.296rem 1.296rem 1.296rem; margin: auto 1.296rem 1.296rem 5%;
} }
#content { #content {
margin: 0.2rem; margin: 0.2rem 2.2%;
} }
/* Main margins. */ /* Main margins. */
...@@ -340,6 +342,7 @@ pre { ...@@ -340,6 +342,7 @@ pre {
} }
.feedbackContent { .feedbackContent {
font-size: 14px;
background-color: #4285F4; background-color: #4285F4;
clear: both; clear: both;
padding: 14px; padding: 14px;
......
...@@ -163,3 +163,209 @@ document.getElementById('feedbackContainer').addEventListener('animationend', ...@@ -163,3 +163,209 @@ document.getElementById('feedbackContainer').addEventListener('animationend',
contentWrap.style.paddingBottom = '0px'; contentWrap.style.paddingBottom = '0px';
}, true); }, true);
document.getElementById('contentWrap').addEventListener('transitionend',
function(e) {
var contentWrap = document.getElementById('contentWrap');
contentWrap.style.transition = '';
}, true);
var pincher = (function() {
'use strict';
// When users pinch in Reader Mode, the page would zoom in or out as if it
// is a normal web page allowing user-zoom. At the end of pinch gesture, the
// page would do text reflow. These pinch-to-zoom and text reflow effects
// are not native, but are emulated using CSS and JavaScript.
//
// In order to achieve near-native zooming and panning frame rate, fake 3D
// transform is used so that the layer doesn't repaint for each frame.
//
// After the text reflow, the web content shown in the viewport should
// roughly be the same paragraph before zooming.
//
// The control point of font size is the html element, so that both "em" and
// "rem" are adjusted.
//
// TODO(wychen): Improve scroll position when elementFromPoint is body.
var pinching = false;
var fontSizeAnchor = 1.0;
var focusElement = null;
var focusPos = 0;
var initClientMid;
var clampedScale = 1;
var lastSpan;
var lastClientMid;
var scale = 1;
var shiftX;
var shiftY;
// The zooming speed relative to pinching speed.
const FONT_SCALE_MULTIPLIER = 0.5;
const MIN_SPAN_LENGTH = 20;
// The font size is guaranteed to be in px.
var baseSize =
parseFloat(getComputedStyle(document.documentElement).fontSize);
var refreshTransform = function() {
var slowedScale = Math.exp(Math.log(scale) * FONT_SCALE_MULTIPLIER);
clampedScale = Math.max(0.4, Math.min(2.5, fontSizeAnchor * slowedScale));
// Use "fake" 3D transform so that the layer is not repainted.
// With 2D transform, the frame rate would be much lower.
document.body.style.transform =
'translate3d(' + shiftX + 'px,' +
shiftY + 'px, 0px)' +
'scale(' + clampedScale/fontSizeAnchor + ')';
};
function endPinch() {
pinching = false;
document.body.style.transformOrigin = '';
document.body.style.transform = '';
document.documentElement.style.fontSize = clampedScale * baseSize + "px";
var rect = focusElement.getBoundingClientRect();
var targetTop = focusPos * (rect.bottom - rect.top) + rect.top +
document.body.scrollTop - (initClientMid.y + shiftY);
document.body.scrollTop = targetTop;
}
function touchSpan(e) {
var count = e.touches.length;
var mid = touchClientMid(e);
var sum = 0;
for (var i = 0; i < count; i++) {
var dx = (e.touches[i].clientX - mid.x);
var dy = (e.touches[i].clientY - mid.y);
sum += Math.hypot(dx, dy);
}
// Avoid very small span.
return Math.max(MIN_SPAN_LENGTH, sum/count);
}
function touchClientMid(e) {
var count = e.touches.length;
var sumX = 0;
var sumY = 0;
for (var i = 0; i < count; i++) {
sumX += e.touches[i].clientX;
sumY += e.touches[i].clientY;
}
return {x: sumX/count, y: sumY/count};
}
function touchPageMid(e) {
var clientMid = touchClientMid(e);
return {x: clientMid.x - e.touches[0].clientX + e.touches[0].pageX,
y: clientMid.y - e.touches[0].clientY + e.touches[0].pageY};
}
return {
handleTouchStart: function(e) {
if (e.touches.length < 2) return;
e.preventDefault();
var span = touchSpan(e);
var clientMid = touchClientMid(e);
if (e.touches.length > 2) {
lastSpan = span;
lastClientMid = clientMid;
refreshTransform();
return;
}
scale = 1;
shiftX = 0;
shiftY = 0;
pinching = true;
fontSizeAnchor =
parseFloat(getComputedStyle(document.documentElement).fontSize) /
baseSize;
var pinchOrigin = touchPageMid(e);
document.body.style.transformOrigin =
pinchOrigin.x + 'px ' + pinchOrigin.y + 'px';
// Try to preserve the pinching center after text reflow.
// This is accurate to the HTML element level.
focusElement = document.elementFromPoint(clientMid.x, clientMid.y);
var rect = focusElement.getBoundingClientRect();
initClientMid = clientMid;
focusPos = (initClientMid.y - rect.top) / (rect.bottom - rect.top);
lastSpan = span;
lastClientMid = clientMid;
refreshTransform();
},
handleTouchMove: function(e) {
if (!pinching) return;
if (e.touches.length < 2) return;
e.preventDefault();
var span = touchSpan(e);
var clientMid = touchClientMid(e);
scale *= touchSpan(e) / lastSpan;
shiftX += clientMid.x - lastClientMid.x;
shiftY += clientMid.y - lastClientMid.y;
refreshTransform();
lastSpan = span;
lastClientMid = clientMid;
},
handleTouchEnd: function(e) {
if (!pinching) return;
e.preventDefault();
var span = touchSpan(e);
var clientMid = touchClientMid(e);
if (e.touches.length >= 2) {
lastSpan = span;
lastClientMid = clientMid;
refreshTransform();
return;
}
endPinch();
},
handleTouchCancel: function(e) {
endPinch();
},
reset: function() {
scale = 1;
shiftX = 0;
shiftY = 0;
clampedScale = 1;
document.documentElement.style.fontSize = clampedScale * baseSize + "px";
},
status: function() {
return {
scale: scale,
clampedScale: clampedScale,
shiftX: shiftX,
shiftY: shiftY
};
}
};
}());
window.addEventListener('touchstart', pincher.handleTouchStart, false);
window.addEventListener('touchmove', pincher.handleTouchMove, false);
window.addEventListener('touchend', pincher.handleTouchEnd, false);
window.addEventListener('touchcancel', pincher.handleTouchCancel, false);
<html>
<head><title>Test Page for Pinch to Zoom</title></head>
<body>
<div id="contentWrap">
<p id="showOriginal">Original</p>
</div>
<div id="feedbackContainer">
<div id="feedbackYes"></div>
<div id="feedbackNo"></div>
</div>
<script src="dom_distiller_viewer.js"></script>
<script src="pinch_tester.js"></script>
</body>
</html>
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment