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

Commit 10c66afb authored by Jim Miller's avatar Jim Miller
Browse files

Fix 6398209: Improve MultiWaveView animations and interaction

TargetDrawables now have a "home" position they can be scaled about.
Added new "focused" state to TargetDrawable.  This is used to distinguish between highlight and selection.
Updated target icons to use new focused state. Currently re-uses "activated" icon.
Change to event handling to allow cancel events when a target is highlighted to cause a selection.
Cleaned up MultiWaveView initialization code.

MultiWaveView animation improvements:
 - new scale animation when center handle is touched.
 - switched to using indices instead of foreach loops in critical path code to avoid creating temporary objects.
 - updated and simplified animation code.

Change-Id: I593c021475f1644c73bdb9f84855e6a9fec7c0ab
parent a482f942
Loading
Loading
Loading
Loading
+173 −130
Original line number Diff line number Diff line
@@ -69,16 +69,19 @@ public class MultiWaveView extends View {
        public void onGrabbedStateChange(View v, int handle);
    }

    // Tune-able parameters
    // Tuneable parameters for animation
    private static final int CHEVRON_INCREMENTAL_DELAY = 160;
    private static final int CHEVRON_ANIMATION_DURATION = 850;
    private static final int RETURN_TO_HOME_DELAY = 1200;
    private static final int RETURN_TO_HOME_DURATION = 300;
    private static final int HIDE_ANIMATION_DELAY = 200;
    private static final int HIDE_ANIMATION_DURATION = RETURN_TO_HOME_DELAY;
    private static final int SHOW_ANIMATION_DURATION = 0;
    private static final int HIDE_ANIMATION_DURATION = 200;
    private static final int SHOW_ANIMATION_DURATION = 200;
    private static final int SHOW_ANIMATION_DELAY = 0;
    private static final float TAP_RADIUS_SCALE_ACCESSIBILITY_ENABLED = 1.3f;
    private static final long RING_EXPAND_DURATION = 200;
    private static final float TARGET_INITIAL_POSITION_SCALE = 0.8f;

    private TimeInterpolator mChevronAnimationInterpolator = Ease.Quad.easeOut;

    private ArrayList<TargetDrawable> mTargetDrawables = new ArrayList<TargetDrawable>();
