summaryrefslogtreecommitdiffstats
path: root/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/OverscrollEdgeEffect.java
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/geckoview/src/main/java/org/mozilla/geckoview/OverscrollEdgeEffect.java')
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/geckoview/OverscrollEdgeEffect.java246
1 files changed, 246 insertions, 0 deletions
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/OverscrollEdgeEffect.java b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/OverscrollEdgeEffect.java
new file mode 100644
index 0000000000..efd8061c98
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/OverscrollEdgeEffect.java
@@ -0,0 +1,246 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * 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.annotation.SuppressLint;
+import android.content.Context;
+import android.graphics.BlendMode;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.Rect;
+import android.os.Build;
+import android.widget.EdgeEffect;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+import java.lang.reflect.Field;
+import org.mozilla.gecko.util.ThreadUtils;
+
+@UiThread
+public final class OverscrollEdgeEffect {
+ // Used to index particular edges in the edges array
+ private static final int TOP = 0;
+ private static final int BOTTOM = 1;
+ private static final int LEFT = 2;
+ private static final int RIGHT = 3;
+
+ /* package */ static final int AXIS_X = 0;
+ /* package */ static final int AXIS_Y = 1;
+
+ // All four edges of the screen
+ private final EdgeEffect[] mEdges = new EdgeEffect[4];
+
+ private GeckoSession mSession;
+ private Runnable mInvalidationCallback;
+ private int mWidth;
+ private int mHeight;
+
+ /* package */ OverscrollEdgeEffect() {}
+
+ private static Field sPaintField;
+
+ @SuppressLint("DiscouragedPrivateApi")
+ private void setBlendMode(final EdgeEffect edgeEffect) {
+ if (Build.VERSION.SDK_INT < 29) {
+ // setBlendMode is only supported on SDK_INT >= 29 and above.
+
+ if (sPaintField == null) {
+ try {
+ sPaintField = EdgeEffect.class.getDeclaredField("mPaint");
+ sPaintField.setAccessible(true);
+ } catch (final NoSuchFieldException e) {
+ // Cannot get the field, nothing we can do here
+ return;
+ }
+ }
+
+ try {
+ final Paint paint = (Paint) sPaintField.get(edgeEffect);
+ final PorterDuffXfermode mode = new PorterDuffXfermode(PorterDuff.Mode.SRC);
+ paint.setXfermode(mode);
+ } catch (final IllegalAccessException ex) {
+ // Nothing we can do
+ }
+
+ return;
+ }
+
+ edgeEffect.setBlendMode(BlendMode.SRC);
+ }
+
+ /**
+ * Set the theme to use for overscroll from a given Context.
+ *
+ * @param context Context to use for the overscroll theme.
+ */
+ public void setTheme(final @NonNull Context context) {
+ ThreadUtils.assertOnUiThread();
+
+ for (int i = 0; i < mEdges.length; i++) {
+ final EdgeEffect edgeEffect = new EdgeEffect(context);
+ if (mWidth != 0 || mHeight != 0) {
+ edgeEffect.setSize(mWidth, mHeight);
+ }
+ setBlendMode(edgeEffect);
+ mEdges[i] = edgeEffect;
+ }
+ }
+
+ /* package */ void setSession(final @Nullable GeckoSession session) {
+ mSession = session;
+ }
+
+ /**
+ * Set a Runnable that acts as a callback to invalidate the overscroll effect (for example, as a
+ * response to user fling for example). The Runnbale should schedule a future call to {@link
+ * #draw(Canvas)} as a result of the invalidation.
+ *
+ * @param runnable Invalidation Runnable.
+ * @see #getInvalidationCallback()
+ */
+ public void setInvalidationCallback(final @Nullable Runnable runnable) {
+ ThreadUtils.assertOnUiThread();
+ mInvalidationCallback = runnable;
+ }
+
+ /**
+ * Get the current invalidatation Runnable.
+ *
+ * @return Invalidation Runnable.
+ * @see #setInvalidationCallback(Runnable)
+ */
+ public @Nullable Runnable getInvalidationCallback() {
+ ThreadUtils.assertOnUiThread();
+ return mInvalidationCallback;
+ }
+
+ /* package */ void setSize(final int width, final int height) {
+ mEdges[LEFT].setSize(height, width);
+ mEdges[RIGHT].setSize(height, width);
+ mEdges[TOP].setSize(width, height);
+ mEdges[BOTTOM].setSize(width, height);
+
+ mWidth = width;
+ mHeight = height;
+ }
+
+ private EdgeEffect getEdgeForAxisAndSide(final int axis, final float side) {
+ if (axis == AXIS_Y) {
+ if (side < 0) {
+ return mEdges[TOP];
+ } else {
+ return mEdges[BOTTOM];
+ }
+ } else {
+ if (side < 0) {
+ return mEdges[LEFT];
+ } else {
+ return mEdges[RIGHT];
+ }
+ }
+ }
+
+ /* package */ void setVelocity(final float velocity, final int axis) {
+ if (velocity == 0.0f) {
+ if (axis == AXIS_Y) {
+ mEdges[TOP].onRelease();
+ mEdges[BOTTOM].onRelease();
+ } else {
+ mEdges[LEFT].onRelease();
+ mEdges[RIGHT].onRelease();
+ }
+
+ if (mInvalidationCallback != null) {
+ mInvalidationCallback.run();
+ }
+ return;
+ }
+
+ final EdgeEffect edge = getEdgeForAxisAndSide(axis, velocity);
+
+ // If we're showing overscroll already, start fading it out.
+ if (!edge.isFinished()) {
+ edge.onRelease();
+ } else {
+ // Otherwise, show an absorb effect
+ edge.onAbsorb((int) velocity);
+ }
+
+ if (mInvalidationCallback != null) {
+ mInvalidationCallback.run();
+ }
+ }
+
+ /* package */ void setDistance(final float distance, final int axis) {
+ // The first overscroll event often has zero distance. Throw it out
+ if (distance == 0.0f) {
+ return;
+ }
+
+ final EdgeEffect edge = getEdgeForAxisAndSide(axis, (int) distance);
+ edge.onPull(distance / (axis == AXIS_X ? mWidth : mHeight));
+
+ if (mInvalidationCallback != null) {
+ mInvalidationCallback.run();
+ }
+ }
+
+ /**
+ * Draw the overscroll effect on a Canvas.
+ *
+ * @param canvas Canvas to draw on.
+ */
+ public void draw(final @NonNull Canvas canvas) {
+ ThreadUtils.assertOnUiThread();
+
+ if (mSession == null) {
+ return;
+ }
+
+ final Rect pageRect = new Rect();
+ mSession.getSurfaceBounds(pageRect);
+
+ // If we're pulling an edge, or fading it out, draw!
+ boolean invalidate = false;
+ if (!mEdges[TOP].isFinished()) {
+ invalidate |= draw(mEdges[TOP], canvas, pageRect.left, pageRect.top, 0);
+ }
+
+ if (!mEdges[BOTTOM].isFinished()) {
+ invalidate |= draw(mEdges[BOTTOM], canvas, pageRect.right, pageRect.bottom, 180);
+ }
+
+ if (!mEdges[LEFT].isFinished()) {
+ invalidate |= draw(mEdges[LEFT], canvas, pageRect.left, pageRect.bottom, 270);
+ }
+
+ if (!mEdges[RIGHT].isFinished()) {
+ invalidate |= draw(mEdges[RIGHT], canvas, pageRect.right, pageRect.top, 90);
+ }
+
+ // If the edge effect is animating off screen, invalidate.
+ if (invalidate && mInvalidationCallback != null) {
+ mInvalidationCallback.run();
+ }
+ }
+
+ private static boolean draw(
+ final EdgeEffect edge,
+ final Canvas canvas,
+ final float translateX,
+ final float translateY,
+ final float rotation) {
+ final int state = canvas.save();
+ canvas.translate(translateX, translateY);
+ canvas.rotate(rotation);
+ final boolean invalidate = edge.draw(canvas);
+ canvas.restoreToCount(state);
+
+ return invalidate;
+ }
+}