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

Commit 10a73a04 authored by Josh Tsuji's avatar Josh Tsuji Committed by Android (Google) Code Review
Browse files

Merge "Updates individual bubble dismissal logic and animation."

parents 5fe2936f 442b6277
Loading
Loading
Loading
Loading
+35 −37
Original line number Diff line number Diff line
@@ -58,7 +58,7 @@ import java.math.RoundingMode;
/**
 * Renders bubbles in a stack and handles animating expanded and collapsed states.
 */
public class BubbleStackView extends FrameLayout implements BubbleTouchHandler.FloatingView {
public class BubbleStackView extends FrameLayout {
    private static final String TAG = "BubbleStackView";

    /**
@@ -146,8 +146,9 @@ public class BubbleStackView extends FrameLayout implements BubbleTouchHandler.F

        mBubbleData = data;
        mInflater = LayoutInflater.from(context);
        mTouchHandler = new BubbleTouchHandler(context);
        mTouchHandler = new BubbleTouchHandler(context, this);
        setOnTouchListener(mTouchHandler);
        mInflater = LayoutInflater.from(context);

        Resources res = getResources();
        mBubbleSize = res.getDimensionPixelSize(R.dimen.individual_bubble_size);
@@ -199,6 +200,8 @@ public class BubbleStackView extends FrameLayout implements BubbleTouchHandler.F
                        .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY));

        setClipChildren(false);

        mBubbleContainer.bringToFront();
    }

    @Override
@@ -484,7 +487,8 @@ public class BubbleStackView extends FrameLayout implements BubbleTouchHandler.F
            if (shouldExpand) {
                mBubbleContainer.setController(mExpandedAnimationController);
                mExpandedAnimationController.expandFromStack(
                                mStackAnimationController.getStackPosition(), () -> {
                        mStackAnimationController.getStackPosition(),
                        () -> {
                            updatePointerPosition();
                            updateAfter.run();
                        });
@@ -544,55 +548,49 @@ public class BubbleStackView extends FrameLayout implements BubbleTouchHandler.F
                : null;
    }

    @Override
    public void setPosition(float x, float y) {
        mStackAnimationController.moveFirstBubbleWithStackFollowing(x, y);
    public PointF getStackPosition() {
        return mStackAnimationController.getStackPosition();
    }

    @Override
    public void setPositionX(float x) {
        // Unsupported, use setPosition(x, y).
    /** Called when a drag operation on an individual bubble has started. */
    public void onBubbleDragStart(View bubble) {
        mExpandedAnimationController.prepareForBubbleDrag(bubble);
    }

