summaryrefslogtreecommitdiffstats
path: root/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/OverscrollEdgeEffect.java
blob: efd8061c98fffdd45473dc7aa91eb6eb0344bcdb (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
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;
  }
}