1 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
2  * This Source Code Form is subject to the terms of the Mozilla Public
3  * License, v. 2.0. If a copy of the MPL was not distributed with this
4  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 
6 package org.mozilla.geckoview;
7 
8 import org.mozilla.gecko.util.ThreadUtils;
9 
10 import android.content.Context;
11 import android.graphics.BlendMode;
12 import android.graphics.Canvas;
13 import android.graphics.Paint;
14 import android.graphics.PorterDuff;
15 import android.graphics.PorterDuffXfermode;
16 import android.graphics.Rect;
17 import android.os.Build;
18 import androidx.annotation.NonNull;
19 import androidx.annotation.Nullable;
20 import androidx.annotation.UiThread;
21 
22 import android.widget.EdgeEffect;
23 
24 import java.lang.reflect.Field;
25 import java.lang.reflect.Method;
26 
27 @UiThread
28 public final class OverscrollEdgeEffect {
29     // Used to index particular edges in the edges array
30     private static final int TOP = 0;
31     private static final int BOTTOM = 1;
32     private static final int LEFT = 2;
33     private static final int RIGHT = 3;
34 
35     /* package */ static final int AXIS_X = 0;
36     /* package */ static final int AXIS_Y = 1;
37 
38     // All four edges of the screen
39     private final EdgeEffect[] mEdges = new EdgeEffect[4];
40 
41     private final GeckoSession mSession;
42     private Runnable mInvalidationCallback;
43     private int mWidth;
44     private int mHeight;
45 
OverscrollEdgeEffect(final GeckoSession session)46     /* package */ OverscrollEdgeEffect(final GeckoSession session) {
47         mSession = session;
48     }
49 
50     private static Field sPaintField;
51     private static Method sSetType;
52 
53     // By default on SDK_INT 31 and above the edge effect default changed to "TYPE_STRETCH"
54     // which is an effect that we can't support due to using SurfaceTexture.
55     // This restores the edge effect type to TYPE_GLOW which is the default (and only option) on
56     // lower versions.
setType(final EdgeEffect edgeEffect)57     private void setType(final EdgeEffect edgeEffect) {
58         if (Build.VERSION.SDK_INT < 31 && !Build.VERSION.CODENAME.equals("S")) {
59             // setType is only available on 31 (early builds advertise themselves as 30,
60             // with codename S)
61             return;
62         }
63 
64         // TODO: remove reflection once 31 is stable
65         if (sSetType == null) {
66             try {
67                 sSetType = EdgeEffect.class.getDeclaredMethod("setType", int.class);
68             } catch (final NoSuchMethodException e) {
69                 // Nothing we can do here
70                 return;
71             }
72         }
73 
74         try {
75             sSetType.invoke(edgeEffect, /* TYPE_GLOW */ 0);
76         } catch (final Exception ex) {
77         }
78     }
79 
setBlendMode(final EdgeEffect edgeEffect)80     private void setBlendMode(final EdgeEffect edgeEffect) {
81         if (Build.VERSION.SDK_INT >= 29) {
82             edgeEffect.setBlendMode(BlendMode.SRC);
83             return;
84         }
85 
86         if (sPaintField == null) {
87             try {
88                 sPaintField = EdgeEffect.class.getDeclaredField("mPaint");
89                 sPaintField.setAccessible(true);
90             } catch (final NoSuchFieldException e) {
91                 // Cannot get the field, nothing we can do here
92                 return;
93             }
94         }
95 
96         try {
97             final Paint paint = (Paint) sPaintField.get(edgeEffect);
98             final PorterDuffXfermode mode = new PorterDuffXfermode(PorterDuff.Mode.SRC);
99             paint.setXfermode(mode);
100         } catch (final IllegalAccessException ex) {
101             // Nothing we can do
102         }
103     }
104 
105     /**
106      * Set the theme to use for overscroll from a given Context.
107      *
108      * @param context Context to use for the overscroll theme.
109      */
setTheme(final @NonNull Context context)110     public void setTheme(final @NonNull Context context) {
111         ThreadUtils.assertOnUiThread();
112 
113         for (int i = 0; i < mEdges.length; i++) {
114             final EdgeEffect edgeEffect = new EdgeEffect(context);
115             setBlendMode(edgeEffect);
116             setType(edgeEffect);
117             mEdges[i] = edgeEffect;
118         }
119     }
120 
121     /**
122      * Set a Runnable that acts as a callback to invalidate the overscroll effect (for
123      * example, as a response to user fling for example). The Runnbale should schedule a
124      * future call to {@link #draw(Canvas)} as a result of the invalidation.
125      *
126      * @param runnable Invalidation Runnable.
127      * @see #getInvalidationCallback()
128      */
setInvalidationCallback(final @Nullable Runnable runnable)129     public void setInvalidationCallback(final @Nullable Runnable runnable) {
130         ThreadUtils.assertOnUiThread();
131         mInvalidationCallback = runnable;
132     }
133 
134     /**
135      * Get the current invalidatation Runnable.
136      *
137      * @return Invalidation Runnable.
138      * @see #setInvalidationCallback(Runnable)
139      */
getInvalidationCallback()140     public @Nullable Runnable getInvalidationCallback() {
141         ThreadUtils.assertOnUiThread();
142         return mInvalidationCallback;
143     }
144 
setSize(final int width, final int height)145     /* package */ void setSize(final int width, final int height) {
146         mEdges[LEFT].setSize(height, width);
147         mEdges[RIGHT].setSize(height, width);
148         mEdges[TOP].setSize(width, height);
149         mEdges[BOTTOM].setSize(width, height);
150 
151         mWidth = width;
152         mHeight = height;
153     }
154 
getEdgeForAxisAndSide(final int axis, final float side)155     private EdgeEffect getEdgeForAxisAndSide(final int axis, final float side) {
156         if (axis == AXIS_Y) {
157             if (side < 0) {
158                 return mEdges[TOP];
159             } else {
160                 return mEdges[BOTTOM];
161             }
162         } else {
163             if (side < 0) {
164                 return mEdges[LEFT];
165             } else {
166                 return mEdges[RIGHT];
167             }
168         }
169     }
170 
setVelocity(final float velocity, final int axis)171     /* package */ void setVelocity(final float velocity, final int axis) {
172         final EdgeEffect edge = getEdgeForAxisAndSide(axis, velocity);
173 
174         // If we're showing overscroll already, start fading it out.
175         if (!edge.isFinished()) {
176             edge.onRelease();
177         } else {
178             // Otherwise, show an absorb effect
179             edge.onAbsorb((int)velocity);
180         }
181 
182         if (mInvalidationCallback != null) {
183             mInvalidationCallback.run();
184         }
185     }
186 
setDistance(final float distance, final int axis)187     /* package */ void setDistance(final float distance, final int axis) {
188         // The first overscroll event often has zero distance. Throw it out
189         if (distance == 0.0f) {
190             return;
191         }
192 
193         final EdgeEffect edge = getEdgeForAxisAndSide(axis, (int)distance);
194         edge.onPull(distance / (axis == AXIS_X ? mWidth : mHeight));
195 
196         if (mInvalidationCallback != null) {
197             mInvalidationCallback.run();
198         }
199     }
200 
201     /**
202      * Draw the overscroll effect on a Canvas.
203      *
204      * @param canvas Canvas to draw on.
205      */
draw(final @NonNull Canvas canvas)206     public void draw(final @NonNull Canvas canvas) {
207         ThreadUtils.assertOnUiThread();
208 
209         final Rect pageRect = new Rect();
210         mSession.getSurfaceBounds(pageRect);
211 
212         // If we're pulling an edge, or fading it out, draw!
213         boolean invalidate = false;
214         if (!mEdges[TOP].isFinished()) {
215             invalidate |= draw(mEdges[TOP], canvas, pageRect.left, pageRect.top, 0);
216         }
217 
218         if (!mEdges[BOTTOM].isFinished()) {
219             invalidate |= draw(mEdges[BOTTOM], canvas, pageRect.right, pageRect.bottom, 180);
220         }
221 
222         if (!mEdges[LEFT].isFinished()) {
223             invalidate |= draw(mEdges[LEFT], canvas, pageRect.left, pageRect.bottom, 270);
224         }
225 
226         if (!mEdges[RIGHT].isFinished()) {
227             invalidate |= draw(mEdges[RIGHT], canvas, pageRect.right, pageRect.top, 90);
228         }
229 
230         // If the edge effect is animating off screen, invalidate.
231         if (invalidate && mInvalidationCallback != null) {
232             mInvalidationCallback.run();
233         }
234     }
235 
draw(final EdgeEffect edge, final Canvas canvas, final float translateX, final float translateY, final float rotation)236     private static boolean draw(final EdgeEffect edge, final Canvas canvas, final float translateX, final float translateY, final float rotation) {
237         final int state = canvas.save();
238         canvas.translate(translateX, translateY);
239         canvas.rotate(rotation);
240         final boolean invalidate = edge.draw(canvas);
241         canvas.restoreToCount(state);
242 
243         return invalidate;
244     }
245 }
246