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

Commit 0af17d55 authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Make ripples silky smooth"

parents 04dacb54 0c453ccb
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.