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