    @Override
    public void setPositionY(float y) {
        // Unsupported, use setPosition(x, y).
    /** Called with the coordinates to which an individual bubble has been dragged. */
    public void onBubbleDragged(View bubble, float x, float y) {
        if (!mIsExpanded || mIsAnimating) {
            return;
        }

    @Override
    public PointF getPosition() {
        return mStackAnimationController.getStackPosition();
        mExpandedAnimationController.dragBubbleOut(bubble, x, y);
    }

    /** Called when a drag operation on an individual bubble has started. */
    public void onBubbleDragStart(BubbleView bubble) {
        // TODO: Save position and snap back if not dismissed.
    /** Called when a drag operation on an individual bubble has finished. */
    public void onBubbleDragFinish(
            View bubble, float x, float y, float velX, float velY, boolean dismissed) {
        if (!mIsExpanded || mIsAnimating) {
            return;
        }

    /** Called with the coordinates to which an individual bubble has been dragged. */
    public void onBubbleDragged(BubbleView bubble, float x, float y) {
        bubble.setTranslationX(x);
        bubble.setTranslationY(y);
        if (dismissed) {
            mExpandedAnimationController.prepareForDismissalWithVelocity(bubble, velX, velY);
        } else {
            mExpandedAnimationController.snapBubbleBack(bubble, velX, velY);
        }

    /** Called when a drag operation on an individual bubble has finished. */
    public void onBubbleDragFinish(BubbleView bubble, float x, float y, float velX, float velY) {
        // TODO: Add fling to bottom to dismiss.
    }

    void onDragStart() {
        if (mIsExpanded) {
        if (mIsExpanded || mIsAnimating) {
            return;
        }

        mStackAnimationController.cancelStackPositionAnimations();
        mBubbleContainer.setController(mStackAnimationController);
        mIsAnimating = false;
    }

    void onDragged(float x, float y) {
        // TODO: We can drag if animating - just need to reroute inflight anims to drag point.
        if (mIsExpanded) {
        if (mIsExpanded || mIsAnimating) {
            return;
        }

@@ -744,9 +742,9 @@ public class BubbleStackView extends FrameLayout implements BubbleTouchHandler.F

    private void updatePointerPosition() {
        if (mExpandedBubble != null) {
            float pointerPosition = mExpandedBubble.iconView.getPosition().x
            float pointerPosition = mExpandedBubble.iconView.getTranslationX()
                    + (mExpandedBubble.iconView.getWidth() / 2f);
            mExpandedBubble.expandedView.setPointerPosition(pointerPosition);
            mExpandedBubble.expandedView.setPointerPosition((int) pointerPosition);
        }
    }

@@ -772,7 +770,7 @@ public class BubbleStackView extends FrameLayout implements BubbleTouchHandler.F
     * @return the normalized x-axis position of the bubble stack rounded to 4 decimal places.
     */
    public float getNormalizedXPosition() {
        return new BigDecimal(getPosition().x / mDisplaySize.x)
        return new BigDecimal(getStackPosition().x / mDisplaySize.x)
                .setScale(4, RoundingMode.CEILING.HALF_UP)
                .floatValue();
    }
@@ -781,7 +779,7 @@ public class BubbleStackView extends FrameLayout implements BubbleTouchHandler.F
     * @return the normalized y-axis position of the bubble stack rounded to 4 decimal places.
     */
    public float getNormalizedYPosition() {
        return new BigDecimal(getPosition().y / mDisplaySize.y)
        return new BigDecimal(getStackPosition().y / mDisplaySize.y)
                .setScale(4, RoundingMode.CEILING.HALF_UP)
                .floatValue();
    }
+65 −103
Original line number Diff line number Diff line
@@ -34,17 +34,16 @@ import com.android.systemui.pip.phone.PipDismissViewController;
 * dismissing, and flings.
 */
class BubbleTouchHandler implements View.OnTouchListener {
    /** Velocity required to dismiss a bubble without dragging it into the dismiss target. */
    private static final float DISMISS_MIN_VELOCITY = 4000f;

    private final PointF mTouchDown = new PointF();
    private final PointF mViewPositionOnTouchDown = new PointF();
    private final BubbleStackView mStack;

    private BubbleController mController = Dependency.get(BubbleController.class);
    private PipDismissViewController mDismissViewController;

    // The position of the bubble on down event
    private float mBubbleDownPosX;
    private float mBubbleDownPosY;
    // The touch position on down event
    private float mDownX = -1;
    private float mDownY = -1;

    private boolean mMovedEnough;
    private int mTouchSlopSquared;
    private VelocityTracker mVelocityTracker;
@@ -58,65 +57,42 @@ class BubbleTouchHandler implements View.OnTouchListener {
        }
    };

    // Bubble being dragged from the row of bubbles when the stack is expanded
    private BubbleView mBubbleDraggingOut;

    /**
     * Views movable by this touch handler should implement this interface.
     */
    public interface FloatingView {

        /**
         * Sets the position of the view.
         */
        void setPosition(float x, float y);

        /**
         * Sets the x position of the view.
         */
        void setPositionX(float x);

        /**
         * Sets the y position of the view.
         */
        void setPositionY(float y);

        /**
         * @return the position of the view.
         */
        PointF getPosition();
    }
    /** View that was initially touched, when we received the first ACTION_DOWN event. */
    private View mTouchedView;

    public BubbleTouchHandler(Context context) {
    BubbleTouchHandler(Context context, BubbleStackView stackView) {
        final int touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
        mTouchSlopSquared = touchSlop * touchSlop;
        mDismissViewController = new PipDismissViewController(context);
        mStack = stackView;
    }

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        int action = event.getActionMasked();

        BubbleStackView stack = (BubbleStackView) v;
        View targetView = mBubbleDraggingOut != null
                ? mBubbleDraggingOut
                : stack.getTargetView(event);
        boolean isFloating = targetView instanceof FloatingView;
        if (!isFloating || targetView == null || action == MotionEvent.ACTION_OUTSIDE) {
            stack.collapseStack();
        final int action = event.getActionMasked();

        // If we aren't currently in the process of touching a view, figure out what we're touching.
        // It'll be the stack, an individual bubble, or nothing.
        if (mTouchedView == null) {
            mTouchedView = mStack.getTargetView(event);
        }

        // If this is an ACTION_OUTSIDE event, or the stack reported that we aren't touching
        // anything, collapse the stack.
        if (action == MotionEvent.ACTION_OUTSIDE || mTouchedView == null) {
            mStack.collapseStack();
            cleanUpDismissTarget();
            resetTouches();
            mTouchedView = null;
            return false;
        }

        FloatingView floatingView = (FloatingView) targetView;
        boolean isBubbleStack = floatingView instanceof BubbleStackView;
        final boolean isStack = mStack.equals(mTouchedView);
        final float rawX = event.getRawX();
        final float rawY = event.getRawY();

        PointF startPos = floatingView.getPosition();
        float rawX = event.getRawX();
        float rawY = event.getRawY();
        float x = mBubbleDownPosX + rawX - mDownX;
        float y = mBubbleDownPosY + rawY - mDownY;
        // The coordinates of the touch event, in terms of the touched view's position.
        final float viewX = mViewPositionOnTouchDown.x + rawX - mTouchDown.x;
        final float viewY = mViewPositionOnTouchDown.y + rawY - mTouchDown.y;
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                trackMovement(event);
@@ -124,87 +100,83 @@ class BubbleTouchHandler implements View.OnTouchListener {
                mDismissViewController.createDismissTarget();
                mHandler.postDelayed(mShowDismissAffordance, SHOW_TARGET_DELAY);

                mBubbleDownPosX = startPos.x;
                mBubbleDownPosY = startPos.y;
                mDownX = rawX;
                mDownY = rawY;
                mMovedEnough = false;
                mTouchDown.set(rawX, rawY);

                if (isBubbleStack) {
                    stack.onDragStart();
                if (isStack) {
                    mViewPositionOnTouchDown.set(mStack.getStackPosition());
                    mStack.onDragStart();
                } else {
                    stack.onBubbleDragStart((BubbleView) floatingView);
                    mViewPositionOnTouchDown.set(
                            mTouchedView.getTranslationX(), mTouchedView.getTranslationY());
                    mStack.onBubbleDragStart(mTouchedView);
                }

                break;

            case MotionEvent.ACTION_MOVE:
                trackMovement(event);
                final float deltaX = rawX - mTouchDown.x;
                final float deltaY = rawY - mTouchDown.y;

                if (mBubbleDownPosX == -1 || mDownX == -1) {
                    mBubbleDownPosX = startPos.x;
                    mBubbleDownPosY = startPos.y;
                    mDownX = rawX;
                    mDownY = rawY;
                }
                final float deltaX = rawX - mDownX;
                final float deltaY = rawY - mDownY;
                if ((deltaX * deltaX) + (deltaY * deltaY) > mTouchSlopSquared && !mMovedEnough) {
                    mMovedEnough = true;
                }

                if (mMovedEnough) {
                    if (floatingView instanceof BubbleView) {
                        mBubbleDraggingOut = ((BubbleView) floatingView);
                        stack.onBubbleDragged(mBubbleDraggingOut, x, y);
                    if (isStack) {
                        mStack.onDragged(viewX, viewY);
                    } else {
                        stack.onDragged(x, y);
                        mStack.onBubbleDragged(mTouchedView, viewX, viewY);
                    }
                }

                // TODO - when we're in the target stick to it / animate in some way?
                mInDismissTarget = mDismissViewController.updateTarget(
                        isBubbleStack ? stack.getBubbleAt(0) : (View) floatingView);
                        isStack ? mStack.getBubbleAt(0) : mTouchedView);
                break;

            case MotionEvent.ACTION_CANCEL:
                resetTouches();
                mTouchedView = null;
                cleanUpDismissTarget();
                break;

            case MotionEvent.ACTION_UP:
                trackMovement(event);
                if (mInDismissTarget) {
                    if (isBubbleStack) {
                if (mInDismissTarget && isStack) {
                    mController.dismissStack();
                    } else {
                        mController.removeBubble(((BubbleView) floatingView).getKey());
                    }
                } else if (mMovedEnough) {
                    mVelocityTracker.computeCurrentVelocity(1000);
                    final float velX = mVelocityTracker.getXVelocity();
                    final float velY = mVelocityTracker.getYVelocity();
                    if (isBubbleStack) {
                        stack.onDragFinish(x, y, velX, velY);
                    if (isStack) {
                        mStack.onDragFinish(viewX, viewY, velX, velY);
                    } else {
                        stack.onBubbleDragFinish(mBubbleDraggingOut, x, y, velX, velY);
                    }
                } else if (floatingView.equals(stack.getExpandedBubbleView())) {
                    stack.collapseStack();
                } else if (isBubbleStack) {
                    if (stack.isExpanded()) {
                        stack.collapseStack();
                        final boolean dismissed = mInDismissTarget || velY > DISMISS_MIN_VELOCITY;
                        mStack.onBubbleDragFinish(
                                mTouchedView, viewX, viewY, velX, velY, /* dismissed */ dismissed);
                        if (dismissed) {
                            mController.removeBubble(((BubbleView) mTouchedView).getKey());
                        }
                    }
                } else if (mTouchedView.equals(mStack.getExpandedBubbleView())) {
                    mStack.collapseStack();
                } else if (isStack) {
                    if (mStack.isExpanded()) {
                        mStack.collapseStack();
                    } else {
                        stack.expandStack();
                        mStack.expandStack();
                    }
                } else {
                    stack.setExpandedBubble(((BubbleView) floatingView).getKey());
                    mStack.setExpandedBubble(((BubbleView) mTouchedView).getKey());
                }

                cleanUpDismissTarget();
                mVelocityTracker.recycle();
                mVelocityTracker = null;
                resetTouches();
                mTouchedView = null;
                mMovedEnough = false;
                break;
        }

        return true;
    }

@@ -216,16 +188,6 @@ class BubbleTouchHandler implements View.OnTouchListener {
        mDismissViewController.destroyDismissTarget();
    }

    /**
     * Resets anything we care about after a gesture is complete.
     */
    private void resetTouches() {
        mDownX = -1;
        mDownY = -1;
        mBubbleDownPosX = -1;
        mBubbleDownPosY = -1;
        mBubbleDraggingOut = null;
    }

    private void trackMovement(MotionEvent event) {
        if (mVelocityTracker == null) {
+1 −23
Original line number Diff line number Diff line
@@ -20,7 +20,6 @@ import android.annotation.Nullable;
import android.app.Notification;
import android.content.Context;
import android.graphics.Color;
import android.graphics.PointF;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
@@ -39,7 +38,7 @@ import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
/**
 * A floating object on the screen that can post message updates.
 */
public class BubbleView extends FrameLayout implements BubbleTouchHandler.FloatingView {
public class BubbleView extends FrameLayout {
    private static final String TAG = "BubbleView";

    // Same value as Launcher3 badge code
@@ -217,25 +216,4 @@ public class BubbleView extends FrameLayout implements BubbleTouchHandler.Floati
        // XXX: should we pull from the drawable, app icon, notif tint?
        return ColorUtils.blendARGB(defaultTint, Color.WHITE, WHITE_SCRIM_ALPHA);
    }

    @Override
    public void setPosition(float x, float y) {
        setPositionX(x);
        setPositionY(y);
    }

    @Override
    public void setPositionX(float x) {
        setTranslationX(x);
    }

    @Override
    public void setPositionY(float y) {
        setTranslationY(y);
    }

    @Override
    public PointF getPosition() {
        return new PointF(getTranslationX(), getTranslationY());
    }
}
+146 −14
Original line number Diff line number Diff line
@@ -44,6 +44,9 @@ public class ExpandedAnimationController
     */
    private static final int ANIMATE_TRANSLATION_FACTOR = 4;

    /** How much to scale down bubbles when they're animating in/out. */
    private static final float ANIMATE_SCALE_PERCENT = 0.5f;

    /**
     * The stack position from which the bubbles were expanded. Saved in {@link #expandFromStack}
     * and used to return to stack form in {@link #collapseBackToStack}.
@@ -59,6 +62,22 @@ public class ExpandedAnimationController
    /** Height of the status bar. */
    private float mStatusBarHeight;

    /**
     * Whether the individual bubble has been dragged out of the row of bubbles far enough to cause
     * the rest of the bubbles to animate to fill the gap.
     */
    private boolean mBubbleDraggedOutEnough = false;

    /** The bubble currently being dragged out of the row (to potentially be dismissed). */
    private View mBubbleDraggingOut;

    /**
     * Drag velocities for the dragging-out bubble when the drag finished. These are used by
     * {@link #onChildRemoved} to animate out the bubble while respecting touch velocity.
     */
    private float mBubbleDraggingOutVelX;
    private float mBubbleDraggingOutVelY;

    @Override
    protected void setLayout(PhysicsAnimationLayout layout) {
        super.setLayout(layout);
@@ -102,6 +121,87 @@ public class ExpandedAnimationController
        runAfterTranslationsEnd(after);
    }

    /** Prepares the given bubble to be dragged out. */
    public void prepareForBubbleDrag(View bubble) {
        mLayout.cancelAnimationsOnView(bubble);

        mBubbleDraggingOut = bubble;
        mBubbleDraggingOut.setTranslationZ(Short.MAX_VALUE);
    }

    /**
     * Drags an individual bubble to the given coordinates. Bubbles to the right will animate to
     * take its place once it's dragged out of the row of bubbles, and animate out of the way if the
     * bubble is dragged back into the row.
     */
    public void dragBubbleOut(View bubbleView, float x, float y) {
        bubbleView.setTranslationX(x);
        bubbleView.setTranslationY(y);

        final boolean draggedOutEnough =
                y > getExpandedY() + mBubbleSizePx || y < getExpandedY() - mBubbleSizePx;
        if (draggedOutEnough != mBubbleDraggedOutEnough) {
            animateStackByBubbleWidthsStartingFrom(
                    /* numBubbleWidths */ draggedOutEnough ? -1 : 0,
                    /* startIndex */ mLayout.indexOfChild(bubbleView) + 1);
            mBubbleDraggedOutEnough = draggedOutEnough;
        }
    }

    /**
     * Snaps a bubble back to its position within the bubble row, and animates the rest of the
     * bubbles to accommodate it if it was previously dragged out past the threshold.
     */
    public void snapBubbleBack(View bubbleView, float velX, float velY) {
        final int index = mLayout.indexOfChild(bubbleView);

        // Snap the bubble back, respecting its current velocity.
        mLayout.animateValueForChildAtIndex(
                DynamicAnimation.TRANSLATION_X, index, getXForChildAtIndex(index), velX);
        mLayout.animateValueForChildAtIndex(
                DynamicAnimation.TRANSLATION_Y, index, getExpandedY(), velY);
        mLayout.setEndListenerForProperties(
                mLayout.new OneTimeMultiplePropertyEndListener() {
                    @Override
                    void onAllAnimationsForPropertiesEnd() {
                        // Reset Z translation once the bubble is done snapping back.
                        bubbleView.setTranslationZ(0f);
                    }
                },
                DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);

        animateStackByBubbleWidthsStartingFrom(
                /* numBubbleWidths */ 0, /* startIndex */ index + 1);

        mBubbleDraggingOut = null;
        mBubbleDraggedOutEnough = false;
    }

    /**
     * Sets configuration variables so that when the given bubble is removed, the animations are
     * started with the given velocities.
     */
    public void prepareForDismissalWithVelocity(View bubbleView, float velX, float velY) {
        mBubbleDraggingOut = bubbleView;
        mBubbleDraggingOutVelX = velX;
        mBubbleDraggingOutVelY = velY;
        mBubbleDraggedOutEnough = false;
    }

    /**
     * Animates the bubbles, starting at the given index, to the left or right by the given number
     * of bubble widths. Passing zero for numBubbleWidths will animate the bubbles to their normal
     * positions.
     */
    private void animateStackByBubbleWidthsStartingFrom(int numBubbleWidths, int startIndex) {
        for (int i = startIndex; i < mLayout.getChildCount(); i++) {
            mLayout.animateValueForChildAtIndex(
                    DynamicAnimation.TRANSLATION_X,
                    i,
                    getXForChildAtIndex(i + numBubbleWidths));
        }
    }

    /** The Y value of the row of expanded bubbles. */
    private float getExpandedY() {
        final WindowInsets insets = mLayout.getRootWindowInsets();
@@ -165,12 +265,7 @@ public class ExpandedAnimationController
        child.setTranslationX(getXForChildAtIndex(index));
        child.setTranslationY(getExpandedY() - mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR);
        mLayout.animateValueForChild(DynamicAnimation.TRANSLATION_Y, child, getExpandedY());

        // Animate the remaining bubbles to the correct X position.
        for (int i = index + 1; i < mLayout.getChildCount(); i++) {
            mLayout.animateValueForChildAtIndex(
                    DynamicAnimation.TRANSLATION_X, i, getXForChildAtIndex(i));
        }
        animateBubblesAfterIndexToCorrectX(index);
    }

    @Override
@@ -179,16 +274,36 @@ public class ExpandedAnimationController
        // TODO: Reverse this when bubbles are at the bottom.
        mLayout.animateValueForChild(
                DynamicAnimation.ALPHA, child, 0f, finishRemoval);

        // If we're removing the dragged-out bubble, that means it got dismissed.
        if (child.equals(mBubbleDraggingOut)) {
            // Throw it to the bottom of the screen, towards the center horizontally.
            mLayout.animateValueForChild(
                    DynamicAnimation.TRANSLATION_X,
                    child,
                    mLayout.getWidth() / 2f - mBubbleSizePx / 2f,
                    mBubbleDraggingOutVelX);
            mLayout.animateValueForChild(
                    DynamicAnimation.TRANSLATION_Y,
                    child,
                getExpandedY() - mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR);
                    mLayout.getHeight() + mBubbleSizePx,
                    mBubbleDraggingOutVelY);

        // Animate the remaining bubbles to the correct X position.
        for (int i = index; i < mLayout.getChildCount(); i++) {
            mLayout.animateValueForChildAtIndex(
                    DynamicAnimation.TRANSLATION_X, i, getXForChildAtIndex(i));
            // Scale it down a bit so it looks like it's disappearing.
            mLayout.animateValueForChild(DynamicAnimation.SCALE_X, child, ANIMATE_SCALE_PERCENT);
            mLayout.animateValueForChild(DynamicAnimation.SCALE_Y, child, ANIMATE_SCALE_PERCENT);

            mBubbleDraggingOut = null;
        } else {
            // If we're removing some random bubble just throw it off the top.
            mLayout.animateValueForChild(
                    DynamicAnimation.TRANSLATION_Y,
                    child,
                    getExpandedY() - mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR);
        }

        // Animate all the other bubbles to their new positions sans this bubble.
        animateBubblesAfterIndexToCorrectX(index);
    }

    @Override
@@ -207,6 +322,23 @@ public class ExpandedAnimationController
                () -> super.setChildVisibility(child, index, visibility));
    }

    /**
     * Animates the bubbles after the given index to the X position they should be in according to
     * {@link #getXForChildAtIndex}.
     */
    private void animateBubblesAfterIndexToCorrectX(int start) {
        for (int i = start; i < mLayout.getChildCount(); i++) {
            final View bubble = mLayout.getChildAt(i);

            // Don't animate the dragging out bubble, or it'll jump around while being dragged. It
            // will be snapped to the correct X value after the drag (if it's not dismissed).
            if (!bubble.equals(mBubbleDraggingOut)) {
                mLayout.animateValueForChild(
                        DynamicAnimation.TRANSLATION_X, bubble, getXForChildAtIndex(i));
            }
        }
    }

    /** Returns the appropriate X translation value for a bubble at the given index. */
    private float getXForChildAtIndex(int index) {
        return mBubblePaddingPx + (mBubbleSizePx + mBubblePaddingPx) * index;
+72 −2

File changed.

Preview size limit exceeded, changes collapsed.

Loading