From 81a77ffc4d36c6054a75acfe7b048e7c0d7a8744 Mon Sep 17 00:00:00 2001 From: yueg Date: Tue, 5 Dec 2017 10:29:03 -0800 Subject: 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 --- .../incallui/NewReturnToCallController.java | 43 -- java/com/android/newbubble/NewBubble.java | 468 +++++++++++++-------- java/com/android/newbubble/NewMoveHandler.java | 40 +- .../newbubble/res/layout/new_bubble_base.xml | 129 +++--- java/com/android/newbubble/res/values/values.xml | 7 +- 5 files changed, 415 insertions(+), 272 deletions(-) (limited to 'java/com') diff --git a/java/com/android/incallui/NewReturnToCallController.java b/java/com/android/incallui/NewReturnToCallController.java index 399b18568..7a1abee51 100644 --- a/java/com/android/incallui/NewReturnToCallController.java +++ b/java/com/android/incallui/NewReturnToCallController.java @@ -29,8 +29,6 @@ import com.android.contacts.common.util.ContactDisplayUtils; import com.android.dialer.common.LogUtil; import com.android.dialer.configprovider.ConfigProviderBindings; import com.android.dialer.lettertile.LetterTileDrawable; -import com.android.dialer.logging.DialerImpression; -import com.android.dialer.logging.Logger; import com.android.dialer.telecom.TelecomUtil; import com.android.incallui.ContactInfoCache.ContactCacheEntry; import com.android.incallui.ContactInfoCache.ContactInfoCacheCallback; @@ -43,8 +41,6 @@ import com.android.incallui.call.DialerCall; import com.android.incallui.speakerbuttonlogic.SpeakerButtonInfo; import com.android.incallui.speakerbuttonlogic.SpeakerButtonInfo.IconSize; import com.android.newbubble.NewBubble; -import com.android.newbubble.NewBubble.BubbleExpansionStateListener; -import com.android.newbubble.NewBubble.ExpansionState; import com.android.newbubble.NewBubbleInfo; import com.android.newbubble.NewBubbleInfo.Action; import java.lang.ref.WeakReference; @@ -150,45 +146,6 @@ public class NewReturnToCallController implements InCallUiListener, Listener, Au return null; } NewBubble returnToCallBubble = NewBubble.createBubble(context, generateBubbleInfo()); - returnToCallBubble.setBubbleExpansionStateListener( - new BubbleExpansionStateListener() { - @Override - public void onBubbleExpansionStateChanged( - @ExpansionState int expansionState, boolean isUserAction) { - if (!isUserAction) { - return; - } - - DialerCall call = CallList.getInstance().getActiveOrBackgroundCall(); - switch (expansionState) { - case ExpansionState.START_EXPANDING: - if (call != null) { - Logger.get(context) - .logCallImpression( - DialerImpression.Type.BUBBLE_PRIMARY_BUTTON_EXPAND, - call.getUniqueCallId(), - call.getTimeAddedMs()); - } else { - Logger.get(context) - .logImpression(DialerImpression.Type.BUBBLE_PRIMARY_BUTTON_EXPAND); - } - break; - case ExpansionState.START_COLLAPSING: - if (call != null) { - Logger.get(context) - .logCallImpression( - DialerImpression.Type.BUBBLE_COLLAPSE_BY_USER, - call.getUniqueCallId(), - call.getTimeAddedMs()); - } else { - Logger.get(context).logImpression(DialerImpression.Type.BUBBLE_COLLAPSE_BY_USER); - } - break; - default: - break; - } - } - }); returnToCallBubble.show(); return returnToCallBubble; } 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); - } } diff --git a/java/com/android/newbubble/NewMoveHandler.java b/java/com/android/newbubble/NewMoveHandler.java index 189ad8472..9cb1f1eca 100644 --- a/java/com/android/newbubble/NewMoveHandler.java +++ b/java/com/android/newbubble/NewMoveHandler.java @@ -48,6 +48,8 @@ class NewMoveHandler implements OnTouchListener { private final int maxX; private final int maxY; private final int bubbleSize; + private final int bubbleShadowPaddingHorizontal; + private final int bubbleExpandedViewWidth; private final float touchSlopSquared; private boolean clickable = true; @@ -70,8 +72,14 @@ class NewMoveHandler implements OnTouchListener { @Override public float getValue(LayoutParams windowParams) { int realX = windowParams.x; - realX = realX + bubbleSize / 2; + // Get bubble center position from real position + if (bubble.getDrawerVisibility() == View.INVISIBLE) { + realX += bubbleExpandedViewWidth / 2 + bubbleShadowPaddingHorizontal * 2; + } else { + realX += bubbleSize / 2 + bubbleShadowPaddingHorizontal; + } if (relativeToRight(windowParams)) { + // If gravity is right, get distant from bubble center position to screen right edge int displayWidth = context.getResources().getDisplayMetrics().widthPixels; realX = displayWidth - realX; } @@ -88,12 +96,19 @@ class NewMoveHandler implements OnTouchListener { } else { onRight = (gravityOverride & Gravity.RIGHT) == Gravity.RIGHT; } - int centeringOffset = bubbleSize / 2; + // Get real position from bubble center position + int centeringOffset; + if (bubble.getDrawerVisibility() == View.INVISIBLE) { + centeringOffset = bubbleExpandedViewWidth / 2 + bubbleShadowPaddingHorizontal * 2; + } else { + centeringOffset = bubbleSize / 2 + bubbleShadowPaddingHorizontal; + } windowParams.x = (int) (onRight ? (displayWidth - value - centeringOffset) : value - centeringOffset); windowParams.gravity = Gravity.TOP | (onRight ? Gravity.RIGHT : Gravity.LEFT); if (bubble.isVisible()) { windowManager.updateViewLayout(bubble.getRootView(), windowParams); + bubble.onLeftRightSwitch(onRight); } } }; @@ -120,8 +135,13 @@ class NewMoveHandler implements OnTouchListener { windowManager = context.getSystemService(WindowManager.class); bubbleSize = context.getResources().getDimensionPixelSize(R.dimen.bubble_size); + bubbleShadowPaddingHorizontal = + context.getResources().getDimensionPixelSize(R.dimen.bubble_shadow_padding_size_horizontal); + bubbleExpandedViewWidth = + context.getResources().getDimensionPixelSize(R.dimen.bubble_expanded_width); + // The following value is based on bubble center minX = - context.getResources().getDimensionPixelOffset(R.dimen.bubble_safe_margin_horizontal) + context.getResources().getDimensionPixelOffset(R.dimen.bubble_off_screen_size_horizontal) + bubbleSize / 2; minY = context.getResources().getDimensionPixelOffset(R.dimen.bubble_safe_margin_vertical) @@ -156,6 +176,12 @@ class NewMoveHandler implements OnTouchListener { moveYAnimation.animateToFinalPosition(yProperty.getValue(bubble.getWindowParams())); } + public int getMoveUpDistance(int deltaAllowed) { + int currentY = (int) yProperty.getValue(bubble.getWindowParams()); + int currentDelta = maxY - currentY; + return currentDelta >= deltaAllowed ? 0 : deltaAllowed - currentDelta; + } + @Override public boolean onTouch(View v, MotionEvent event) { float eventX = event.getRawX(); @@ -222,6 +248,14 @@ class NewMoveHandler implements OnTouchListener { moveXAnimation = new SpringAnimation(bubble.getWindowParams(), xProperty); moveXAnimation.setSpring(new SpringForce()); moveXAnimation.getSpring().setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY); + // Moving when expanded makes expanded view INVISIBLE, and the whole view is not at the + // boundary. It's time to create a viewHolder. + moveXAnimation.addEndListener( + (animation, canceled, value, velocity) -> { + if (!isMoving && bubble.getDrawerVisibility() == View.INVISIBLE) { + bubble.replaceViewHolder(); + } + }); } if (moveYAnimation == null) { diff --git a/java/com/android/newbubble/res/layout/new_bubble_base.xml b/java/com/android/newbubble/res/layout/new_bubble_base.xml index 8cac982f4..8d4771631 100644 --- a/java/com/android/newbubble/res/layout/new_bubble_base.xml +++ b/java/com/android/newbubble/res/layout/new_bubble_base.xml @@ -19,7 +19,8 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:clipChildren="false" + android:clipChildren="true" + android:clipToPadding="false" tools:theme="@style/Theme.AppCompat"> - - + + + android:clipChildren="true" + android:clipToPadding="false" + android:layout_below="@id/bubble_primary_container"> - - - - - + - + android:elevation="@dimen/bubble_expanded_elevation" + android:rotation="45"> + + + + + + + diff --git a/java/com/android/newbubble/res/values/values.xml b/java/com/android/newbubble/res/values/values.xml index 6dda61d6c..71f813ac6 100644 --- a/java/com/android/newbubble/res/values/values.xml +++ b/java/com/android/newbubble/res/values/values.xml @@ -24,8 +24,11 @@ 16dp 12dp 16dp - -16dp - 64dp + + -4dp + + 48dp + 16dp -16dp 12dp -- cgit v1.2.3