Bug 1508372 - Add scrollTo and scrollBy to PanZoomController r=geckoview-reviewers,snorp,esawin

The scrollTo() and scrollBy() functions in the PanZoomController may be
used to scroll the root document in GeckoView.

Differential Revision: https://phabricator.services.mozilla.com/D18898

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Randall Barker 2019-02-14 19:04:06 +00:00
parent 773b9d611a
commit ed2e78aef0
9 changed files with 441 additions and 1 deletions

View File

@ -7,6 +7,17 @@ const {GeckoViewChildModule} = ChromeUtils.import("resource://gre/modules/GeckoV
var {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
// This needs to match ScreenLength.java
const SCREEN_LENGTH_TYPE_PIXEL = 0;
const SCREEN_LENGTH_TYPE_VIEWPORT_WIDTH = 1;
const SCREEN_LENGTH_TYPE_VIEWPORT_HEIGHT = 2;
const SCREEN_LENGTH_DOCUMENT_WIDTH = 3;
const SCREEN_LENGTH_DOCUMENT_HEIGHT = 4;
// This need to match PanZoomController.java
const SCROLL_BEHAVIOR_SMOOTH = 0;
const SCROLL_BEHAVIOR_AUTO = 1;
XPCOMUtils.defineLazyModuleGetters(this, {
FormLikeFactory: "resource://gre/modules/FormLikeFactory.jsm",
GeckoViewAutoFill: "resource://gre/modules/GeckoViewAutoFill.jsm",
@ -33,6 +44,8 @@ class GeckoViewContentChild extends GeckoViewChildModule {
this.messageManager.addMessageListener("GeckoView:SetActive", this);
this.messageManager.addMessageListener("GeckoView:UpdateInitData", this);
this.messageManager.addMessageListener("GeckoView:ZoomToInput", this);
this.messageManager.addMessageListener("GeckoView:ScrollBy", this);
this.messageManager.addMessageListener("GeckoView:ScrollTo", this);
const options = {
mozSystemGroup: true,
@ -109,6 +122,31 @@ class GeckoViewContentChild extends GeckoViewChildModule {
return {history, formdata, scrolldata};
}
toPixels(aLength, aType) {
if (aType === SCREEN_LENGTH_TYPE_PIXEL) {
return aLength;
} else if (aType === SCREEN_LENGTH_TYPE_VIEWPORT_WIDTH) {
return aLength * content.innerWidth;
} else if (aType === SCREEN_LENGTH_TYPE_VIEWPORT_HEIGHT) {
return aLength * content.innerHeight;
} else if (aType === SCREEN_LENGTH_DOCUMENT_WIDTH) {
return aLength * content.document.body.scrollWidth;
} else if (aType === SCREEN_LENGTH_DOCUMENT_HEIGHT) {
return aLength * content.document.body.scrollHeight;
}
return aLength;
}
toScrollBehavior(aBehavior) {
if (aBehavior === SCROLL_BEHAVIOR_SMOOTH) {
return "smooth";
} else if (aBehavior === SCROLL_BEHAVIOR_AUTO) {
return "auto";
}
return "smooth";
}
receiveMessage(aMsg) {
debug `receiveMessage: ${aMsg.name}`;
@ -246,6 +284,20 @@ class GeckoViewContentChild extends GeckoViewChildModule {
Services.obs.notifyObservers(
docShell, "geckoview-content-global-transferred");
break;
case "GeckoView:ScrollBy":
content.scrollBy({
top: this.toPixels(aMsg.data.heightValue, aMsg.data.heightType),
left: this.toPixels(aMsg.data.widthValue, aMsg.data.widthType),
behavior: this.toScrollBehavior(aMsg.data.behavior),
});
break;
case "GeckoView:ScrollTo":
content.scrollTo({
top: this.toPixels(aMsg.data.heightValue, aMsg.data.heightType),
left: this.toPixels(aMsg.data.widthValue, aMsg.data.widthType),
behavior: this.toScrollBehavior(aMsg.data.behavior),
});
break;
}
}

View File

@ -832,8 +832,16 @@ package org.mozilla.geckoview {
method public boolean onMotionEvent(@android.support.annotation.NonNull android.view.MotionEvent);
method public boolean onMouseEvent(@android.support.annotation.NonNull android.view.MotionEvent);
method public boolean onTouchEvent(@android.support.annotation.NonNull android.view.MotionEvent);
method @android.support.annotation.UiThread public void scrollBy(@android.support.annotation.NonNull org.mozilla.geckoview.ScreenLength, @android.support.annotation.NonNull org.mozilla.geckoview.ScreenLength);
method @android.support.annotation.UiThread public void scrollBy(@android.support.annotation.NonNull org.mozilla.geckoview.ScreenLength, @android.support.annotation.NonNull org.mozilla.geckoview.ScreenLength, int);
method @android.support.annotation.UiThread public void scrollTo(@android.support.annotation.NonNull org.mozilla.geckoview.ScreenLength, @android.support.annotation.NonNull org.mozilla.geckoview.ScreenLength);
method @android.support.annotation.UiThread public void scrollTo(@android.support.annotation.NonNull org.mozilla.geckoview.ScreenLength, @android.support.annotation.NonNull org.mozilla.geckoview.ScreenLength, int);
method @android.support.annotation.UiThread public void scrollToBottom();
method @android.support.annotation.UiThread public void scrollToTop();
method public void setIsLongpressEnabled(boolean);
method public void setScrollFactor(float);
field public static final int SCROLL_BEHAVIOR_AUTO = 1;
field public static final int SCROLL_BEHAVIOR_SMOOTH = 0;
}
public abstract class RuntimeSettings implements android.os.Parcelable {
@ -854,6 +862,22 @@ package org.mozilla.geckoview {
method @android.support.annotation.AnyThread @android.support.annotation.NonNull public org.mozilla.geckoview.GeckoResult<org.mozilla.gecko.util.GeckoBundle> getSnapshots(boolean);
}
public class ScreenLength {
method @android.support.annotation.NonNull @android.support.annotation.AnyThread public static org.mozilla.geckoview.ScreenLength bottom();
method @android.support.annotation.NonNull @android.support.annotation.AnyThread public static org.mozilla.geckoview.ScreenLength fromPixels(double);
method @android.support.annotation.NonNull @android.support.annotation.AnyThread public static org.mozilla.geckoview.ScreenLength fromViewportHeight(double);
method @android.support.annotation.NonNull @android.support.annotation.AnyThread public static org.mozilla.geckoview.ScreenLength fromViewportWidth(double);
method @android.support.annotation.AnyThread public int getType();
method @android.support.annotation.AnyThread public double getValue();
method @android.support.annotation.NonNull @android.support.annotation.AnyThread public static org.mozilla.geckoview.ScreenLength top();
method @android.support.annotation.NonNull @android.support.annotation.AnyThread public static org.mozilla.geckoview.ScreenLength zero();
field public static final int DOCUMENT_HEIGHT = 4;
field public static final int DOCUMENT_WIDTH = 3;
field public static final int PIXEL = 0;
field public static final int VIEWPORT_HEIGHT = 2;
field public static final int VIEWPORT_WIDTH = 1;
}
@android.support.annotation.UiThread public class SessionAccessibility {
method @android.support.annotation.Nullable public android.view.View getView();
method public boolean onMotionEvent(@android.support.annotation.NonNull android.view.MotionEvent);

View File

@ -0,0 +1,35 @@
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style type="text/css">
body {
background-color: white;
margin: 0;
}
#one {
background-color: red;
width: 200vw;
height: 100vh;
}
#two {
background-color: green;
width: 200vw;
height: 100vh;
}
#three {
background-color: blue;
width: 200vw;
height: 100vh;
}
</style>
</head>
<body>
<div id="one"></div>
<div id="two"></div>
<div id="three"></div>
</body>
</html>

View File

@ -52,6 +52,7 @@ open class BaseSessionTest(noErrorCollector: Boolean = false) {
const val IFRAME_REDIRECT_LOCAL = "/assets/www/iframe_redirect_local.html"
const val IFRAME_REDIRECT_AUTOMATION = "/assets/www/iframe_redirect_automation.html"
const val AUTOPLAY_PATH = "/assets/www/autoplay.html"
const val SCROLL_TEST_PATH = "/assets/www/scroll.html"
}
@get:Rule val sessionRule = GeckoSessionTestRule()

View File

@ -0,0 +1,64 @@
package org.mozilla.geckoview.test
import org.mozilla.geckoview.ScreenLength
import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.WithDevToolsAPI
import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.WithDisplay
import android.support.test.filters.MediumTest
import android.support.test.runner.AndroidJUnit4
import org.hamcrest.Matchers.*
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.geckoview.PanZoomController
@RunWith(AndroidJUnit4::class)
@MediumTest
class PanZoomControllerTest : BaseSessionTest() {
private val errorEpsilon = 3.0
@WithDevToolsAPI
@WithDisplay(width = 100, height = 100)
@Test
fun scrollBy() {
sessionRule.session.loadTestPath(SCROLL_TEST_PATH)
sessionRule.waitForPageStop()
val vh = sessionRule.evaluateJS(mainSession, "window.innerHeight") as Double
assertThat("Viewport height is not zero", vh, greaterThan(0.0))
sessionRule.session.panZoomController.scrollBy(ScreenLength.zero(), ScreenLength.fromViewportHeight(1.0), PanZoomController.SCROLL_BEHAVIOR_AUTO)
val scrollY = sessionRule.evaluateJS(mainSession, "window.scrollY") as Double
assertThat("scrollBy should have scrolled along y axis one viewport", scrollY, closeTo(vh, errorEpsilon))
sessionRule.session.loadTestPath(SCROLL_TEST_PATH)
sessionRule.waitForPageStop()
val vw = sessionRule.evaluateJS(mainSession, "window.innerWidth") as Double
assertThat("Viewport width is not zero", vw, greaterThan(0.0))
sessionRule.session.panZoomController.scrollBy(ScreenLength.fromViewportWidth(1.0), ScreenLength.zero(), PanZoomController.SCROLL_BEHAVIOR_AUTO)
val scrollX = sessionRule.evaluateJS(mainSession, "window.scrollX") as Double
assertThat("scrollBy should have scrolled along x axis one viewport", scrollX, closeTo(vw, errorEpsilon))
}
@WithDevToolsAPI
@WithDisplay(width = 100, height = 100)
@Test
fun scrollTo() {
sessionRule.session.loadTestPath(SCROLL_TEST_PATH)
sessionRule.waitForPageStop()
val vh = sessionRule.evaluateJS(mainSession, "window.innerHeight") as Double
val scrollHeight = sessionRule.evaluateJS(mainSession, "window.document.body.scrollHeight") as Double
assertThat("Viewport height is not zero", vh, greaterThan(0.0))
assertThat("scrollHeight height is not zero", scrollHeight, greaterThan(0.0))
sessionRule.session.panZoomController.scrollTo(ScreenLength.zero(), ScreenLength.bottom(), PanZoomController.SCROLL_BEHAVIOR_AUTO)
var scrollY = sessionRule.evaluateJS(mainSession, "window.scrollY") as Double
assertThat("scrollTo should have scrolled to bottom", scrollY, closeTo(scrollHeight - vh, 3.0))
sessionRule.session.panZoomController.scrollTo(ScreenLength.zero(), ScreenLength.top(), PanZoomController.SCROLL_BEHAVIOR_AUTO)
scrollY = sessionRule.evaluateJS(mainSession, "window.scrollY") as Double
assertThat("scrollTo should have scrolled to top", scrollY, closeTo(0.0, errorEpsilon))
val vw = sessionRule.evaluateJS(mainSession, "window.innerWidth") as Double
val scrollWidth = sessionRule.evaluateJS(mainSession, "window.document.body.scrollWidth") as Double
sessionRule.session.panZoomController.scrollTo(ScreenLength.fromViewportWidth(1.0), ScreenLength.zero(), PanZoomController.SCROLL_BEHAVIOR_AUTO)
val scrollX = sessionRule.evaluateJS(mainSession, "window.scrollX") as Double
assertThat("scrollTo should have scrolled to right", scrollX, closeTo(scrollWidth - vw, errorEpsilon))
}
}

View File

@ -7,17 +7,22 @@ package org.mozilla.geckoview;
import org.mozilla.gecko.annotation.WrapForJNI;
import org.mozilla.gecko.mozglue.JNIObject;
import org.mozilla.gecko.util.GeckoBundle;
import org.mozilla.gecko.util.ThreadUtils;
import android.graphics.Rect;
import android.os.SystemClock;
import android.support.annotation.NonNull;
import android.support.annotation.UiThread;
import android.support.annotation.IntDef;
import android.util.Log;
import android.util.Pair;
import android.view.MotionEvent;
import android.view.InputDevice;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
@UiThread
@ -33,6 +38,19 @@ public class PanZoomController extends JNIObject {
private float mPointerScrollFactor = 64.0f;
private long mLastDownTime;
@Retention(RetentionPolicy.SOURCE)
@IntDef({SCROLL_BEHAVIOR_SMOOTH, SCROLL_BEHAVIOR_AUTO})
/* package */ @interface ScrollBehaviorType {}
/**
* Specifies smooth scrolling which animates content to the desired scroll position.
*/
public static final int SCROLL_BEHAVIOR_SMOOTH = 0;
/**
* Specifies auto scrolling which jumps content to the desired scroll position.
*/
public static final int SCROLL_BEHAVIOR_AUTO = 1;
private SynthesizedEventState mPointerState;
private ArrayList<Pair<Integer, MotionEvent>> mQueuedEvents;
@ -530,4 +548,84 @@ public class PanZoomController extends JNIObject {
PointerInfo.RESERVED_MOUSE_POINTER_ID,
eventType, clientX, clientY, 0, 0);
}
/**
* Scroll the document body by an offset from the current scroll position.
* Uses {@link #SCROLL_BEHAVIOR_SMOOTH}.
*
* @param width {@link ScreenLength} offset to scroll along X axis.
* @param height {@link ScreenLength} offset to scroll along Y axis.
*/
@UiThread
public void scrollBy(@NonNull ScreenLength width, @NonNull ScreenLength height) {
scrollBy(width, height, SCROLL_BEHAVIOR_SMOOTH);
}
/**
* Scroll the document body by an offset from the current scroll position.
*
* @param width {@link ScreenLength} offset to scroll along X axis.
* @param height {@link ScreenLength} offset to scroll along Y axis.
* @param behavior ScrollBehaviorType One of {@link #SCROLL_BEHAVIOR_SMOOTH}, {@link #SCROLL_BEHAVIOR_AUTO},
* that specifies how to scroll the content.
*/
@UiThread
public void scrollBy(@NonNull ScreenLength width, @NonNull ScreenLength height, @ScrollBehaviorType int behavior) {
final GeckoBundle msg = buildScrollMessage(width, height, behavior);
mSession.getEventDispatcher().dispatch("GeckoView:ScrollBy", msg);
}
/**
* Scroll the document body to an absolute position.
* Uses {@link #SCROLL_BEHAVIOR_SMOOTH}.
*
* @param width {@link ScreenLength} position to scroll along X axis.
* @param height {@link ScreenLength} position to scroll along Y axis.
*/
@UiThread
public void scrollTo(@NonNull ScreenLength width, @NonNull ScreenLength height) {
scrollTo(width, height, SCROLL_BEHAVIOR_SMOOTH);
}
/**
* Scroll the document body to an absolute position.
*
* @param width {@link ScreenLength} position to scroll along X axis.
* @param height {@link ScreenLength} position to scroll along Y axis.
* @param behavior ScrollBehaviorType One of {@link #SCROLL_BEHAVIOR_SMOOTH}, {@link #SCROLL_BEHAVIOR_AUTO},
* that specifies how to scroll the content.
*/
@UiThread
public void scrollTo(@NonNull ScreenLength width, @NonNull ScreenLength height, @ScrollBehaviorType int behavior) {
final GeckoBundle msg = buildScrollMessage(width, height, behavior);
mSession.getEventDispatcher().dispatch("GeckoView:ScrollTo", msg);
}
/**
* Scroll to the top left corner of the screen.
* Uses {@link #SCROLL_BEHAVIOR_SMOOTH}.
*/
@UiThread
public void scrollToTop() {
scrollTo(ScreenLength.zero(), ScreenLength.top(), SCROLL_BEHAVIOR_SMOOTH);
}
/**
* Scroll to the bottom left corner of the screen.
* Uses {@link #SCROLL_BEHAVIOR_SMOOTH}.
*/
@UiThread
public void scrollToBottom() {
scrollTo(ScreenLength.zero(), ScreenLength.bottom(), SCROLL_BEHAVIOR_SMOOTH);
}
private GeckoBundle buildScrollMessage(@NonNull ScreenLength width, @NonNull ScreenLength height, @ScrollBehaviorType int behavior) {
final GeckoBundle msg = new GeckoBundle();
msg.putDouble("widthValue", width.getValue());
msg.putInt("widthType", width.getType());
msg.putDouble("heightValue", height.getValue());
msg.putInt("heightType", height.getType());
msg.putInt("behavior", behavior);
return msg;
}
}

View File

@ -0,0 +1,156 @@
/* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.geckoview;
import android.support.annotation.AnyThread;
import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* ScreenLength is a class that represents a length on the screen using different units.
* The default unit is a pixel. However lengths may be also represented by a dimension
* of the visual viewport or of the full scroll size of the root document.
*/
public class ScreenLength {
@Retention(RetentionPolicy.SOURCE)
@IntDef({PIXEL, VIEWPORT_WIDTH, VIEWPORT_HEIGHT, DOCUMENT_WIDTH, DOCUMENT_HEIGHT})
/* package */ @interface ScreenLengthType {}
/**
* Pixel units.
*/
public static final int PIXEL = 0;
/**
* Units are in visual viewport width. If the visual viewport is 100 pixels wide, then a value
* of 2.0 would represent a length of 200 pixels.
* @see <a href="https://developer.mozilla.org/en-US/docs/Glossary/Visual_Viewport">MDN Visual Viewport</a>
*/
public static final int VIEWPORT_WIDTH = 1;
/**
* Units are in visual viewport height. If the visual viewport is 100 pixels high, then a value
* of 2.0 would represent a length of 200 pixels.
* @see <a href="https://developer.mozilla.org/en-US/docs/Glossary/Visual_Viewport">MDN Visual Viewport</a>
*/
public static final int VIEWPORT_HEIGHT = 2;
/**
* Units represent the entire scrollable documents width. If the document is 1000 pixels wide
* then a value of 1.0 would represent 1000 pixels.
*/
public static final int DOCUMENT_WIDTH = 3;
/**
* Units represent the entire scrollable documents height. If the document is 1000 pixels tall
* then a value of 1.0 would represent 1000 pixels.
*/
public static final int DOCUMENT_HEIGHT = 4;
/**
* Create a ScreenLength of zero pixels length.
* Type is {@link #PIXEL}.
* @return ScreenLength of zero length.
*/
@NonNull
@AnyThread
public static ScreenLength zero() {
return new ScreenLength(0.0, PIXEL);
}
/**
* Create a ScreenLength of zero pixels length.
* Type is {@link #PIXEL}. Can be used to scroll to the top of a page when used with
* PanZoomController.scrollTo()
* @return ScreenLength of zero length.
*/
@NonNull
@AnyThread
public static ScreenLength top() {
return zero();
}
/**
* Create a ScreenLength of the documents height.
* Type is {@link #DOCUMENT_HEIGHT}. Can be used to scroll to the bottom of a page when used with
* {@link PanZoomController#scrollTo(ScreenLength, ScreenLength)}
* @return ScreenLength of document height.
*/
@NonNull
@AnyThread
public static ScreenLength bottom() {
return new ScreenLength(1.0, DOCUMENT_HEIGHT);
}
/**
* Create a ScreenLength of a specific length.
* Type is {@link #PIXEL}.
* @param value Pixel length.
* @return ScreenLength of document height.
*/
@NonNull
@AnyThread
public static ScreenLength fromPixels(final double value) {
return new ScreenLength(value, PIXEL);
}
/**
* Create a ScreenLength that uses the visual viewport width as units.
* Type is {@link #VIEWPORT_WIDTH}. Can be used with {@link PanZoomController#scrollBy(ScreenLength, ScreenLength)}
* to scroll a value of the width of visual viewport content.
* @param value Factor used to calculate length. A value of 2.0 would indicate a length
* twice as long as the length of the visual viewports width.
* @return ScreenLength of specifying a length of value * visual viewport width.
*/
@NonNull
@AnyThread
public static ScreenLength fromViewportWidth(final double value) {
return new ScreenLength(value, VIEWPORT_WIDTH);
}
/**
* Create a ScreenLength that uses the visual viewport width as units.
* Type is {@link #VIEWPORT_HEIGHT}. Can be used with {@link PanZoomController#scrollBy(ScreenLength, ScreenLength)}
* to scroll a value of the height of visual viewport content.
* @param value Factor used to calculate length. A value of 2.0 would indicate a length
* twice as long as the length of the visual viewports height.
* @return ScreenLength of specifying a length of value * visual viewport width.
*/
@NonNull
@AnyThread
public static ScreenLength fromViewportHeight(final double value) {
return new ScreenLength(value, VIEWPORT_HEIGHT);
}
private final double mValue;
@ScreenLengthType private final int mType;
/* package */ ScreenLength(final double value, @ScreenLengthType final int type) {
mValue = value;
mType = type;
}
/**
* Returns the scalar value used to calculate length.
* The units of the returned valued are defined by what is returned by
* {@link #getType()}
* @return Scalar value of the length.
*/
@AnyThread
public double getValue() {
return mValue;
}
/**
* Returns the unit type of the length
* The length can be one of the following:
* {@link #PIXEL}, {@link #VIEWPORT_WIDTH}, {@link #VIEWPORT_HEIGHT}, {@link #DOCUMENT_WIDTH}, {@link #DOCUMENT_HEIGHT}
* @return Unit type of the length.
*/
@AnyThread
@ScreenLengthType
public int getType() {
return mType;
}
}

View File

@ -23,6 +23,8 @@ exclude: true
- Added `baseUri` to [`ContentDelegate.ContextElement`][65.21] and changed
`linkUri` to absolute form.
- Added `scrollBy()` and `scrollTo()` to `PanZoomController`.
## v66
- Removed redundant field `GeckoSession.ProgressDelegate.SecurityInformation.trackingMode`.
Use `GeckoSession.TrackingProtectionDelegate.onTrackerBlocked` for
@ -132,4 +134,4 @@ exclude: true
[65.24]: ../CrashReporter.html#sendCrashReport-android.content.Context-android.os.Bundle-java.lang.String-
[65.25]: ../GeckoResult.html
[api-version]: a42a6f4481dd690ac46f14d8e692785bb00e8b04
[api-version]: fb3375bbe85695b337d830dcf3fffec7231f4e4b

View File

@ -25,6 +25,8 @@ class GeckoViewContent extends GeckoViewModule {
"GeckoView:SetActive",
"GeckoView:SetFocused",
"GeckoView:ZoomToInput",
"GeckoView:ScrollBy",
"GeckoView:ScrollTo",
]);
this.messageManager.addMessageListener("GeckoView:SaveStateFinish", this);
@ -77,6 +79,12 @@ class GeckoViewContent extends GeckoViewModule {
case "GeckoView:ZoomToInput":
this.messageManager.sendAsyncMessage(aEvent);
break;
case "GeckoView:ScrollBy":
this.messageManager.sendAsyncMessage(aEvent, aData);
break;
case "GeckoView:ScrollTo":
this.messageManager.sendAsyncMessage(aEvent, aData);
break;
case "GeckoView:SetActive":
if (aData.active) {
this.browser.docShellIsActive = true;