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

Commit 4d7c0171 authored by Alan Viverette's avatar Alan Viverette Committed by Android (Google) Code Review
Browse files

Merge "Cleaning up TouchFeedbackDrawable and Ripple APIs"

parents 1eac187d 53d1cfe2
Loading
Loading
Loading
Loading
+265 −213
Original line number Diff line number Diff line
@@ -16,20 +16,21 @@

package android.graphics.drawable;

import android.animation.Animator;
import android.animation.Animator.AnimatorListener;
import android.animation.ObjectAnimator;
import android.animation.TimeInterpolator;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.graphics.Rect;
import android.util.MathUtils;
import android.view.animation.AnimationUtils;
import android.view.animation.DecelerateInterpolator;

/**
 * Draws a Quantum Paper ripple.
 */
class Ripple {
    private static final TimeInterpolator INTERPOLATOR = new DecelerateInterpolator(2.0f);
    private static final TimeInterpolator INTERPOLATOR = new DecelerateInterpolator();

    /** Starting radius for a ripple. */
    private static final int STARTING_RADIUS_DP = 16;
@@ -37,6 +38,9 @@ class Ripple {
    /** Radius when finger is outside view bounds. */
    private static final int OUTSIDE_RADIUS_DP = 16;

    /** Radius when finger is inside view bounds. */
    private static final int INSIDE_RADIUS_DP = 96;

    /** Margin when constraining outside touches (fraction of outer radius). */
    private static final float OUTSIDE_MARGIN = 0.8f;

@@ -44,15 +48,52 @@ class Ripple {
    private static final float OUTSIDE_RESISTANCE = 0.7f;

    /** Minimum alpha value during a pulse animation. */
    private static final int PULSE_MIN_ALPHA = 128;
    private static final float PULSE_MIN_ALPHA = 0.5f;

    /** Duration for animating the trailing edge of the ripple. */
    private static final int EXIT_DURATION = 600;

    /** Duration for animating the leading edge of the ripple. */
    private static final int ENTER_DURATION = 400;

    /** Duration for animating the ripple alpha in and out. */
    private static final int FADE_DURATION = 50;

    /** Minimum elapsed time between start of enter and exit animations. */
    private static final int EXIT_MIN_DELAY = 200;

    /** Duration for animating between inside and outside touch. */
    private static final int OUTSIDE_DURATION = 300;

    /** Duration for animating pulses. */
    private static final int PULSE_DURATION = 400;

    /** Interval between pulses while inside and fully entered. */
    private static final int PULSE_INTERVAL = 400;

    /** Delay before pulses start. */
    private static final int PULSE_DELAY = 500;

    private final Drawable mOwner;

    /** Bounds used for computing max radius and containment. */
    private final Rect mBounds;
    private final Rect mPadding;

    private RippleAnimator mAnimator;
    /** Configured maximum ripple radius when the center is outside the bounds. */
    private final int mMaxOutsideRadius;

    /** Configured maximum ripple radius. */
    private final int mMaxInsideRadius;

    private ObjectAnimator mEnter;
    private ObjectAnimator mExit;

    /** Maximum ripple radius. */
    private int mMaxRadius;

    private int mMinRadius;
    private int mOutsideRadius;
    private float mOuterRadius;
    private float mInnerRadius;
    private float mAlphaMultiplier;

    /** Center x-coordinate. */
    private float mX;
@@ -61,272 +102,283 @@ class Ripple {
    private float mY;

    /** Whether the center is within the parent bounds. */
    private boolean mInside;
    private boolean mInsideBounds;

    /** Whether to pulse this ripple. */
    boolean mPulse;
    private boolean mPulseEnabled;

    /** Enter state. A value in [0...1] or -1 if not set. */
    float mEnterState = -1;
    /** Temporary hack since we can't check finished state of animator. */
    private boolean mExitFinished;

    /** Exit state. A value in [0...1] or -1 if not set. */
    float mExitState = -1;

    /** Outside state. A value in [0...1] or -1 if not set. */
    float mOutsideState = -1;

    /** Pulse state. A value in [0...1] or -1 if not set. */
    float mPulseState = -1;
    /** Whether this ripple has ever moved. */
    private boolean mHasMoved;

    /**
     * Creates a new ripple with the specified parent bounds, padding, initial
     * position, and screen density.
     * Creates a new ripple.
     */
    public Ripple(Rect bounds, Rect padding, float x, float y, float density, boolean pulse) {
    public Ripple(Drawable owner, Rect bounds, float density, boolean pulseEnabled) {
        mOwner = owner;
        mBounds = bounds;
        mPadding = padding;
        mInside = mBounds.contains((int) x, (int) y);
        mPulse = pulse;
        mPulseEnabled = pulseEnabled;

        mX = x;
        mY = y;
        mOuterRadius = (int) (density * STARTING_RADIUS_DP + 0.5f);
        mMaxOutsideRadius = (int) (density * OUTSIDE_RADIUS_DP + 0.5f);
        mMaxInsideRadius = (int) (density * INSIDE_RADIUS_DP + 0.5f);
        mMaxRadius = Math.min(mMaxInsideRadius, Math.max(bounds.width(), bounds.height()));
    }

        mMinRadius = (int) (density * STARTING_RADIUS_DP + 0.5f);
        mOutsideRadius = (int) (density * OUTSIDE_RADIUS_DP + 0.5f);
    public void setOuterRadius(float r) {
        mOuterRadius = r;
        invalidateSelf();
    }

    public void setMinRadius(int minRadius) {
        mMinRadius = minRadius;
    public float getOuterRadius() {
        return mOuterRadius;
    }

    public void setOutsideRadius(int outsideRadius) {
        mOutsideRadius = outsideRadius;
    public void setInnerRadius(float r) {
        mInnerRadius = r;
        invalidateSelf();
    }

    /**
     * Updates the center coordinates.
     */
    public void move(float x, float y) {
        mX = x;
        mY = y;
    public float getInnerRadius() {
        return mInnerRadius;
    }

        final boolean inside = mBounds.contains((int) x, (int) y);
        if (mInside != inside) {
            if (mAnimator != null) {
                mAnimator.outside();
    public void setAlphaMultiplier(float a) {
        mAlphaMultiplier = a;
        invalidateSelf();
    }
            mInside = inside;

    public float getAlphaMultiplier() {
        return mAlphaMultiplier;
    }

    /**
     * Returns whether this ripple has finished exiting.
     */
    public boolean isFinished() {
        return mExitFinished;
    }

    /**
     * Called when the bounds change.
     */
    public void onBoundsChanged() {
        final boolean inside = mBounds.contains((int) mX, (int) mY);
        if (mInside != inside) {
            if (mAnimator != null) {
                mAnimator.outside();
            }
            mInside = inside;
        }
    }
        mMaxRadius = Math.min(mMaxInsideRadius, Math.max(mBounds.width(), mBounds.height()));

    public RippleAnimator animate() {
        if (mAnimator == null) {
            mAnimator = new RippleAnimator(this);
        }
        return mAnimator;
        updateInsideBounds();
    }

    public boolean draw(Canvas c, Paint p) {
        final Rect bounds = mBounds;
        final Rect padding = mPadding;
        final float dX = Math.max(mX - bounds.left, bounds.right - mX);
        final float dY = Math.max(mY - bounds.top, bounds.bottom - mY);
        final int maxRadius = (int) Math.ceil(Math.sqrt(dX * dX + dY * dY));

        final float enterState = mEnterState;
        final float exitState = mExitState;
        final float outsideState = mOutsideState;
        final float pulseState = mPulseState;
        final float insideRadius = MathUtils.lerp(mMinRadius, maxRadius, enterState);
        final float outerRadius = MathUtils.lerp(mOutsideRadius, insideRadius,
                mInside ? outsideState : 1 - outsideState);
    private void updateInsideBounds() {
        final boolean insideBounds = mBounds.contains((int) (mX + 0.5f), (int) (mY + 0.5f));
        if (mInsideBounds != insideBounds || !mHasMoved) {
            mInsideBounds = insideBounds;
            mHasMoved = true;

        // Apply resistance effect when outside bounds.
        final float x = looseConstrain(mX, bounds.left + padding.left, bounds.right - padding.right,
                outerRadius * OUTSIDE_MARGIN, OUTSIDE_RESISTANCE);
        final float y = looseConstrain(mY, bounds.top + padding.top, bounds.bottom - padding.bottom,
                outerRadius * OUTSIDE_MARGIN, OUTSIDE_RESISTANCE);

        // Compute maximum alpha, taking pulse into account when active.
        final int maxAlpha;
        if (pulseState < 0 || pulseState >= 1) {
            maxAlpha = 255;
        } else {
            final float pulseAlpha;
            if (pulseState > 0.5) {
                // Pulsing in to max alpha.
                pulseAlpha = MathUtils.lerp(PULSE_MIN_ALPHA, 255, (pulseState - .5f) * 2);
            if (insideBounds) {
                enter();
            } else {
                // Pulsing out to min alpha.
                pulseAlpha = MathUtils.lerp(255, PULSE_MIN_ALPHA, pulseState * 2f);
                outside();
            }

            if (exitState > 0) {
                // Animating exit, interpolate pulse with exit state.
                maxAlpha = (int) (MathUtils.lerp(255, pulseAlpha, exitState) + 0.5f);
            } else if (mInside) {
                // No animation, no need to interpolate.
                maxAlpha = (int) (pulseAlpha + 0.5f);
            } else {
                // Animating inside, interpolate pulse with inside state.
                maxAlpha = (int) (MathUtils.lerp(pulseAlpha, 255, outsideState) + 0.5f);
        }
    }

        if (maxAlpha > 0) {
            if (exitState <= 0) {
                // Exit state isn't showing, so we can simplify to a solid
                // circle.
                if (outerRadius > 0) {
                    p.setAlpha(maxAlpha);
                    p.setStyle(Style.FILL);
                    c.drawCircle(x, y, outerRadius, p);
                    return true;
                }
    /**
     * Draws the ripple using the specified paint.
     */
    public boolean draw(Canvas c, Paint p) {
        final Rect bounds = mBounds;
        final float outerRadius = mOuterRadius;
        final float innerRadius = mInnerRadius;
        final float alphaMultiplier = mAlphaMultiplier;

        // Cache the paint alpha so we can restore it later.
        final int paintAlpha = p.getAlpha();
        final int alpha = (int) (paintAlpha * alphaMultiplier + 0.5f);

        // Apply resistance effect when outside bounds.
        final float x;
        final float y;
        if (mInsideBounds) {
            x = mX;
            y = mY;
        } else {
                // Both states are showing, so we need a circular stroke.
                final float innerRadius = MathUtils.lerp(0, outerRadius, exitState);
            // TODO: We need to do this outside of draw() so that our dirty
            // bounds accurately reflect resistance.
            x = looseConstrain(mX, bounds.left, bounds.right,
                    mOuterRadius * OUTSIDE_MARGIN, OUTSIDE_RESISTANCE);
            y = looseConstrain(mY, bounds.top, bounds.bottom,
                    mOuterRadius * OUTSIDE_MARGIN, OUTSIDE_RESISTANCE);
        }

        final boolean hasContent;
        if (alphaMultiplier <= 0 || innerRadius >= outerRadius) {
            // Nothing to draw.
            hasContent = false;
        } else if (innerRadius > 0) {
            // Draw a ring.
            final float strokeWidth = outerRadius - innerRadius;
                if (strokeWidth > 0) {
                    final float strokeRadius = innerRadius + strokeWidth / 2f;
                    final int alpha = (int) (MathUtils.lerp(maxAlpha, 0, exitState) + 0.5f);
                    if (alpha > 0) {
            final float strokeRadius = innerRadius + strokeWidth / 2.0f;
            p.setAlpha(alpha);
            p.setStyle(Style.STROKE);
            p.setStrokeWidth(strokeWidth);
            c.drawCircle(x, y, strokeRadius, p);
                        return true;
                    }
                }
            }
            hasContent = true;
        } else if (outerRadius > 0) {
            // Draw a circle.
            p.setAlpha(alpha);
            p.setStyle(Style.FILL);
            c.drawCircle(x, y, outerRadius, p);
            hasContent = true;
        } else {
            hasContent = false;
        }

        return false;
        p.setAlpha(paintAlpha);
        return hasContent;
    }

    /**
     * Returns the maximum bounds for this ripple.
     */
    public void getBounds(Rect bounds) {
        final int x = (int) mX;
        final int y = (int) mY;
        final int dX = Math.max(x, mBounds.right - x);
        final int dY = Math.max(x, mBounds.bottom - y);
        final int maxRadius = (int) Math.ceil(Math.sqrt(dX * dX + dY * dY));
        final int maxRadius = mMaxRadius;
        bounds.set(x - maxRadius, y - maxRadius, x + maxRadius, y + maxRadius);
    }

    /**
     * Constrains a value within a specified asymptotic margin outside a minimum
     * and maximum.
     * Updates the center coordinates.
     */
    private static float looseConstrain(float value, float min, float max, float margin,
            float factor) {
        if (value < min) {
            return min - Math.min(margin, (float) Math.pow(min - value, factor));
        } else if (value > max) {
            return max + Math.min(margin, (float) Math.pow(value - max, factor));
        } else {
            return value;
        }
    }
    
    public static class RippleAnimator {
        /** Duration for animating the trailing edge of the ripple. */
        private static final int EXIT_DURATION = 600;

        /** Duration for animating the leading edge of the ripple. */
        private static final int ENTER_DURATION = 400;

        /** Minimum elapsed time between start of enter and exit animations. */
        private static final int EXIT_MIN_DELAY = 200;

        /** Duration for animating between inside and outside touch. */
        private static final int OUTSIDE_DURATION = 300;
    public void move(float x, float y) {
        mX = x;
        mY = y;

        /** Duration for animating pulses. */
        private static final int PULSE_DURATION = 400;
        updateInsideBounds();
        invalidateSelf();
    }

        /** Interval between pulses while inside and fully entered. */
        private static final int PULSE_INTERVAL = 400;
    /**
     * Starts the exit animation. If {@link #enter()} was called recently, the
     * animation may be postponed.
     */
    public void exit() {
        mExitFinished = false;

        /** Delay before pulses start. */
        private static final int PULSE_DELAY = 500;
        final ObjectAnimator exit = ObjectAnimator.ofFloat(this, "innerRadius", 0, mMaxRadius);
        exit.setAutoCancel(true);
        exit.setDuration(EXIT_DURATION);
        exit.setInterpolator(INTERPOLATOR);
        exit.addListener(mAnimationListener);

        /** The target ripple being animated. */
        private final Ripple mTarget;
        if (mEnter != null && mEnter.isStarted()) {
            // If we haven't been running the enter animation for long enough,
            // delay the exit animator.
            final int elapsed = (int) (mEnter.getAnimatedFraction() * mEnter.getDuration());
            final int delay = Math.max(0, EXIT_MIN_DELAY - elapsed);
            exit.setStartDelay(delay);
        }

        /** When the ripple started appearing. */
        private long mEnterTime = -1;
        exit.start();

        /** When the ripple started vanishing. */
        private long mExitTime = -1;
        final ObjectAnimator fade = ObjectAnimator.ofFloat(this, "alphaMultiplier", 0);
        fade.setAutoCancel(true);
        fade.setDuration(EXIT_DURATION);
        fade.start();

        /** When the ripple last transitioned between inside and outside touch. */
        private long mOutsideTime = -1;
        mExit = exit;
    }

        public RippleAnimator(Ripple target) {
            mTarget = target;
    private void invalidateSelf() {
        mOwner.invalidateSelf();
    }

    /**
     * Starts the enter animation.
     */
        public void enter() {
            mEnterTime = AnimationUtils.currentAnimationTimeMillis();
    private void enter() {
        final ObjectAnimator enter = ObjectAnimator.ofFloat(this, "outerRadius", mMaxRadius);
        enter.setAutoCancel(true);
        enter.setDuration(ENTER_DURATION);
        enter.setInterpolator(INTERPOLATOR);
        enter.start();

        final ObjectAnimator fade = ObjectAnimator.ofFloat(this, "alphaMultiplier", 1);
        fade.setAutoCancel(true);
        fade.setDuration(FADE_DURATION);
        fade.start();

        // TODO: Starting with a delay will still cancel the fade in.
        if (false && mPulseEnabled) {
            final ObjectAnimator pulse = ObjectAnimator.ofFloat(
                    this, "alphaMultiplier", 1, PULSE_MIN_ALPHA);
            pulse.setAutoCancel(true);
            pulse.setDuration(PULSE_DURATION + PULSE_INTERVAL);
            pulse.setRepeatCount(ObjectAnimator.INFINITE);
            pulse.setRepeatMode(ObjectAnimator.REVERSE);
            pulse.setStartDelay(PULSE_DELAY);
            pulse.start();
        }

        /**
         * Starts the exit animation. If {@link #enter()} was called recently, the
         * animation may be postponed.
         */
        public void exit() {
            final long minTime = mEnterTime + EXIT_MIN_DELAY;
            mExitTime = Math.max(minTime, AnimationUtils.currentAnimationTimeMillis());
        mEnter = enter;
    }

    /**
     * Starts the outside transition animation.
     */
        public void outside() {
            mOutsideTime = AnimationUtils.currentAnimationTimeMillis();
    private void outside() {
        final float targetRadius = mMaxOutsideRadius;
        final ObjectAnimator outside = ObjectAnimator.ofFloat(this, "outerRadius", targetRadius);
        outside.setAutoCancel(true);
        outside.setDuration(OUTSIDE_DURATION);
        outside.setInterpolator(INTERPOLATOR);
        outside.start();

        final ObjectAnimator fade = ObjectAnimator.ofFloat(this, "alphaMultiplier", 1);
        fade.setAutoCancel(true);
        fade.setDuration(FADE_DURATION);
        fade.start();
    }

    /**
         * Returns whether this ripple is currently animating.
     * Constrains a value within a specified asymptotic margin outside a minimum
     * and maximum.
     */
        public boolean isRunning() {
            final long currentTime = AnimationUtils.currentAnimationTimeMillis();
            return mEnterTime >= 0 && mEnterTime <= currentTime
                    && (mExitTime < 0 || currentTime <= mExitTime + EXIT_DURATION);
    private static float looseConstrain(float value, float min, float max, float margin,
            float factor) {
        // TODO: Can we use actual spring physics here?
        if (value < min) {
            return min - Math.min(margin, (float) Math.pow(min - value, factor));
        } else if (value > max) {
            return max + Math.min(margin, (float) Math.pow(value - max, factor));
        } else {
            return value;
        }
    }

    private final AnimatorListener mAnimationListener = new AnimatorListener() {
        @Override
        public void onAnimationStart(Animator animation) {
        }

        @Override
        public void onAnimationRepeat(Animator animation) {
        }

        public void update() {
            // Track three states:
            // - Enter: touch begins, affects outer radius
            // - Outside: touch moves outside bounds, affects maximum outer radius
            // - Exit: touch ends, affects inner radius
            final long currentTime = AnimationUtils.currentAnimationTimeMillis();
            mTarget.mEnterState = mEnterTime < 0 ? 0 : INTERPOLATOR.getInterpolation(
                    MathUtils.constrain((currentTime - mEnterTime) / (float) ENTER_DURATION, 0, 1));
            mTarget.mExitState = mExitTime < 0 ? 0 : INTERPOLATOR.getInterpolation(
                    MathUtils.constrain((currentTime - mExitTime) / (float) EXIT_DURATION, 0, 1));
            mTarget.mOutsideState = mOutsideTime < 0 ? 1 : INTERPOLATOR.getInterpolation(
                    MathUtils.constrain((currentTime - mOutsideTime) / (float) OUTSIDE_DURATION, 0, 1));

            // Pulse is a little more complicated.
            if (mTarget.mPulse) {
                final long pulseTime = (currentTime - mEnterTime - ENTER_DURATION - PULSE_DELAY);
                mTarget.mPulseState = pulseTime < 0 ? -1
                        : (pulseTime % (PULSE_INTERVAL + PULSE_DURATION)) / (float) PULSE_DURATION;
        @Override
        public void onAnimationEnd(Animator animation) {
            if (animation == mExit) {
                mExitFinished = true;
                mOuterRadius = 0;
                mInnerRadius = 0;
                mAlphaMultiplier = 1;
            }
        }

        @Override
        public void onAnimationCancel(Animator animation) {
        }
    };
}
+38 −97

File changed.

Preview size limit exceeded, changes collapsed.