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

Commit 53d1cfe2 authored by Alan Viverette's avatar Alan Viverette
Browse files

Cleaning up TouchFeedbackDrawable and Ripple APIs

Change-Id: I73ce0507ce98140c01fe77cc277b0fea75350be9
parent 61bc9f37
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.