diff options
author | yueg <yueg@google.com> | 2017-12-05 10:29:03 -0800 |
---|---|---|
committer | Copybara-Service <copybara-piper@google.com> | 2017-12-07 11:02:15 -0800 |
commit | 81a77ffc4d36c6054a75acfe7b048e7c0d7a8744 (patch) | |
tree | 650eabb797c3a2176bf4260dfadddfd9e4d9908e /java/com/android/newbubble/NewBubble.java | |
parent | 5b77c7dfd420cb77cdb195fd441a8d57676a837f (diff) |
Bubble v2 animation changes.
Including:
- expanded view expands/collapses from top of itself
- small icon on avatar shows on left side when bubble is on right side
- when expand on bottom, bubble move up a bit so that expanded view doesn't go off screen. It also go back to previous position when collapse.
- remove animation for collapse when move expanded bubble
This change should not enable bubble v2 for anyone.
Bug: 67605985
Test: manual
PiperOrigin-RevId: 177974562
Change-Id: Id83f3f744b717d51fbe58e58769ac2cd2810d2b5
Diffstat (limited to 'java/com/android/newbubble/NewBubble.java')
-rw-r--r-- | java/com/android/newbubble/NewBubble.java | 468 |
1 files changed, 303 insertions, 165 deletions
diff --git a/java/com/android/newbubble/NewBubble.java b/java/com/android/newbubble/NewBubble.java index e690f4be4..ef3a971dd 100644 --- a/java/com/android/newbubble/NewBubble.java +++ b/java/com/android/newbubble/NewBubble.java @@ -17,12 +17,15 @@ package com.android.newbubble; import android.animation.Animator; +import android.animation.Animator.AnimatorListener; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; +import android.animation.ValueAnimator; import android.annotation.SuppressLint; import android.app.PendingIntent.CanceledException; import android.content.Context; import android.content.Intent; +import android.graphics.Path; import android.graphics.PixelFormat; import android.graphics.drawable.Animatable; import android.graphics.drawable.Drawable; @@ -51,10 +54,16 @@ import android.view.WindowManager; import android.view.WindowManager.LayoutParams; import android.view.animation.AnticipateInterpolator; import android.view.animation.OvershootInterpolator; +import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.TextView; import android.widget.ViewAnimator; +import com.android.dialer.common.LogUtil; +import com.android.dialer.logging.DialerImpression; +import com.android.dialer.logging.Logger; import com.android.dialer.util.DrawableConverter; +import com.android.incallui.call.CallList; +import com.android.incallui.call.DialerCall; import com.android.newbubble.NewBubbleInfo.Action; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -96,11 +105,14 @@ public class NewBubble { private CharSequence textAfterShow; private int collapseEndAction; - @VisibleForTesting ViewHolder viewHolder; + ViewHolder viewHolder; private ViewPropertyAnimator collapseAnimation; private Integer overrideGravity; private ViewPropertyAnimator exitAnimator; + private int leftBoundary; + private int savedYPosition = -1; + private final Runnable collapseRunnable = new Runnable() { @Override @@ -110,17 +122,11 @@ public class NewBubble { // Always reset here since text shouldn't keep showing. hideAndReset(); } else { - doResize( - () -> - viewHolder - .getPrimaryButton() - .setDisplayedChild(ViewHolder.CHILD_INDEX_AVATAR_AND_ICON)); + viewHolder.getPrimaryButton().setDisplayedChild(ViewHolder.CHILD_INDEX_AVATAR_AND_ICON); } } }; - private BubbleExpansionStateListener bubbleExpansionStateListener; - /** Type of action after bubble collapse */ @Retention(RetentionPolicy.SOURCE) @IntDef({CollapseEnd.NOTHING, CollapseEnd.HIDE}) @@ -206,15 +212,20 @@ public class NewBubble { windowManager = context.getSystemService(WindowManager.class); viewHolder = new ViewHolder(context); + + leftBoundary = + context.getResources().getDimensionPixelOffset(R.dimen.bubble_off_screen_size_horizontal) + - context + .getResources() + .getDimensionPixelSize(R.dimen.bubble_shadow_padding_size_horizontal); } /** Expands the main bubble menu. */ public void expand(boolean isUserAction) { - if (bubbleExpansionStateListener != null) { - bubbleExpansionStateListener.onBubbleExpansionStateChanged( - ExpansionState.START_EXPANDING, isUserAction); + if (isUserAction) { + logBasicOrCallImpression(DialerImpression.Type.BUBBLE_PRIMARY_BUTTON_EXPAND); } - doResize(() -> viewHolder.setDrawerVisibility(View.VISIBLE)); + viewHolder.setDrawerVisibility(View.INVISIBLE); View expandedView = viewHolder.getExpandedView(); expandedView .getViewTreeObserver() @@ -222,13 +233,62 @@ public class NewBubble { new OnPreDrawListener() { @Override public boolean onPreDraw() { - // Animate expanded view to move from above primary button to its final position + // Move the whole bubble up so that expanded view is still in screen + int moveUpDistance = viewHolder.getMoveUpDistance(); + if (moveUpDistance != 0) { + savedYPosition = windowParams.y; + } + + // Calculate the move-to-middle distance + int deltaX = + (int) viewHolder.getRoot().findViewById(R.id.bubble_primary_container).getX(); + float k = (float) moveUpDistance / deltaX; + if (isDrawingFromRight()) { + deltaX = -deltaX; + } + + // Do X-move and Y-move together + + final int startX = windowParams.x - deltaX; + final int startY = windowParams.y; + ValueAnimator animator = ValueAnimator.ofFloat(startX, windowParams.x); + animator.setInterpolator(new LinearOutSlowInInterpolator()); + animator.addUpdateListener( + (valueAnimator) -> { + // Update windowParams and the root layout. + // We can't do ViewPropertyAnimation since it clips children. + float newX = (float) valueAnimator.getAnimatedValue(); + if (moveUpDistance != 0) { + windowParams.y = startY - (int) (Math.abs(newX - (float) startX) * k); + } + windowParams.x = (int) newX; + windowManager.updateViewLayout(viewHolder.getRoot(), windowParams); + }); + animator.addListener( + new AnimatorListener() { + @Override + public void onAnimationEnd(Animator animation) { + // Show expanded view + expandedView.setVisibility(View.VISIBLE); + expandedView.setTranslationY(-expandedView.getHeight()); + expandedView + .animate() + .setInterpolator(new LinearOutSlowInInterpolator()) + .translationY(0); + } + + @Override + public void onAnimationStart(Animator animation) {} + + @Override + public void onAnimationCancel(Animator animation) {} + + @Override + public void onAnimationRepeat(Animator animation) {} + }); + animator.start(); + expandedView.getViewTreeObserver().removeOnPreDrawListener(this); - expandedView.setTranslationY(-viewHolder.getRoot().getHeight()); - expandedView - .animate() - .setInterpolator(new LinearOutSlowInInterpolator()) - .translationY(0); return false; } }); @@ -236,6 +296,115 @@ public class NewBubble { expanded = true; } + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + public void startCollapse( + @CollapseEnd int endAction, boolean isUserAction, boolean shouldRecoverYPosition) { + View expandedView = viewHolder.getExpandedView(); + if (expandedView.getVisibility() != View.VISIBLE || collapseAnimation != null) { + // Drawer is already collapsed or animation is running. + return; + } + + overrideGravity = isDrawingFromRight() ? Gravity.RIGHT : Gravity.LEFT; + setFocused(false); + + if (collapseEndAction == CollapseEnd.NOTHING) { + collapseEndAction = endAction; + } + if (isUserAction && collapseEndAction == CollapseEnd.NOTHING) { + logBasicOrCallImpression(DialerImpression.Type.BUBBLE_COLLAPSE_BY_USER); + } + // Animate expanded view to move from its position to above primary button and hide + collapseAnimation = + expandedView + .animate() + .translationY(-expandedView.getHeight()) + .setInterpolator(new FastOutLinearInInterpolator()) + .withEndAction( + () -> { + collapseAnimation = null; + expanded = false; + + if (textShowing) { + // Will do resize once the text is done. + return; + } + + // Set drawer visibility to INVISIBLE instead of GONE to keep primary button fixed + viewHolder.setDrawerVisibility(View.INVISIBLE); + + // Do X-move and Y-move together + int deltaX = + (int) viewHolder.getRoot().findViewById(R.id.bubble_primary_container).getX(); + int startX = windowParams.x; + int startY = windowParams.y; + float k = + (savedYPosition != -1 && shouldRecoverYPosition) + ? (savedYPosition - startY) / (float) deltaX + : 0; + Path path = new Path(); + path.moveTo(windowParams.x, windowParams.y); + path.lineTo( + windowParams.x - deltaX, + (savedYPosition != -1 && shouldRecoverYPosition) + ? savedYPosition + : windowParams.y); + // The position is not useful after collapse + savedYPosition = -1; + + ValueAnimator animator = ValueAnimator.ofFloat(startX, startX - deltaX); + animator.setInterpolator(new LinearOutSlowInInterpolator()); + animator.addUpdateListener( + (valueAnimator) -> { + // Update windowParams and the root layout. + // We can't do ViewPropertyAnimation since it clips children. + float newX = (float) valueAnimator.getAnimatedValue(); + if (k != 0) { + windowParams.y = startY + (int) (Math.abs(newX - (float) startX) * k); + } + windowParams.x = (int) newX; + windowManager.updateViewLayout(viewHolder.getRoot(), windowParams); + }); + animator.addListener( + new AnimatorListener() { + @Override + public void onAnimationEnd(Animator animation) { + // If collapse on the right side, the primary button move left a bit after + // drawer + // visibility becoming GONE. To avoid it, we create a new ViewHolder. + replaceViewHolder(); + } + + @Override + public void onAnimationStart(Animator animation) {} + + @Override + public void onAnimationCancel(Animator animation) {} + + @Override + public void onAnimationRepeat(Animator animation) {} + }); + animator.start(); + + // If this collapse was to come before a hide, do it now. + if (collapseEndAction == CollapseEnd.HIDE) { + hide(); + } + collapseEndAction = CollapseEnd.NOTHING; + + // Resume normal gravity after any resizing is done. + handler.postDelayed( + () -> { + overrideGravity = null; + if (!viewHolder.isMoving()) { + viewHolder.undoGravityOverride(); + } + }, + // Need to wait twice as long for resize and layout + WINDOW_REDRAW_DELAY_MILLIS * 2); + }); + } + /** * Make the bubble visible. Will show a short entrance animation as it enters. If the bubble is * already showing this method does nothing. @@ -269,8 +438,7 @@ public class NewBubble { | LayoutParams.FLAG_LAYOUT_NO_LIMITS, PixelFormat.TRANSLUCENT); windowParams.gravity = Gravity.TOP | Gravity.LEFT; - windowParams.x = - context.getResources().getDimensionPixelOffset(R.dimen.bubble_safe_margin_horizontal); + windowParams.x = leftBoundary; windowParams.y = currentInfo.getStartingYPosition(); windowParams.height = LayoutParams.WRAP_CONTENT; windowParams.width = LayoutParams.WRAP_CONTENT; @@ -392,7 +560,8 @@ public class NewBubble { public void showText(@NonNull CharSequence text) { textShowing = true; if (expanded) { - startCollapse(CollapseEnd.NOTHING, false); + startCollapse( + CollapseEnd.NOTHING, false /* isUserAction */, false /* shouldRecoverYPosition */); doShowText(text); } else { // Need to transition from old bounds to new bounds manually @@ -409,68 +578,65 @@ public class NewBubble { return; } - doResize( - () -> { - doShowText(text); - // Hide the text so we can animate it in - viewHolder.getPrimaryText().setAlpha(0); - - ViewAnimator primaryButton = viewHolder.getPrimaryButton(); - // Cancel the automatic transition scheduled in doShowText - TransitionManager.endTransitions((ViewGroup) primaryButton.getParent()); - primaryButton - .getViewTreeObserver() - .addOnPreDrawListener( - new OnPreDrawListener() { - @Override - public boolean onPreDraw() { - primaryButton.getViewTreeObserver().removeOnPreDrawListener(this); - - // Prepare and capture end values, always use the size of primaryText since - // its invisibility makes primaryButton smaller than expected - TransitionValues endValues = new TransitionValues(); - endValues.values.put( - NewChangeOnScreenBounds.PROPNAME_WIDTH, - viewHolder.getPrimaryText().getWidth()); - endValues.values.put( - NewChangeOnScreenBounds.PROPNAME_HEIGHT, - viewHolder.getPrimaryText().getHeight()); - endValues.view = primaryButton; - transition.addTarget(endValues.view); - transition.captureEndValues(endValues); - - // animate the primary button bounds change - Animator bounds = - transition.createAnimator(primaryButton, startValues, endValues); - - // Animate the text in - Animator alpha = - ObjectAnimator.ofFloat(viewHolder.getPrimaryText(), View.ALPHA, 1f); - - AnimatorSet set = new AnimatorSet(); - set.play(bounds).before(alpha); - set.start(); - return false; - } - }); - }); + doShowText(text); + // Hide the text so we can animate it in + viewHolder.getPrimaryText().setAlpha(0); + + ViewAnimator primaryButton = viewHolder.getPrimaryButton(); + // Cancel the automatic transition scheduled in doShowText + TransitionManager.endTransitions((ViewGroup) primaryButton.getParent()); + primaryButton + .getViewTreeObserver() + .addOnPreDrawListener( + new OnPreDrawListener() { + @Override + public boolean onPreDraw() { + primaryButton.getViewTreeObserver().removeOnPreDrawListener(this); + + // Prepare and capture end values, always use the size of primaryText since + // its invisibility makes primaryButton smaller than expected + TransitionValues endValues = new TransitionValues(); + endValues.values.put( + NewChangeOnScreenBounds.PROPNAME_WIDTH, + viewHolder.getPrimaryText().getWidth()); + endValues.values.put( + NewChangeOnScreenBounds.PROPNAME_HEIGHT, + viewHolder.getPrimaryText().getHeight()); + endValues.view = primaryButton; + transition.addTarget(endValues.view); + transition.captureEndValues(endValues); + + // animate the primary button bounds change + Animator bounds = + transition.createAnimator(primaryButton, startValues, endValues); + + // Animate the text in + Animator alpha = + ObjectAnimator.ofFloat(viewHolder.getPrimaryText(), View.ALPHA, 1f); + + AnimatorSet set = new AnimatorSet(); + set.play(bounds).before(alpha); + set.start(); + return false; + } + }); } handler.removeCallbacks(collapseRunnable); handler.postDelayed(collapseRunnable, SHOW_TEXT_DURATION_MILLIS); } - public void setBubbleExpansionStateListener( - BubbleExpansionStateListener bubbleExpansionStateListener) { - this.bubbleExpansionStateListener = bubbleExpansionStateListener; - } - @Nullable Integer getGravityOverride() { return overrideGravity; } void onMoveStart() { - startCollapse(CollapseEnd.NOTHING, true); + if (viewHolder.getExpandedView().getVisibility() == View.VISIBLE) { + viewHolder.setDrawerVisibility(View.INVISIBLE); + } + expanded = false; + savedYPosition = -1; + viewHolder .getPrimaryButton() .animate() @@ -482,11 +648,6 @@ public class NewBubble { void onMoveFinish() { viewHolder.getPrimaryButton().animate().translationZ(0); - // If it's GONE, no resize is necessary. If it's VISIBLE, it will get cleaned up when the - // collapse animation finishes - if (viewHolder.getExpandedView().getVisibility() == View.INVISIBLE) { - doResize(null); - } } void primaryButtonClick() { @@ -494,12 +655,22 @@ public class NewBubble { return; } if (expanded) { - startCollapse(CollapseEnd.NOTHING, true); + startCollapse( + CollapseEnd.NOTHING, true /* isUserAction */, true /* shouldRecoverYPosition */); } else { expand(true); } } + void onLeftRightSwitch(boolean onRight) { + // Set layout direction so the small icon is not partially hidden. + View primaryIcon = viewHolder.getPrimaryIcon(); + int newGravity = (onRight ? Gravity.LEFT : Gravity.RIGHT) | Gravity.BOTTOM; + FrameLayout.LayoutParams layoutParams = + new FrameLayout.LayoutParams(primaryIcon.getWidth(), primaryIcon.getHeight(), newGravity); + primaryIcon.setLayoutParams(layoutParams); + } + LayoutParams getWindowParams() { return windowParams; } @@ -532,7 +703,7 @@ public class NewBubble { } if (expanded) { - startCollapse(CollapseEnd.HIDE, false); + startCollapse(CollapseEnd.HIDE, false /* isUserAction */, false /* shouldRecoverYPosition */); return; } @@ -618,34 +789,39 @@ public class NewBubble { } } - private void doResize(@Nullable Runnable operation) { - // If we're resizing on the right side of the screen, there is an implicit move operation - // necessary. The WindowManager does not sync the move and resize operations, so serious jank - // would occur. To fix this, instead of resizing the window, we create a new one and destroy - // the old one. There is a short delay before destroying the old view to ensure the new one has - // had time to draw. + /** + * Create a new ViewHolder object to replace the old one.It only happens when not moving and + * collapsed. + */ + void replaceViewHolder() { + LogUtil.enterBlock("NewBubble.replaceViewHolder"); ViewHolder oldViewHolder = viewHolder; - if (isDrawingFromRight()) { - viewHolder = new ViewHolder(oldViewHolder.getRoot().getContext()); - update(); - viewHolder - .getPrimaryButton() - .setDisplayedChild(oldViewHolder.getPrimaryButton().getDisplayedChild()); - viewHolder.getPrimaryText().setText(oldViewHolder.getPrimaryText().getText()); - } - if (operation != null) { - operation.run(); - } + // Create a new ViewHolder and copy needed info. + viewHolder = new ViewHolder(oldViewHolder.getRoot().getContext()); + viewHolder + .getPrimaryButton() + .setDisplayedChild(oldViewHolder.getPrimaryButton().getDisplayedChild()); + viewHolder.getPrimaryText().setText(oldViewHolder.getPrimaryText().getText()); - if (isDrawingFromRight()) { - swapViewHolders(oldViewHolder); - } - } + int size = context.getResources().getDimensionPixelSize(R.dimen.bubble_small_icon_size); + viewHolder + .getPrimaryIcon() + .setLayoutParams( + new FrameLayout.LayoutParams( + size, + size, + Gravity.BOTTOM | (isDrawingFromRight() ? Gravity.LEFT : Gravity.RIGHT))); - private void swapViewHolders(ViewHolder oldViewHolder) { + update(); + + // Add new view at its horizontal boundary ViewGroup root = viewHolder.getRoot(); + windowParams.x = leftBoundary; + windowParams.gravity = Gravity.TOP | (isDrawingFromRight() ? Gravity.RIGHT : Gravity.LEFT); windowManager.addView(root, windowParams); + + // Remove the old view after delay root.getViewTreeObserver() .addOnPreDrawListener( new OnPreDrawListener() { @@ -661,63 +837,8 @@ public class NewBubble { }); } - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - public void startCollapse(@CollapseEnd int endAction, boolean isUserAction) { - View expandedView = viewHolder.getExpandedView(); - if (expandedView.getVisibility() != View.VISIBLE || collapseAnimation != null) { - // Drawer is already collapsed or animation is running. - return; - } - - overrideGravity = isDrawingFromRight() ? Gravity.RIGHT : Gravity.LEFT; - setFocused(false); - - if (collapseEndAction == CollapseEnd.NOTHING) { - collapseEndAction = endAction; - } - if (bubbleExpansionStateListener != null && collapseEndAction == CollapseEnd.NOTHING) { - bubbleExpansionStateListener.onBubbleExpansionStateChanged( - ExpansionState.START_COLLAPSING, isUserAction); - } - // Animate expanded view to move from its position to above primary button and hide - collapseAnimation = - expandedView - .animate() - .translationY(-viewHolder.getRoot().getHeight()) - .setInterpolator(new FastOutLinearInInterpolator()) - .withEndAction( - () -> { - collapseAnimation = null; - expanded = false; - - if (textShowing) { - // Will do resize once the text is done. - return; - } - - // Hide the drawer and resize if possible. - viewHolder.setDrawerVisibility(View.INVISIBLE); - if (!viewHolder.isMoving() || !isDrawingFromRight()) { - doResize(() -> viewHolder.setDrawerVisibility(View.GONE)); - } - - // If this collapse was to come before a hide, do it now. - if (collapseEndAction == CollapseEnd.HIDE) { - hide(); - } - collapseEndAction = CollapseEnd.NOTHING; - - // Resume normal gravity after any resizing is done. - handler.postDelayed( - () -> { - overrideGravity = null; - if (!viewHolder.isMoving()) { - viewHolder.undoGravityOverride(); - } - }, - // Need to wait twice as long for resize and layout - WINDOW_REDRAW_DELAY_MILLIS * 2); - }); + int getDrawerVisibility() { + return viewHolder.getExpandedView().getVisibility(); } private boolean isDrawingFromRight() { @@ -741,6 +862,16 @@ public class NewBubble { updatePrimaryIconAnimation(); } + private void logBasicOrCallImpression(DialerImpression.Type impressionType) { + DialerCall call = CallList.getInstance().getActiveOrBackgroundCall(); + if (call != null) { + Logger.get(context) + .logCallImpression(impressionType, call.getUniqueCallId(), call.getTimeAddedMs()); + } else { + Logger.get(context).logImpression(impressionType); + } + } + @VisibleForTesting class ViewHolder { @@ -779,7 +910,8 @@ public class NewBubble { root.setOnBackPressedListener( () -> { if (visibility == Visibility.SHOWING && expanded) { - startCollapse(CollapseEnd.NOTHING, true); + startCollapse( + CollapseEnd.NOTHING, true /* isUserAction */, true /* shouldRecoverYPosition */); return true; } return false; @@ -794,7 +926,8 @@ public class NewBubble { root.setOnTouchListener( (v, event) -> { if (expanded && event.getActionMasked() == MotionEvent.ACTION_OUTSIDE) { - startCollapse(CollapseEnd.NOTHING, true); + startCollapse( + CollapseEnd.NOTHING, true /* isUserAction */, true /* shouldRecoverYPosition */); return true; } return false; @@ -812,6 +945,16 @@ public class NewBubble { moveHandler.setClickable(clickable); } + public int getMoveUpDistance() { + int deltaAllowed = + expandedView.getHeight() + - context + .getResources() + .getDimensionPixelOffset(R.dimen.bubble_button_padding_vertical) + * 2; + return moveHandler.getMoveUpDistance(deltaAllowed); + } + public ViewGroup getRoot() { return root; } @@ -864,9 +1007,4 @@ public class NewBubble { moveHandler.undoGravityOverride(); } } - - /** Listener for bubble expansion state change. */ - public interface BubbleExpansionStateListener { - void onBubbleExpansionStateChanged(@ExpansionState int expansionState, boolean isUserAction); - } } |