Bug 1271998 - Part 1 - Provide a ScrollView with a more efficient fadingEdge implementation. r=jwu
authorJan Henning <jh+bugzilla@buttercookie.de>
Thu, 31 Aug 2017 20:06:33 +0200
changeset 429519 fe02dfd16dc37b13888abd56e279b2d5b08a8d9e
parent 429518 d509047f7935f8cdd4f5bad571c52966fa95ba3b
child 429520 c8aa9b29b27834382b0b9ce67114927c2f348925
push id7761
push userjlund@mozilla.com
push dateFri, 15 Sep 2017 00:19:52 +0000
treeherdermozilla-beta@c38455951db4 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjwu
bugs1271998
milestone57.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1271998 - Part 1 - Provide a ScrollView with a more efficient fadingEdge implementation. r=jwu Our previous iteration of a more efficient fadingEdge implementation in FadedMultiColorTextView works by blending the text with a chosen colour. By choosing the same colour as the parent view onto which the TextView is placed, it was thus possible to achieve the impression of fading. With our new URL bar design this is no longer possible quite as easily, since the image used for a lightweight theme will now be displayed behind the URL itself as well. Since the implementation would have also needed more work to make it compatible with scrolling text or being placed in a ScrollView anyway, the fading effect is now achieved directly via the ScrollView instead. Android's built-in fadingEdge implementation calls Canvas.saveLayer (with CLIP_TO_LAYER_SAVE_FLAG omitted!) during a View's onDraw in order to fade out the contents of its children while preserving the background provided by its parents. This saveLayer call is rather expensive and is quite noticeable on a GPU profile even today. Therefore, we implement a more efficient variety of fadingEdges that paints over its children's content in onDrawForeground. To avoid any background content from being faded out, the whole view then has to be placed on a separate layer, however this is still much more efficient than calling Canvas.saveLayer and doesn't show up noticeably in a GPU profile. Prior to Marshmallow, onDrawForeground is not available, so we have to override draw instead in order to be able to paint over the content drawn by the ScrollView's descendants. This means that e.g. scrollbars would be faded out as well, but as we don't intend on showing a scrollbar within the context of this bug, it is an acceptable compromise. MozReview-Commit-ID: DCDPt6ogs0h
mobile/android/base/java/org/mozilla/gecko/widget/FadedHorizontalScrollView.java
mobile/android/base/moz.build
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/widget/FadedHorizontalScrollView.java
@@ -0,0 +1,159 @@
+/* -*- 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.gecko.widget;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.LinearGradient;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.Shader;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.HorizontalScrollView;
+
+import org.mozilla.gecko.AppConstants.Versions;
+import org.mozilla.gecko.R;
+
+/**
+ * A {@link HorizontalScrollView} implementation with a more efficient fadingEdge drawing strategy
+ * than the built-in version provided by Android. The width of the fade effect can be controlled via
+ * <code>gecko:fadeWidth</code>. To control in how far the fading effect should affect any views
+ * further up in the View hierarchy, place this view or one of its parents onto a separate layer
+ * using <code>android:layerType</code>. Currently, only horizontal fading is supported.
+ */
+public class FadedHorizontalScrollView extends HorizontalScrollView {
+    // Width of the fade effect from end of the view.
+    private final int mFadeWidth;
+    private final boolean mPreMarshmallow;
+
+    private final FadePaint mFadePaint;
+    private float mFadeTop;
+    private float mFadeBottom;
+    private boolean mVerticalFadeBordersDirty;
+
+    public FadedHorizontalScrollView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        mPreMarshmallow = Versions.preMarshmallow;
+
+        mFadePaint = new FadePaint();
+        mVerticalFadeBordersDirty = true;
+        addOnLayoutChangeListener(new OnLayoutChangeListener() {
+            @Override
+            public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
+                final int oldHeight = oldBottom - oldTop;
+                if (getHeight() != oldHeight) {
+                    mVerticalFadeBordersDirty = true;
+                }
+            }
+        });
+
+        final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FadedTextView);
+        mFadeWidth = a.getDimensionPixelSize(R.styleable.FadedTextView_fadeWidth, 0);
+        a.recycle();
+    }
+
+    @Override
+    public int getHorizontalFadingEdgeLength() {
+        return mFadeWidth;
+    }
+
+    @Override
+    public void draw(Canvas canvas) {
+        super.draw(canvas);
+        if (mPreMarshmallow) {
+            // Let our descendants draw their contents first, so we can then fade them out.
+            drawFading(canvas);
+        }
+    }
+
+    @TargetApi(23)
+    @Override
+    public void onDrawForeground(Canvas canvas) {
+        // Our descendants have already painted, so we can draw the fading first to avoid fading out
+        // any scrollbars etc. as well.
+        drawFading(canvas);
+        super.onDrawForeground(canvas);
+    }
+
+    private void drawFading(final Canvas canvas) {
+        // This code here is mostly a condensed version of Android's fadingEdge implementation
+        // in View#draw.
+
+        final int left = getScrollX() + getPaddingLeft();
+        final int right = getScrollX() + getRight() - getLeft() - getPaddingRight();
+
+        // Clip the fade length to prevent the opposing fadingEdges from overlapping each other.
+        int fadeWidth = getHorizontalFadingEdgeLength();
+        if (left + fadeWidth > right - fadeWidth) {
+            fadeWidth = (right - left) / 2;
+        }
+
+        final float effectiveFadeLeft = fadeWidth * getLeftFadingEdgeStrength();
+        final float effectiveFadeRight = fadeWidth * getRightFadingEdgeStrength();
+        final boolean drawLeft = effectiveFadeLeft > 1.0f;
+        final boolean drawRight = effectiveFadeRight > 1.0f;
+
+        if (!drawLeft && !drawRight) {
+            return;
+        }
+
+        if (mVerticalFadeBordersDirty) {
+            updateVerticalFadeBorders();
+        }
+
+        final Matrix matrix = mFadePaint.matrix;
+        final Shader fade = mFadePaint.fade;
+
+        if (drawLeft) {
+            matrix.setScale(1, effectiveFadeLeft);
+            matrix.postRotate(-90);
+            matrix.postTranslate(left, mFadeTop);
+            fade.setLocalMatrix(matrix);
+            mFadePaint.setShader(fade);
+            canvas.drawRect(left, mFadeTop, left + effectiveFadeLeft, mFadeBottom, mFadePaint);
+        }
+
+        if (drawRight) {
+            matrix.setScale(1, effectiveFadeRight);
+            matrix.postRotate(90);
+            matrix.postTranslate(right, mFadeTop);
+            fade.setLocalMatrix(matrix);
+            mFadePaint.setShader(fade);
+            canvas.drawRect(right - effectiveFadeRight, mFadeTop, right, mFadeBottom, mFadePaint);
+        }
+    }
+
+    private void updateVerticalFadeBorders() {
+        final View child = getChildAt(0);
+
+        if (child != null) {
+            mFadeTop = child.getTop() + child.getPaddingTop();
+            mFadeBottom = child.getBottom() - child.getPaddingBottom();
+        } else {
+            mFadeTop = 0;
+            mFadeBottom = 0;
+        }
+
+        mVerticalFadeBordersDirty = false;
+    }
+
+    private class FadePaint extends Paint {
+        public final Matrix matrix;
+        public final Shader fade;
+
+        public FadePaint() {
+            matrix = new Matrix();
+            fade = new LinearGradient(0, 0, 0, 1, 0xFF000000, 0, Shader.TileMode.CLAMP);
+            setShader(fade);
+            setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
+        }
+    }
+}
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -960,16 +960,17 @@ gbjar.sources += ['java/org/mozilla/geck
     'widget/CropImageView.java',
     'widget/DateTimePicker.java',
     'widget/DefaultDoorHanger.java',
     'widget/DefaultItemAnimatorBase.java',
     'widget/DoorHanger.java',
     'widget/DoorhangerConfig.java',
     'widget/EllipsisTextView.java',
     'widget/ExternalIntentDuringPrivateBrowsingPromptFragment.java',
+    'widget/FadedHorizontalScrollView.java',
     'widget/FadedMultiColorTextView.java',
     'widget/FadedSingleColorTextView.java',
     'widget/FadedTextView.java',
     'widget/FaviconView.java',
     'widget/FilledCardView.java',
     'widget/FlowLayout.java',
     'widget/GeckoActionProvider.java',
     'widget/GeckoPopupMenu.java',