Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 0c453ccb authored by John Reck's avatar John Reck
Browse files

Make ripples silky smooth

* Updates press state ripple to match UX spec
* Makes it ungodly silky smooth LIKE BUTTAH
* Update hover & focus states to be closer to UX spec,
  still needs a final pass.

Bug: 63635160
Test: Clicked on a bunch of stuff

Change-Id: I162ab9d8d669002f2ae511f93b5d9fe67f99c533
parent c0c6ee6b
Loading
Loading
Loading
Loading
+3 −7
Original line number Diff line number Diff line
@@ -19,7 +19,6 @@ package android.view;
import android.animation.Animator;
import android.animation.TimeInterpolator;
import android.animation.ValueAnimator;
import android.graphics.Canvas;
import android.graphics.CanvasProperty;
import android.graphics.Paint;
import android.util.SparseIntArray;
@@ -281,12 +280,9 @@ public class RenderNodeAnimator extends Animator {
        setTarget(mViewTarget.mRenderNode);
    }

    public void setTarget(Canvas canvas) {
        if (!(canvas instanceof DisplayListCanvas)) {
            throw new IllegalArgumentException("Not a GLES20RecordingCanvas");
        }
        final DisplayListCanvas recordingCanvas = (DisplayListCanvas) canvas;
        setTarget(recordingCanvas.mNode);
    /** Sets the animation target to the owning view of the DisplayListCanvas */
    public void setTarget(DisplayListCanvas canvas) {
        setTarget(canvas.mNode);
    }

    private void setTarget(RenderNode node) {
+34 −103
Original line number Diff line number Diff line
@@ -36,138 +36,69 @@ class RippleBackground extends RippleComponent {

    private static final TimeInterpolator LINEAR_INTERPOLATOR = new LinearInterpolator();

    private static final int OPACITY_ENTER_DURATION = 600;
    private static final int OPACITY_ENTER_DURATION_FAST = 120;
    private static final int OPACITY_EXIT_DURATION = 480;
    private static final int OPACITY_DURATION = 80;

    // Hardware rendering properties.
    private CanvasProperty<Paint> mPropPaint;
    private CanvasProperty<Float> mPropRadius;
    private CanvasProperty<Float> mPropX;
    private CanvasProperty<Float> mPropY;
    private ObjectAnimator mAnimator;

    // Software rendering properties.
    private float mOpacity = 0;

    /** Whether this ripple is bounded. */
    private boolean mIsBounded;

    public RippleBackground(RippleDrawable owner, Rect bounds, boolean isBounded,
            boolean forceSoftware) {
        super(owner, bounds, forceSoftware);
    private boolean mFocused = false;
    private boolean mHovered = false;

    public RippleBackground(RippleDrawable owner, Rect bounds, boolean isBounded) {
        super(owner, bounds);

        mIsBounded = isBounded;
    }

    public boolean isVisible() {
        return mOpacity > 0 || isHardwareAnimating();
        return mOpacity > 0;
    }

    @Override
    protected boolean drawSoftware(Canvas c, Paint p) {
        boolean hasContent = false;

    public void draw(Canvas c, Paint p) {
        final int origAlpha = p.getAlpha();
        final int alpha = (int) (origAlpha * mOpacity + 0.5f);
        final int alpha = Math.min((int) (origAlpha * mOpacity + 0.5f), 255);
        if (alpha > 0) {
            p.setAlpha(alpha);
            c.drawCircle(0, 0, mTargetRadius, p);
            p.setAlpha(origAlpha);
            hasContent = true;
        }

        return hasContent;
    }

    @Override
    protected boolean drawHardware(DisplayListCanvas c) {
        c.drawCircle(mPropX, mPropY, mPropRadius, mPropPaint);
        return true;
    public void setState(boolean focused, boolean hovered, boolean animateChanged) {
        if (mHovered != hovered || mFocused != focused) {
            mHovered = hovered;
            mFocused = focused;
            onStateChanged(animateChanged);
        }

    @Override
    protected Animator createSoftwareEnter(boolean fast) {
        // Linear enter based on current opacity.
        final int maxDuration = fast ? OPACITY_ENTER_DURATION_FAST : OPACITY_ENTER_DURATION;
        final int duration = (int) ((1 - mOpacity) * maxDuration);

        final ObjectAnimator opacity = ObjectAnimator.ofFloat(this, OPACITY, 1);
        opacity.setAutoCancel(true);
        opacity.setDuration(duration);
        opacity.setInterpolator(LINEAR_INTERPOLATOR);

        return opacity;
    }

    @Override
    protected Animator createSoftwareExit() {
        final AnimatorSet set = new AnimatorSet();

        // Linear exit after enter is completed.
        final ObjectAnimator exit = ObjectAnimator.ofFloat(this, OPACITY, 0);
        exit.setInterpolator(LINEAR_INTERPOLATOR);
        exit.setDuration(OPACITY_EXIT_DURATION);
        exit.setAutoCancel(true);

        final AnimatorSet.Builder builder = set.play(exit);

        // Linear "fast" enter based on current opacity.
        final int fastEnterDuration = mIsBounded ?
                (int) ((1 - mOpacity) * OPACITY_ENTER_DURATION_FAST) : 0;
        if (fastEnterDuration > 0) {
            final ObjectAnimator enter = ObjectAnimator.ofFloat(this, OPACITY, 1);
            enter.setInterpolator(LINEAR_INTERPOLATOR);
            enter.setDuration(fastEnterDuration);
            enter.setAutoCancel(true);

            builder.after(enter);
        }

        return set;
    private void onStateChanged(boolean animateChanged) {
        float newOpacity = 0.0f;
        if (mHovered) newOpacity += 1.0f;
        if (mFocused) newOpacity += 1.0f;
        if (mAnimator != null) {
            mAnimator.cancel();
            mAnimator = null;
        }

    @Override
    protected RenderNodeAnimatorSet createHardwareExit(Paint p) {
        final RenderNodeAnimatorSet set = new RenderNodeAnimatorSet();

        final int targetAlpha = p.getAlpha();
        final int currentAlpha = (int) (mOpacity * targetAlpha + 0.5f);
        p.setAlpha(currentAlpha);

        mPropPaint = CanvasProperty.createPaint(p);
        mPropRadius = CanvasProperty.createFloat(mTargetRadius);
        mPropX = CanvasProperty.createFloat(0);
        mPropY = CanvasProperty.createFloat(0);

        final int fastEnterDuration = mIsBounded ?
                (int) ((1 - mOpacity) * OPACITY_ENTER_DURATION_FAST) : 0;

        // Linear exit after enter is completed.
        final RenderNodeAnimator exit = new RenderNodeAnimator(
                mPropPaint, RenderNodeAnimator.PAINT_ALPHA, 0);
        exit.setInterpolator(LINEAR_INTERPOLATOR);
        exit.setDuration(OPACITY_EXIT_DURATION);
        if (fastEnterDuration > 0) {
            exit.setStartDelay(fastEnterDuration);
            exit.setStartValue(targetAlpha);
        if (animateChanged) {
            mAnimator = ObjectAnimator.ofFloat(this, OPACITY, newOpacity);
            mAnimator.setDuration(OPACITY_DURATION);
            mAnimator.setInterpolator(LINEAR_INTERPOLATOR);
            mAnimator.start();
        } else {
            mOpacity = newOpacity;
        }
        set.add(exit);

        // Linear "fast" enter based on current opacity.
        if (fastEnterDuration > 0) {
            final RenderNodeAnimator enter = new RenderNodeAnimator(
                    mPropPaint, RenderNodeAnimator.PAINT_ALPHA, targetAlpha);
            enter.setInterpolator(LINEAR_INTERPOLATOR);
            enter.setDuration(fastEnterDuration);
            set.add(enter);
    }

        return set;
    public void jumpToFinal() {
        if (mAnimator != null) {
            mAnimator.end();
            mAnimator = null;
        }

    @Override
    protected void jumpValuesToExit() {
        mOpacity = 0;
    }

    private static abstract class BackgroundProperty extends FloatProperty<RippleBackground> {
+3 −241
Original line number Diff line number Diff line
@@ -27,23 +27,14 @@ import android.view.RenderNodeAnimator;
import java.util.ArrayList;

/**
 * Abstract class that handles hardware/software hand-off and lifecycle for
 * animated ripple foreground and background components.
 * Abstract class that handles size & positioning common to the ripple & focus states.
 */
abstract class RippleComponent {
    private final RippleDrawable mOwner;
    protected final RippleDrawable mOwner;

    /** Bounds used for computing max radius. May be modified by the owner. */
    protected final Rect mBounds;

    /** Whether we can use hardware acceleration for the exit animation. */
    private boolean mHasDisplayListCanvas;

    private boolean mHasPendingHardwareAnimator;
    private RenderNodeAnimatorSet mHardwareAnimator;

    private Animator mSoftwareAnimator;

    /** Whether we have an explicit maximum radius. */
    private boolean mHasMaxRadius;

@@ -53,16 +44,9 @@ abstract class RippleComponent {
    /** Screen density used to adjust pixel-based constants. */
    protected float mDensityScale;

    /**
     * If set, force all ripple animations to not run on RenderThread, even if it would be
     * available.
     */
    private final boolean mForceSoftware;

    public RippleComponent(RippleDrawable owner, Rect bounds, boolean forceSoftware) {
    public RippleComponent(RippleDrawable owner, Rect bounds) {
        mOwner = owner;
        mBounds = bounds;
        mForceSoftware = forceSoftware;
    }

    public void onBoundsChange() {
@@ -91,89 +75,6 @@ abstract class RippleComponent {
        return (float) Math.sqrt(halfWidth * halfWidth + halfHeight * halfHeight);
    }

    /**
     * Starts a ripple enter animation.
     *
     * @param fast whether the ripple should enter quickly
     */
    public final void enter(boolean fast) {
        cancel();

        mSoftwareAnimator = createSoftwareEnter(fast);

        if (mSoftwareAnimator != null) {
            mSoftwareAnimator.start();
        }
    }

    /**
     * Starts a ripple exit animation.
     */
    public final void exit() {
        cancel();

        if (mHasDisplayListCanvas) {
            // We don't have access to a canvas here, but we expect one on the
            // next frame. We'll start the render thread animation then.
            mHasPendingHardwareAnimator = true;

            // Request another frame.
            invalidateSelf();
        } else {
            mSoftwareAnimator = createSoftwareExit();
            mSoftwareAnimator.start();
        }
    }

    /**
     * Cancels all animations. Software animation values are left in the
     * current state, while hardware animation values jump to the end state.
     */
    public void cancel() {
        cancelSoftwareAnimations();
        endHardwareAnimations();
    }

    /**
     * Ends all animations, jumping values to the end state.
     */
    public void end() {
        endSoftwareAnimations();
        endHardwareAnimations();
    }

    /**
     * Draws the ripple to the canvas, inheriting the paint's color and alpha
     * properties.
     *
     * @param c the canvas to which the ripple should be drawn
     * @param p the paint used to draw the ripple
     * @return {@code true} if something was drawn, {@code false} otherwise
     */
    public boolean draw(Canvas c, Paint p) {
        final boolean hasDisplayListCanvas = !mForceSoftware && c.isHardwareAccelerated()
                && c instanceof DisplayListCanvas;
        if (mHasDisplayListCanvas != hasDisplayListCanvas) {
            mHasDisplayListCanvas = hasDisplayListCanvas;

            if (!hasDisplayListCanvas) {
                // We've switched from hardware to non-hardware mode. Panic.
                endHardwareAnimations();
            }
        }

        if (hasDisplayListCanvas) {
            final DisplayListCanvas hw = (DisplayListCanvas) c;
            startPendingAnimation(hw, p);

            if (mHardwareAnimator != null) {
                return drawHardware(hw);
            }
        }

        return drawSoftware(c, p);
    }

    /**
     * Populates {@code bounds} with the maximum drawing bounds of the ripple
     * relative to its center. The resulting bounds should be translated into
@@ -186,77 +87,10 @@ abstract class RippleComponent {
        bounds.set(-r, -r, r, r);
    }

    /**
     * Starts the pending hardware animation, if available.
     *
     * @param hw hardware canvas on which the animation should draw
     * @param p paint whose properties the hardware canvas should use
     */
    private void startPendingAnimation(DisplayListCanvas hw, Paint p) {
        if (mHasPendingHardwareAnimator) {
            mHasPendingHardwareAnimator = false;

            mHardwareAnimator = createHardwareExit(new Paint(p));
            mHardwareAnimator.start(hw);

            // Preemptively jump the software values to the end state now that
            // the hardware exit has read whatever values it needs.
            jumpValuesToExit();
        }
    }

    /**
     * Cancels any current software animations, leaving the values in their
     * current state.
     */
    private void cancelSoftwareAnimations() {
        if (mSoftwareAnimator != null) {
            mSoftwareAnimator.cancel();
            mSoftwareAnimator = null;
        }
    }

    /**
     * Ends any current software animations, jumping the values to their end
     * state.
     */
    private void endSoftwareAnimations() {
        if (mSoftwareAnimator != null) {
            mSoftwareAnimator.end();
            mSoftwareAnimator = null;
        }
    }

    /**
     * Ends any pending or current hardware animations.
     * <p>
     * Hardware animations can't synchronize values back to the software
     * thread, so there is no "cancel" equivalent.
     */
    private void endHardwareAnimations() {
        if (mHardwareAnimator != null) {
            mHardwareAnimator.end();
            mHardwareAnimator = null;
        }

        if (mHasPendingHardwareAnimator) {
            mHasPendingHardwareAnimator = false;

            // Manually jump values to their exited state. Normally we'd do that
            // later when starting the hardware exit, but we're aborting early.
            jumpValuesToExit();
        }
    }

    protected final void invalidateSelf() {
        mOwner.invalidateSelf(false);
    }

    protected final boolean isHardwareAnimating() {
        return mHardwareAnimator != null && mHardwareAnimator.isRunning()
                || mHasPendingHardwareAnimator;
    }

    protected final void onHotspotBoundsChanged() {
        if (!mHasMaxRadius) {
            final float halfWidth = mBounds.width() / 2.0f;
@@ -276,76 +110,4 @@ abstract class RippleComponent {
    protected void onTargetRadiusChanged(float targetRadius) {
        // Stub.
    }

    protected abstract Animator createSoftwareEnter(boolean fast);

    protected abstract Animator createSoftwareExit();

    protected abstract RenderNodeAnimatorSet createHardwareExit(Paint p);

    protected abstract boolean drawHardware(DisplayListCanvas c);

    protected abstract boolean drawSoftware(Canvas c, Paint p);

    /**
     * Called when the hardware exit is cancelled. Jumps software values to end
     * state to ensure that software and hardware values are synchronized.
     */
    protected abstract void jumpValuesToExit();

    public static class RenderNodeAnimatorSet {
        private final ArrayList<RenderNodeAnimator> mAnimators = new ArrayList<>();

        public void add(RenderNodeAnimator anim) {
            mAnimators.add(anim);
        }

        public void clear() {
            mAnimators.clear();
        }

        public void start(DisplayListCanvas target) {
            if (target == null) {
                throw new IllegalArgumentException("Hardware canvas must be non-null");
            }

            final ArrayList<RenderNodeAnimator> animators = mAnimators;
            final int N = animators.size();
            for (int i = 0; i < N; i++) {
                final RenderNodeAnimator anim = animators.get(i);
                anim.setTarget(target);
                anim.start();
            }
        }

        public void cancel() {
            final ArrayList<RenderNodeAnimator> animators = mAnimators;
            final int N = animators.size();
            for (int i = 0; i < N; i++) {
                final RenderNodeAnimator anim = animators.get(i);
                anim.cancel();
            }
        }

        public void end() {
            final ArrayList<RenderNodeAnimator> animators = mAnimators;
            final int N = animators.size();
            for (int i = 0; i < N; i++) {
                final RenderNodeAnimator anim = animators.get(i);
                anim.end();
            }
        }

        public boolean isRunning() {
            final ArrayList<RenderNodeAnimator> animators = mAnimators;
            final int N = animators.size();
            for (int i = 0; i < N; i++) {
                final RenderNodeAnimator anim = animators.get(i);
                if (anim.isRunning()) {
                    return true;
                }
            }
            return false;
        }
    }
}
+53 −77
Original line number Diff line number Diff line
@@ -16,11 +16,6 @@

package android.graphics.drawable;

import com.android.internal.R;

import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.pm.ActivityInfo.Config;
@@ -42,6 +37,11 @@ import android.graphics.Rect;
import android.graphics.Shader;
import android.util.AttributeSet;

import com.android.internal.R;

import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;

import java.io.IOException;
import java.util.Arrays;

@@ -135,9 +135,6 @@ public class RippleDrawable extends LayerDrawable {
    private PorterDuffColorFilter mMaskColorFilter;
    private boolean mHasValidMask;

    /** Whether we expect to draw a background when visible. */
    private boolean mBackgroundActive;

    /** The current ripple. May be actively animating or pending entry. */
    private RippleForeground mRipple;

@@ -217,7 +214,7 @@ public class RippleDrawable extends LayerDrawable {
        }

        if (mBackground != null) {
            mBackground.end();
            mBackground.jumpToFinal();
        }

        cancelExitingRipples();
@@ -266,9 +263,9 @@ public class RippleDrawable extends LayerDrawable {
            }
        }

        setRippleActive(focused || (enabled && pressed));
        setRippleActive(enabled && pressed);

        setBackgroundActive(hovered, hovered);
        setBackgroundActive(hovered, focused);
        return changed;
    }

@@ -283,14 +280,13 @@ public class RippleDrawable extends LayerDrawable {
        }
    }

    private void setBackgroundActive(boolean active, boolean focused) {
        if (mBackgroundActive != active) {
            mBackgroundActive = active;
            if (active) {
                tryBackgroundEnter(focused);
            } else {
                tryBackgroundExit();
    private void setBackgroundActive(boolean hovered, boolean focused) {
        if (mBackground == null && (hovered || focused)) {
            mBackground = new RippleBackground(this, mHotspotBounds, isBounded());
            mBackground.setup(mState.mMaxRadius, mDensity);
        }
        if (mBackground != null) {
            mBackground.setState(focused, hovered, true);
        }
    }

@@ -327,10 +323,6 @@ public class RippleDrawable extends LayerDrawable {
                tryRippleEnter();
            }

            if (mBackgroundActive) {
                tryBackgroundEnter(false);
            }

            // Skip animations, just show the correct final states.
            jumpToCurrentState();
        }
@@ -545,26 +537,6 @@ public class RippleDrawable extends LayerDrawable {
        }
    }

    /**
     * Creates an active hotspot at the specified location.
     */
    private void tryBackgroundEnter(boolean focused) {
        if (mBackground == null) {
            final boolean isBounded = isBounded();
            mBackground = new RippleBackground(this, mHotspotBounds, isBounded, mForceSoftware);
        }

        mBackground.setup(mState.mMaxRadius, mDensity);
        mBackground.enter(focused);
    }

    private void tryBackgroundExit() {
        if (mBackground != null) {
            // Don't null out the background, we need it to draw!
            mBackground.exit();
        }
    }

    /**
     * Attempts to start an enter animation for the active hotspot. Fails if
     * there are too many animating ripples.
@@ -593,7 +565,7 @@ public class RippleDrawable extends LayerDrawable {
        }

        mRipple.setup(mState.mMaxRadius, mDensity);
        mRipple.enter(false);
        mRipple.enter();
    }

    /**
@@ -623,9 +595,7 @@ public class RippleDrawable extends LayerDrawable {
        }

        if (mBackground != null) {
            mBackground.end();
            mBackground = null;
            mBackgroundActive = false;
            mBackground.setState(false, false, false);
        }

        cancelExitingRipples();
@@ -858,6 +828,40 @@ public class RippleDrawable extends LayerDrawable {
        final float y = mHotspotBounds.exactCenterY();
        canvas.translate(x, y);

        final Paint p = getRipplePaint();

        if (background != null && background.isVisible()) {
            background.draw(canvas, p);
        }

        if (count > 0) {
            final RippleForeground[] ripples = mExitingRipples;
            for (int i = 0; i < count; i++) {
                ripples[i].draw(canvas, p);
            }
        }

        if (active != null) {
            active.draw(canvas, p);
        }

        canvas.translate(-x, -y);
    }

    private void drawMask(Canvas canvas) {
        mMask.draw(canvas);
    }

    Paint getRipplePaint() {
        if (mRipplePaint == null) {
            mRipplePaint = new Paint();
            mRipplePaint.setAntiAlias(true);
            mRipplePaint.setStyle(Paint.Style.FILL);
        }

        final float x = mHotspotBounds.exactCenterX();
        final float y = mHotspotBounds.exactCenterY();

        updateMaskShaderIfNeeded();

        // Position the shader to account for canvas translation.
@@ -871,7 +875,7 @@ public class RippleDrawable extends LayerDrawable {
        // half so that the ripple and background together yield full alpha.
        final int color = mState.mColor.getColorForState(getState(), Color.BLACK);
        final int halfAlpha = (Color.alpha(color) / 2) << 24;
        final Paint p = getRipplePaint();
        final Paint p = mRipplePaint;

        if (mMaskColorFilter != null) {
            // The ripple timing depends on the paint's alpha value, so we need
@@ -890,35 +894,7 @@ public class RippleDrawable extends LayerDrawable {
            p.setShader(null);
        }

        if (background != null && background.isVisible()) {
            background.draw(canvas, p);
        }

        if (count > 0) {
            final RippleForeground[] ripples = mExitingRipples;
            for (int i = 0; i < count; i++) {
                ripples[i].draw(canvas, p);
            }
        }

        if (active != null) {
            active.draw(canvas, p);
        }

        canvas.translate(-x, -y);
    }

    private void drawMask(Canvas canvas) {
        mMask.draw(canvas);
    }

    private Paint getRipplePaint() {
        if (mRipplePaint == null) {
            mRipplePaint = new Paint();
            mRipplePaint.setAntiAlias(true);
            mRipplePaint.setStyle(Paint.Style.FILL);
        }
        return mRipplePaint;
        return p;
    }

    @Override
+218 −126

File changed.

Preview size limit exceeded, changes collapsed.