Bug 1271998 - Part 1 - Provide a ScrollView with a more efficient fadingEdge implementation. r?walkingice,jwu draft
authorJan Henning <jh+bugzilla@buttercookie.de>
Thu, 31 Aug 2017 20:06:33 +0200
changeset 659240 79a5527aa40d1da45e25dc15843bf47cc76457b7
parent 659239 c18ce3c96c7c8848d29b60492adc1b291402230e
child 659241 9e97442cdf91a20486f5d09841a90b6e45eab83b
push id78062
push usermozilla@buttercookie.de
push dateTue, 05 Sep 2017 18:20:38 +0000
reviewerswalkingice, jwu
bugs1271998
milestone57.0a1
Bug 1271998 - Part 1 - Provide a ScrollView with a more efficient fadingEdge implementation. r?walkingice,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
@@ -979,16 +979,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',