@@ -149,6 +152,7 @@ public class MultiWaveView extends View {
    private int mHorizontalInset;
    private int mVerticalInset;
    private int mGravity = Gravity.TOP;
    private boolean mInitialLayout = true;

    public MultiWaveView(Context context) {
        this(context, null);
@@ -274,7 +278,7 @@ public class MultiWaveView extends View {
        final int minimumHeight = getSuggestedMinimumHeight();
        int computedWidth = resolveMeasured(widthMeasureSpec, minimumWidth);
        int computedHeight = resolveMeasured(heightMeasureSpec, minimumHeight);
        setupGravity((computedWidth - minimumWidth), (computedHeight - minimumHeight));
        computeInsets((computedWidth - minimumWidth), (computedHeight - minimumHeight));
        setMeasuredDimension(computedWidth, computedHeight);
    }

@@ -315,17 +319,17 @@ public class MultiWaveView extends View {
     */
    private void startChevronAnimation() {
        final float r = mHandleDrawable.getWidth() * 0.4f;
        final float chevronAnimationDistance = mOuterRadius * 0.9f;
        final float chevronAnimationDistance = mOuterRadius * 0.9f / 2.0f;
        final float from[][] = {
                {mWaveCenterX - r, mWaveCenterY},  // left
                {mWaveCenterX + r, mWaveCenterY},  // right
                {mWaveCenterX, mWaveCenterY - r},  // top
                {mWaveCenterX, mWaveCenterY + r} }; // bottom
                { -r, 0},  // left
                { +r, 0},  // right
                {0, -r},  // top
                {0, +r} }; // bottom
        final float to[][] = {
                {mWaveCenterX - chevronAnimationDistance, mWaveCenterY},  // left
                {mWaveCenterX + chevronAnimationDistance, mWaveCenterY},  // right
                {mWaveCenterX, mWaveCenterY - chevronAnimationDistance},  // top
                {mWaveCenterX, mWaveCenterY + chevronAnimationDistance} }; // bottom
                { -chevronAnimationDistance, 0},  // left
                { chevronAnimationDistance, 0},  // right
                { 0, -chevronAnimationDistance},  // top
                { 0, +chevronAnimationDistance} }; // bottom

        mChevronAnimations.clear();
        final float startScale = 0.5f;
@@ -416,31 +420,24 @@ public class MultiWaveView extends View {
        mHandleDrawable.setAlpha(targetHit ? 0.0f : 1.0f);
        if (targetHit) {
            mTargetDrawables.get(activeTarget).setState(TargetDrawable.STATE_ACTIVE);

            hideUnselected(activeTarget);

            // Inform listener of any active targets.  Typically only one will be active.
            if (DEBUG) Log.v(TAG, "Finish with target hit = " + targetHit);
            dispatchTriggerEvent(mActiveTarget);
            mHandleAnimation = Tweener.to(mHandleDrawable, 0,
                    "ease", Ease.Quart.easeOut,
                    "delay", RETURN_TO_HOME_DELAY,
                    "alpha", 1.0f,
                    "x", mWaveCenterX,
                    "y", mWaveCenterY,
                    "onUpdate", mUpdateListener,
                    "onComplete", mResetListener);
        } else {
            // Animate finger outline back to home position
            mHandleAnimation = Tweener.to(mHandleDrawable, RETURN_TO_HOME_DURATION,
        }

        // Animate handle back to the center based on current state.
        int delay = targetHit ? RETURN_TO_HOME_DELAY : 0;
        int duration = targetHit ? 0 : RETURN_TO_HOME_DURATION;
        mHandleAnimation = Tweener.to(mHandleDrawable, duration,
                "ease", Ease.Quart.easeOut,
                    "delay", 0,
                "delay", delay,
                "alpha", 1.0f,
                    "x", mWaveCenterX,
                    "y", mWaveCenterY,
                "x", 0,
                "y", 0,
                "onUpdate", mUpdateListener,
                    "onComplete", mDragging ? mResetListenerWithPing : mResetListener);
        }
                "onComplete", (mDragging && !targetHit) ? mResetListenerWithPing : mResetListener);

        setGrabbedState(OnTriggerListener.NO_HANDLE);
    }
@@ -461,27 +458,30 @@ public class MultiWaveView extends View {
        // Note: these animations should complete at the same time so that we can swap out
        // the target assets asynchronously from the setTargetResources() call.
        mAnimatingTargets = animate;
        if (animate) {
        final int duration = animate ? HIDE_ANIMATION_DURATION : 0;
            for (TargetDrawable target : mTargetDrawables) {
        final int delay = animate ? HIDE_ANIMATION_DELAY : 0;
        final int length = mTargetDrawables.size();
        for (int i = 0; i < length; i++) {
            TargetDrawable target = mTargetDrawables.get(i);
            target.setState(TargetDrawable.STATE_INACTIVE);
            mTargetAnimations.add(Tweener.to(target, duration,
                    "ease", Ease.Cubic.easeOut,
                    "alpha", 0.0f,
                        "delay", HIDE_ANIMATION_DELAY,
                    "scaleX", TARGET_INITIAL_POSITION_SCALE,
                    "scaleY", TARGET_INITIAL_POSITION_SCALE,
                    "delay", delay,
                    "onUpdate", mUpdateListener));
        }

        float ringScaleTarget = mActiveTarget != -1 ? 1.5f : 0.5f;
        mTargetAnimations.add(Tweener.to(mOuterRing, duration,
                "ease", Ease.Cubic.easeOut,
                "alpha", 0.0f,
                    "delay", HIDE_ANIMATION_DELAY,
                "scaleX", ringScaleTarget,
                "scaleY", ringScaleTarget,
                "delay", delay,
                "onUpdate", mUpdateListener,
                "onComplete", mTargetUpdateListener));
        } else {
            for (TargetDrawable target : mTargetDrawables) {
                target.setState(TargetDrawable.STATE_INACTIVE);
                target.setAlpha(0.0f);
            }
            mOuterRing.setAlpha(0.0f);
        }
    }

    private void showTargets(boolean animate) {
@@ -489,26 +489,31 @@ public class MultiWaveView extends View {
            stopTargetAnimation();
        }
        mAnimatingTargets = animate;
        if (animate) {
            for (TargetDrawable target : mTargetDrawables) {
        final int delay = animate ? SHOW_ANIMATION_DELAY : 0;
        final int length = mTargetDrawables.size();
        for (int i = 0; i < length; i++) {
            TargetDrawable target = mTargetDrawables.get(i);
            target.setState(TargetDrawable.STATE_INACTIVE);
                mTargetAnimations.add(Tweener.to(target, SHOW_ANIMATION_DURATION,
            target.setScaleX(TARGET_INITIAL_POSITION_SCALE);
            target.setScaleY(TARGET_INITIAL_POSITION_SCALE);
            mTargetAnimations.add(Tweener.to(target, animate ? SHOW_ANIMATION_DURATION : 0,
                    "ease", Ease.Cubic.easeOut,
                    "alpha", 1.0f,
                        "delay", SHOW_ANIMATION_DELAY,
                    "scaleX", 1.0f,
                    "scaleY", 1.0f,
                    "delay", delay,
                    "onUpdate", mUpdateListener));
        }
            mTargetAnimations.add(Tweener.to(mOuterRing, SHOW_ANIMATION_DURATION,
        mOuterRing.setScaleX(0.5f);
        mOuterRing.setScaleY(0.5f);
        mTargetAnimations.add(Tweener.to(mOuterRing, animate ? RING_EXPAND_DURATION : 0,
                "ease", Ease.Cubic.easeOut,
                "alpha", 1.0f,
                    "delay", SHOW_ANIMATION_DELAY,
                "scaleX", 1.0f,
                "scaleY", 1.0f,
                "delay", delay,
                "onUpdate", mUpdateListener,
                "onComplete", mTargetUpdateListener));
        } else {
            for (TargetDrawable target : mTargetDrawables) {
                target.setState(TargetDrawable.STATE_INACTIVE);
                target.setAlpha(1.0f);
            }
            mOuterRing.setAlpha(1.0f);
        }
    }

    private void stopTargetAnimation() {
@@ -545,7 +550,8 @@ public class MultiWaveView extends View {
            mMaxTargetHeight = maxHeight;
            requestLayout(); // required to resize layout and call updateTargetPositions()
        } else {
            updateTargetPositions();
            updateTargetPositions(mWaveCenterX, mWaveCenterY);
            updateChevronPositions(mWaveCenterX, mWaveCenterY);
        }
        array.recycle();
    }
@@ -645,8 +651,8 @@ public class MultiWaveView extends View {
        stopTargetAnimation();
        hideChevrons();
        hideTargets(animate);
        mHandleDrawable.setX(mWaveCenterX);
        mHandleDrawable.setY(mWaveCenterY);
        mHandleDrawable.setX(0);
        mHandleDrawable.setY(0);
        mHandleDrawable.setState(TargetDrawable.STATE_INACTIVE);
        Tweener.reset();
    }
@@ -677,7 +683,7 @@ public class MultiWaveView extends View {

            case MotionEvent.ACTION_CANCEL:
                if (DEBUG) Log.v(TAG, "*** CANCEL ***");
                // handleMove(event);
                handleMove(event);
                handleCancel(event);
                handled = true;
                break;
@@ -687,7 +693,6 @@ public class MultiWaveView extends View {
    }

    private void moveHandleTo(float x, float y, boolean animate) {
        // TODO: animate the handle based on the current state/position
        mHandleDrawable.setX(x);
        mHandleDrawable.setY(y);
    }
@@ -707,7 +712,14 @@ public class MultiWaveView extends View {

    private void handleCancel(MotionEvent event) {
        if (DEBUG && mDragging) Log.v(TAG, "** Handle CANCEL");
        mActiveTarget = -1; // Drop the active target if canceled.

        // We should drop the active target here but it interferes with
        // moving off the screen in the direction of the navigation bar. At some point we may
        // want to revisit how we handle this. For now we'll allow a canceled event to
        // activate the current target.

        // mActiveTarget = -1; // Drop the active target if canceled.

        switchToState(STATE_FINISH, event.getX(), event.getY());
    }

@@ -719,24 +731,25 @@ public class MultiWaveView extends View {

        int activeTarget = -1;
        final int historySize = event.getHistorySize();
        final boolean singleTarget = mTargetDrawables.size() == 1;
        float x = 0.0f;
        float y = 0.0f;
        for (int k = 0; k < historySize + 1; k++) {
            float x = k < historySize ? event.getHistoricalX(k) : event.getX();
            float y = k < historySize ? event.getHistoricalY(k) : event.getY();
            float tx = x - mWaveCenterX;
            float ty = y - mWaveCenterY;
            float eventX = k < historySize ? event.getHistoricalX(k) : event.getX();
            float eventY = k < historySize ? event.getHistoricalY(k) : event.getY();
            // tx and ty are relative to wave center
            float tx = eventX - mWaveCenterX;
            float ty = eventY - mWaveCenterY;
            float touchRadius = (float) Math.sqrt(dist2(tx, ty));
            final float scale = touchRadius > mOuterRadius ? mOuterRadius / touchRadius : 1.0f;
            float limitX = mWaveCenterX + tx * scale;
            float limitY = mWaveCenterY + ty * scale;
            float limitX = tx * scale;
            float limitY = ty * scale;

            boolean singleTarget = mTargetDrawables.size() == 1;
            if (singleTarget) {
                // Snap to outer ring if there's only one target
                float snapRadius = mOuterRadius - mSnapMargin;
                if (touchRadius > snapRadius) {
                    activeTarget = 0;
                    x = limitX;
                    y = limitY;
                }
            } else {
                // If there's more than one target, snap to the closest one less than hitRadius away.
@@ -753,36 +766,49 @@ public class MultiWaveView extends View {
                        best = dist2;
                    }
                }
            }
            x = limitX;
            y = limitY;
        }

        if (activeTarget != -1) {
            switchToState(STATE_SNAP, x,y);
                float newX = singleTarget ? limitX : mTargetDrawables.get(activeTarget).getX();
                float newY = singleTarget ? limitY : mTargetDrawables.get(activeTarget).getY();
            TargetDrawable target = mTargetDrawables.get(activeTarget);
            float newX = singleTarget ? x : target.getX();
            float newY = singleTarget ? y : target.getY();
            moveHandleTo(newX, newY, false);
                TargetDrawable currentTarget = mTargetDrawables.get(activeTarget);
                if (currentTarget.hasState(TargetDrawable.STATE_FOCUSED)) {
                    currentTarget.setState(TargetDrawable.STATE_FOCUSED);
                    mHandleDrawable.setAlpha(0.0f);
                }
        } else {
            switchToState(STATE_TRACKING, x, y);
            moveHandleTo(x, y, false);
            mHandleDrawable.setAlpha(1.0f);
        }
        }

        // Draw handle outside parent's bounds
        invalidateGlobalRegion(mHandleDrawable);

        if (mActiveTarget != activeTarget && activeTarget != -1) {
        if (mActiveTarget != activeTarget) {
            // Defocus the old target
            if (mActiveTarget != -1) {
                TargetDrawable target = mTargetDrawables.get(mActiveTarget);
                if (target.hasState(TargetDrawable.STATE_FOCUSED)) {
                    target.setState(TargetDrawable.STATE_INACTIVE);
                    mHandleDrawable.setAlpha(1.0f);
                }
            }
            // Focus the new target
            if (activeTarget != -1) {
                TargetDrawable target = mTargetDrawables.get(activeTarget);
                if (target.hasState(TargetDrawable.STATE_FOCUSED)) {
                    target.setState(TargetDrawable.STATE_FOCUSED);
                    mHandleDrawable.setAlpha(0.0f);
                }
                dispatchGrabbedEvent(activeTarget);
                if (AccessibilityManager.getInstance(mContext).isEnabled()) {
                    String targetContentDescription = getTargetDescription(activeTarget);
                    announceText(targetContentDescription);
                }
            }
        }
        mActiveTarget = activeTarget;
    }

@@ -831,21 +857,21 @@ public class MultiWaveView extends View {
    private boolean trySwitchToFirstTouchState(MotionEvent event) {
        final float x = event.getX();
        final float y = event.getY();
        final float dx = x - mWaveCenterX;
        final float dy = y - mWaveCenterY;
        if (mAlwaysTrackFinger || dist2(dx,dy) <= getScaledTapRadiusSquared()) {
        final float tx = x - mWaveCenterX;
        final float ty = y - mWaveCenterY;
        if (mAlwaysTrackFinger || dist2(tx,ty) <= getScaledTapRadiusSquared()) {
            if (DEBUG) Log.v(TAG, "** Handle HIT");
            switchToState(STATE_FIRST_TOUCH, x, y);
            moveHandleTo(x, y, false);
            moveHandleTo(tx, ty, false);
            mDragging = true;
            return true;
        }
        return false;
    }

    private void performInitialLayout(float centerX, float centerY) {
    private void assignDefaultsIfNeeded(float centerX, float centerY) {
        if (mOuterRadius == 0.0f) {
            mOuterRadius = 0.5f*(float) Math.sqrt(dist2(centerX, centerY));
            mOuterRadius = 0.5f*(float) Math.hypot(centerX, centerY);
        }
        if (mHitRadius == 0.0f) {
            // Use the radius of inscribed circle of the first target.
@@ -855,12 +881,9 @@ public class MultiWaveView extends View {
            mSnapMargin = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
                    SNAP_MARGIN_DEFAULT, getContext().getResources().getDisplayMetrics());
        }
        hideChevrons();
        hideTargets(false);
        moveHandleTo(centerX, centerY, false);
    }

    private void setupGravity(int dx, int dy) {
    private void computeInsets(int dx, int dy) {
        final int layoutDirection = getResolvedLayoutDirection();
        final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, layoutDirection);

@@ -899,29 +922,49 @@ public class MultiWaveView extends View {
                + Math.max(width, mMaxTargetWidth + mOuterRing.getWidth()) / 2;
        float newWaveCenterY = mVerticalOffset + mVerticalInset
                + Math.max(height, + mMaxTargetHeight + mOuterRing.getHeight()) / 2;
        if (newWaveCenterX != mWaveCenterX || newWaveCenterY != mWaveCenterY) {
            if (mWaveCenterX == 0 && mWaveCenterY == 0) {
                performInitialLayout(newWaveCenterX, newWaveCenterY);

        assignDefaultsIfNeeded(newWaveCenterX, newWaveCenterY);

        if (mInitialLayout) {
            hideChevrons();
            hideTargets(false);
            moveHandleTo(0, 0, false);
            mInitialLayout = false;
        }

        mOuterRing.setPositionX(newWaveCenterX);
        mOuterRing.setPositionY(newWaveCenterY);

        mHandleDrawable.setPositionX(newWaveCenterX);
        mHandleDrawable.setPositionY(newWaveCenterY);

        updateTargetPositions(newWaveCenterX, newWaveCenterY);
        updateChevronPositions(newWaveCenterX, newWaveCenterY);

        mWaveCenterX = newWaveCenterX;
        mWaveCenterY = newWaveCenterY;

            mOuterRing.setX(mWaveCenterX);
            mOuterRing.setY(Math.max(mWaveCenterY, mWaveCenterY));
        }
        updateTargetPositions();
        if (DEBUG) dump();
    }

    private void updateTargetPositions() {
    private void updateTargetPositions(float centerX, float centerY) {
        // Reposition the target drawables if the view changed.
        for (int i = 0; i < mTargetDrawables.size(); i++) {
            final TargetDrawable targetIcon = mTargetDrawables.get(i);
            double angle = -2.0f * Math.PI * i / mTargetDrawables.size();
            float xPosition = mWaveCenterX + mOuterRadius * (float) Math.cos(angle);
            float yPosition = mWaveCenterY + mOuterRadius * (float) Math.sin(angle);
            targetIcon.setX(xPosition);
            targetIcon.setY(yPosition);
            targetIcon.setPositionX(centerX);
            targetIcon.setPositionY(centerY);
            targetIcon.setX(mOuterRadius * (float) Math.cos(angle));
            targetIcon.setY(mOuterRadius * (float) Math.sin(angle));
        }
    }

    private void updateChevronPositions(float centerX, float centerY) {
        for (TargetDrawable target : mChevronDrawables) {
            if (target != null) {
                target.setPositionX(centerX);
                target.setPositionY(centerY);
            }
        }
    }

+22 −3
Original line number Diff line number Diff line
@@ -32,10 +32,13 @@ public class TargetDrawable {
    public static final int[] STATE_INACTIVE =
            { android.R.attr.state_enabled, -android.R.attr.state_active };
    public static final int[] STATE_FOCUSED =
            { android.R.attr.state_enabled, android.R.attr.state_focused };
            { android.R.attr.state_enabled, -android.R.attr.state_active,
                android.R.attr.state_focused };

    private float mTranslationX = 0.0f;
    private float mTranslationY = 0.0f;
    private float mPositionX = 0.0f;
    private float mPositionY = 0.0f;
    private float mScaleX = 1.0f;
    private float mScaleY = 1.0f;
    private float mAlpha = 1.0f;
@@ -196,6 +199,22 @@ public class TargetDrawable {
        return mAlpha;
    }

    public void setPositionX(float x) {
        mPositionX = x;
    }

    public void setPositionY(float y) {
        mPositionY = y;
    }

    public float getPositionX() {
        return mPositionX;
    }

    public float getPositionY() {
        return mPositionY;
    }

    public int getWidth() {
        return mDrawable != null ? mDrawable.getIntrinsicWidth() : 0;
    }
@@ -209,8 +228,8 @@ public class TargetDrawable {
            return;
        }
        canvas.save(Canvas.MATRIX_SAVE_FLAG);
        canvas.translate(mTranslationX, mTranslationY);
        canvas.scale(mScaleX, mScaleY);
        canvas.scale(mScaleX, mScaleY, mPositionX, mPositionY);
        canvas.translate(mTranslationX + mPositionX, mTranslationY + mPositionY);
        canvas.translate(-0.5f * getWidth(), -0.5f * getHeight());
        mDrawable.setAlpha((int) Math.round(mAlpha * 255f));
        mDrawable.draw(canvas);
+6 −0
Original line number Diff line number Diff line
@@ -27,4 +27,10 @@
        android:state_focused="false"
        android:drawable="@drawable/ic_lockscreen_answer_active" />

    <item
        android:state_enabled="true"
        android:state_active="false"
        android:state_focused="true"
        android:drawable="@drawable/ic_lockscreen_answer_active" />

</selector>
+6 −0
Original line number Diff line number Diff line
@@ -27,4 +27,10 @@
        android:state_focused="false"
        android:drawable="@drawable/ic_lockscreen_camera_activated" />

    <item
        android:state_enabled="true"
        android:state_active="false"
        android:state_focused="true"
        android:drawable="@drawable/ic_lockscreen_camera_activated" />

</selector>
+6 −0
Original line number Diff line number Diff line
@@ -27,4 +27,10 @@
        android:state_focused="false"
        android:drawable="@drawable/ic_lockscreen_decline_activated" />

    <item
        android:state_enabled="true"
        android:state_active="false"
        android:state_focused="true"
        android:drawable="@drawable/ic_lockscreen_decline_activated" />

</selector>
Loading