blob: 7e218b7ea81483967c05076d1bfa2476faee9ad5 [file] [log] [blame]
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.wm.shell.bubbles;
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
import static com.android.wm.shell.animation.Interpolators.ALPHA_IN;
import static com.android.wm.shell.animation.Interpolators.ALPHA_OUT;
import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_GESTURE;
import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_STACK_VIEW;
import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES;
import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME;
import static com.android.wm.shell.bubbles.BubblePositioner.NUM_VISIBLE_WHEN_RESTING;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.annotation.SuppressLint;
import android.app.ActivityManager;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Outline;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.ColorDrawable;
import android.os.Bundle;
import android.os.SystemProperties;
import android.provider.Settings;
import android.util.Log;
import android.view.Choreographer;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.SurfaceControl;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewOutlineProvider;
import android.view.ViewTreeObserver;
import android.view.WindowManagerPolicyConstants;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.dynamicanimation.animation.DynamicAnimation;
import androidx.dynamicanimation.animation.FloatPropertyCompat;
import androidx.dynamicanimation.animation.SpringAnimation;
import androidx.dynamicanimation.animation.SpringForce;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.policy.ScreenDecorationsUtils;
import com.android.internal.util.FrameworkStatsLog;
import com.android.wm.shell.R;
import com.android.wm.shell.animation.Interpolators;
import com.android.wm.shell.animation.PhysicsAnimator;
import com.android.wm.shell.bubbles.BubblesNavBarMotionEventHandler.MotionEventListener;
import com.android.wm.shell.bubbles.animation.AnimatableScaleMatrix;
import com.android.wm.shell.bubbles.animation.ExpandedAnimationController;
import com.android.wm.shell.bubbles.animation.ExpandedViewAnimationController;
import com.android.wm.shell.bubbles.animation.ExpandedViewAnimationControllerImpl;
import com.android.wm.shell.bubbles.animation.ExpandedViewAnimationControllerStub;
import com.android.wm.shell.bubbles.animation.PhysicsAnimationLayout;
import com.android.wm.shell.bubbles.animation.StackAnimationController;
import com.android.wm.shell.common.FloatingContentCoordinator;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.magnetictarget.MagnetizedObject;
import java.io.PrintWriter;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.stream.Collectors;
/**
* Renders bubbles in a stack and handles animating expanded and collapsed states.
*/
public class BubbleStackView extends FrameLayout
implements ViewTreeObserver.OnComputeInternalInsetsListener {
/**
* Set to {@code true} to enable home gesture handling in bubbles
*/
public static final boolean HOME_GESTURE_ENABLED =
SystemProperties.getBoolean("persist.wm.debug.bubbles_home_gesture", false);
private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleStackView" : TAG_BUBBLES;
/** How far the flyout needs to be dragged before it's dismissed regardless of velocity. */
static final float FLYOUT_DRAG_PERCENT_DISMISS = 0.25f;
/** Velocity required to dismiss the flyout via drag. */
private static final float FLYOUT_DISMISS_VELOCITY = 2000f;
/**
* Factor for attenuating translation when the flyout is overscrolled (8f = flyout moves 1 pixel
* for every 8 pixels overscrolled).
*/
private static final float FLYOUT_OVERSCROLL_ATTENUATION_FACTOR = 8f;
private static final int FADE_IN_DURATION = 320;
/** How long to wait, in milliseconds, before hiding the flyout. */
@VisibleForTesting
static final int FLYOUT_HIDE_AFTER = 5000;
private static final float EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT = 0.1f;
private static final int EXPANDED_VIEW_ALPHA_ANIMATION_DURATION = 150;
private static final float SCRIM_ALPHA = 0.6f;
/**
* How long to wait to animate the stack temporarily invisible after a drag/flyout hide
* animation ends, if we are in fact temporarily invisible.
*/
private static final int ANIMATE_TEMPORARILY_INVISIBLE_DELAY = 1000;
private static final PhysicsAnimator.SpringConfig FLYOUT_IME_ANIMATION_SPRING_CONFIG =
new PhysicsAnimator.SpringConfig(
StackAnimationController.IME_ANIMATION_STIFFNESS,
StackAnimationController.DEFAULT_BOUNCINESS);
private final PhysicsAnimator.SpringConfig mScaleInSpringConfig =
new PhysicsAnimator.SpringConfig(300f, 0.9f);
private final PhysicsAnimator.SpringConfig mScaleOutSpringConfig =
new PhysicsAnimator.SpringConfig(900f, 1f);
private final PhysicsAnimator.SpringConfig mTranslateSpringConfig =
new PhysicsAnimator.SpringConfig(
SpringForce.STIFFNESS_VERY_LOW, SpringForce.DAMPING_RATIO_NO_BOUNCY);
/**
* Handler to use for all delayed animations - this way, we can easily cancel them before
* starting a new animation.
*/
private final ShellExecutor mMainExecutor;
private Runnable mDelayedAnimation;
/**
* Interface to synchronize {@link View} state and the screen.
*
* {@hide}
*/
public interface SurfaceSynchronizer {
/**
* Wait until requested change on a {@link View} is reflected on the screen.
*
* @param callback callback to run after the change is reflected on the screen.
*/
void syncSurfaceAndRun(Runnable callback);
}
private static final SurfaceSynchronizer DEFAULT_SURFACE_SYNCHRONIZER =
new SurfaceSynchronizer() {
@Override
public void syncSurfaceAndRun(Runnable callback) {
Choreographer.FrameCallback frameCallback = new Choreographer.FrameCallback() {
// Just wait 2 frames. There is no guarantee, but this is usually enough
// time that the requested change is reflected on the screen.
// TODO: Once SurfaceFlinger provide APIs to sync the state of
// {@code View} and surfaces, rewrite this logic with them.
private int mFrameWait = 2;
@Override
public void doFrame(long frameTimeNanos) {
if (--mFrameWait > 0) {
Choreographer.getInstance().postFrameCallback(this);
} else {
callback.run();
}
}
};
Choreographer.getInstance().postFrameCallback(frameCallback);
}
};
private final BubbleController mBubbleController;
private final BubbleData mBubbleData;
private StackViewState mStackViewState = new StackViewState();
private final ValueAnimator mDismissBubbleAnimator;
private PhysicsAnimationLayout mBubbleContainer;
private StackAnimationController mStackAnimationController;
private ExpandedAnimationController mExpandedAnimationController;
private ExpandedViewAnimationController mExpandedViewAnimationController;
private View mScrim;
private View mManageMenuScrim;
private FrameLayout mExpandedViewContainer;
/** Matrix used to scale the expanded view container with a given pivot point. */
private final AnimatableScaleMatrix mExpandedViewContainerMatrix = new AnimatableScaleMatrix();
/**
* SurfaceView that we draw screenshots of animating-out bubbles into. This allows us to animate
* between bubble activities without needing both to be alive at the same time.
*/
private SurfaceView mAnimatingOutSurfaceView;
private boolean mAnimatingOutSurfaceReady;
/** Container for the animating-out SurfaceView. */
private FrameLayout mAnimatingOutSurfaceContainer;
/** Animator for animating the alpha value of the animating out SurfaceView. */
private final ValueAnimator mAnimatingOutSurfaceAlphaAnimator = ValueAnimator.ofFloat(0f, 1f);
/**
* Buffer containing a screenshot of the animating-out bubble. This is drawn into the
* SurfaceView during animations.
*/
private SurfaceControl.ScreenshotHardwareBuffer mAnimatingOutBubbleBuffer;
private BubbleFlyoutView mFlyout;
/** Runnable that fades out the flyout and then sets it to GONE. */
private Runnable mHideFlyout = () -> animateFlyoutCollapsed(true, 0 /* velX */);
/**
* Callback to run after the flyout hides. Also called if a new flyout is shown before the
* previous one animates out.
*/
private Runnable mAfterFlyoutHidden;
/**
* Set when the flyout is tapped, so that we can expand the bubble associated with the flyout
* once it collapses.
*/
@Nullable
private BubbleViewProvider mBubbleToExpandAfterFlyoutCollapse = null;
/** Layout change listener that moves the stack to the nearest valid position on rotation. */
private OnLayoutChangeListener mOrientationChangedListener;
@Nullable private RelativeStackPosition mRelativeStackPositionBeforeRotation;
private int mBubbleSize;
private int mBubbleElevation;
private int mBubbleTouchPadding;
private int mExpandedViewPadding;
private int mCornerRadius;
@Nullable private BubbleViewProvider mExpandedBubble;
private boolean mIsExpanded;
/** Whether the stack is currently on the left side of the screen, or animating there. */
private boolean mStackOnLeftOrWillBe = true;
/** Whether a touch gesture, such as a stack/bubble drag or flyout drag, is in progress. */
private boolean mIsGestureInProgress = false;
/** Whether or not the stack is temporarily invisible off the side of the screen. */
private boolean mTemporarilyInvisible = false;
/** Whether we're in the middle of dragging the stack around by touch. */
private boolean mIsDraggingStack = false;
/** Whether the expanded view has been hidden, because we are dragging out a bubble. */
private boolean mExpandedViewTemporarilyHidden = false;
/** Animator for animating the expanded view's alpha (including the TaskView inside it). */
private final ValueAnimator mExpandedViewAlphaAnimator = ValueAnimator.ofFloat(0f, 1f);
/**
* The pointer index of the ACTION_DOWN event we received prior to an ACTION_UP. We'll ignore
* touches from other pointer indices.
*/
private int mPointerIndexDown = -1;
@Nullable
private BubblesNavBarGestureTracker mBubblesNavBarGestureTracker;
/** Description of current animation controller state. */
public void dump(PrintWriter pw, String[] args) {
pw.println("Stack view state:");
String bubblesOnScreen = BubbleDebugConfig.formatBubblesString(
getBubblesOnScreen(), getExpandedBubble());
pw.print(" bubbles on screen: "); pw.println(bubblesOnScreen);
pw.print(" gestureInProgress: "); pw.println(mIsGestureInProgress);
pw.print(" showingDismiss: "); pw.println(mDismissView.isShowing());
pw.print(" isExpansionAnimating: "); pw.println(mIsExpansionAnimating);
pw.print(" expandedContainerVis: "); pw.println(mExpandedViewContainer.getVisibility());
pw.print(" expandedContainerAlpha: "); pw.println(mExpandedViewContainer.getAlpha());
pw.print(" expandedContainerMatrix: ");
pw.println(mExpandedViewContainer.getAnimationMatrix());
mStackAnimationController.dump(pw, args);
mExpandedAnimationController.dump(pw, args);
if (mExpandedBubble != null) {
pw.println("Expanded bubble state:");
pw.println(" expandedBubbleKey: " + mExpandedBubble.getKey());
final BubbleExpandedView expandedView = mExpandedBubble.getExpandedView();
if (expandedView != null) {
pw.println(" expandedViewVis: " + expandedView.getVisibility());
pw.println(" expandedViewAlpha: " + expandedView.getAlpha());
pw.println(" expandedViewTaskId: " + expandedView.getTaskId());
final View av = expandedView.getTaskView();
if (av != null) {
pw.println(" activityViewVis: " + av.getVisibility());
pw.println(" activityViewAlpha: " + av.getAlpha());
} else {
pw.println(" activityView is null");
}
} else {
pw.println("Expanded bubble view state: expanded bubble view is null");
}
} else {
pw.println("Expanded bubble state: expanded bubble is null");
}
}
private Bubbles.BubbleExpandListener mExpandListener;
/** Callback to run when we want to unbubble the given notification's conversation. */
private Consumer<String> mUnbubbleConversationCallback;
private boolean mViewUpdatedRequested = false;
private boolean mIsExpansionAnimating = false;
private boolean mIsBubbleSwitchAnimating = false;
/** The view to shrink and apply alpha to when magneted to the dismiss target. */
@Nullable private View mViewBeingDismissed;
private Rect mTempRect = new Rect();
private final List<Rect> mSystemGestureExclusionRects = Collections.singletonList(new Rect());
private ViewTreeObserver.OnPreDrawListener mViewUpdater =
new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
getViewTreeObserver().removeOnPreDrawListener(mViewUpdater);
updateExpandedView();
mViewUpdatedRequested = false;
return true;
}
};
private ViewTreeObserver.OnDrawListener mSystemGestureExcludeUpdater =
this::updateSystemGestureExcludeRects;
/** Float property that 'drags' the flyout. */
private final FloatPropertyCompat mFlyoutCollapseProperty =
new FloatPropertyCompat("FlyoutCollapseSpring") {
@Override
public float getValue(Object o) {
return mFlyoutDragDeltaX;
}
@Override
public void setValue(Object o, float v) {
setFlyoutStateForDragLength(v);
}
};
/** SpringAnimation that springs the flyout collapsed via onFlyoutDragged. */
private final SpringAnimation mFlyoutTransitionSpring =
new SpringAnimation(this, mFlyoutCollapseProperty);
/** Distance the flyout has been dragged in the X axis. */
private float mFlyoutDragDeltaX = 0f;
/**
* Runnable that animates in the flyout. This reference is needed to cancel delayed postings.
*/
private Runnable mAnimateInFlyout;
/**
* End listener for the flyout spring that either posts a runnable to hide the flyout, or hides
* it immediately.
*/
private final DynamicAnimation.OnAnimationEndListener mAfterFlyoutTransitionSpring =
(dynamicAnimation, b, v, v1) -> {
if (mFlyoutDragDeltaX == 0) {
mFlyout.postDelayed(mHideFlyout, FLYOUT_HIDE_AFTER);
} else {
mFlyout.hideFlyout();
}
};
@NonNull
private final SurfaceSynchronizer mSurfaceSynchronizer;
/**
* The currently magnetized object, which is being dragged and will be attracted to the magnetic
* dismiss target.
*
* This is either the stack itself, or an individual bubble.
*/
private MagnetizedObject<?> mMagnetizedObject;
/**
* The MagneticTarget instance for our circular dismiss view. This is added to the
* MagnetizedObject instances for the stack and any dragged-out bubbles.
*/
private MagnetizedObject.MagneticTarget mMagneticTarget;
/** Magnet listener that handles animating and dismissing individual dragged-out bubbles. */
private final MagnetizedObject.MagnetListener mIndividualBubbleMagnetListener =
new MagnetizedObject.MagnetListener() {
@Override
public void onStuckToTarget(@NonNull MagnetizedObject.MagneticTarget target) {
if (mExpandedAnimationController.getDraggedOutBubble() == null) {
return;
}
animateDismissBubble(
mExpandedAnimationController.getDraggedOutBubble(), true);
}
@Override
public void onUnstuckFromTarget(@NonNull MagnetizedObject.MagneticTarget target,
float velX, float velY, boolean wasFlungOut) {
if (mExpandedAnimationController.getDraggedOutBubble() == null) {
return;
}
animateDismissBubble(
mExpandedAnimationController.getDraggedOutBubble(), false);
if (wasFlungOut) {
mExpandedAnimationController.snapBubbleBack(
mExpandedAnimationController.getDraggedOutBubble(), velX, velY);
mDismissView.hide();
} else {
mExpandedAnimationController.onUnstuckFromTarget();
}
}
@Override
public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target) {
if (mExpandedAnimationController.getDraggedOutBubble() == null) {
return;
}
mExpandedAnimationController.dismissDraggedOutBubble(
mExpandedAnimationController.getDraggedOutBubble() /* bubble */,
mDismissView.getHeight() /* translationYBy */,
BubbleStackView.this::dismissMagnetizedObject /* after */);
mDismissView.hide();
}
};
/** Magnet listener that handles animating and dismissing the entire stack. */
private final MagnetizedObject.MagnetListener mStackMagnetListener =
new MagnetizedObject.MagnetListener() {
@Override
public void onStuckToTarget(
@NonNull MagnetizedObject.MagneticTarget target) {
animateDismissBubble(mBubbleContainer, true);
}
@Override
public void onUnstuckFromTarget(@NonNull MagnetizedObject.MagneticTarget target,
float velX, float velY, boolean wasFlungOut) {
animateDismissBubble(mBubbleContainer, false);
if (wasFlungOut) {
mStackAnimationController.flingStackThenSpringToEdge(
mStackAnimationController.getStackPosition().x, velX, velY);
mDismissView.hide();
} else {
mStackAnimationController.onUnstuckFromTarget();
}
}
@Override
public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target) {
mStackAnimationController.animateStackDismissal(
mDismissView.getHeight() /* translationYBy */,
() -> {
resetDismissAnimator();
dismissMagnetizedObject();
}
);
mDismissView.hide();
}
};
/**
* Click listener set on each bubble view. When collapsed, clicking a bubble expands the stack.
* When expanded, clicking a bubble either expands that bubble, or collapses the stack.
*/
private OnClickListener mBubbleClickListener = new OnClickListener() {
@Override
public void onClick(View view) {
mIsDraggingStack = false; // If the touch ended in a click, we're no longer dragging.
// Bubble clicks either trigger expansion/collapse or a bubble switch, both of which we
// shouldn't interrupt. These are quick transitions, so it's not worth trying to adjust
// the animations inflight.
if (mIsExpansionAnimating || mIsBubbleSwitchAnimating) {
return;
}
final Bubble clickedBubble = mBubbleData.getBubbleWithView(view);
// If the bubble has since left us, ignore the click.
if (clickedBubble == null) {
return;
}
final boolean clickedBubbleIsCurrentlyExpandedBubble =
clickedBubble.getKey().equals(mExpandedBubble.getKey());
if (isExpanded()) {
mExpandedAnimationController.onGestureFinished();
}
if (isExpanded() && !clickedBubbleIsCurrentlyExpandedBubble) {
if (clickedBubble != mBubbleData.getSelectedBubble()) {
// Select the clicked bubble.
mBubbleData.setSelectedBubble(clickedBubble);
} else {
// If the clicked bubble is the selected bubble (but not the expanded bubble),
// that means overflow was previously expanded. Set the selected bubble
// internally without going through BubbleData (which would ignore it since it's
// already selected).
setSelectedBubble(clickedBubble);
}
} else {
// Otherwise, we either tapped the stack (which means we're collapsed
// and should expand) or the currently selected bubble (we're expanded
// and should collapse).
if (!maybeShowStackEdu() && !mShowedUserEducationInTouchListenerActive) {
mBubbleData.setExpanded(!mBubbleData.isExpanded());
}
mShowedUserEducationInTouchListenerActive = false;
}
}
};
/**
* Touch listener set on each bubble view. This enables dragging and dismissing the stack (when
* collapsed), or individual bubbles (when expanded).
*/
private RelativeTouchListener mBubbleTouchListener = new RelativeTouchListener() {
@Override
public boolean onDown(@NonNull View v, @NonNull MotionEvent ev) {
// If we're expanding or collapsing, consume but ignore all touch events.
if (mIsExpansionAnimating) {
return true;
}
mShowedUserEducationInTouchListenerActive = false;
if (maybeShowStackEdu()) {
mShowedUserEducationInTouchListenerActive = true;
return true;
} else if (isStackEduShowing()) {
mStackEduView.hide(false /* fromExpansion */);
}
// If the manage menu is visible, just hide it.
if (mShowingManage) {
showManageMenu(false /* show */);
}
if (mBubbleData.isExpanded()) {
if (mManageEduView != null) {
mManageEduView.hide();
}
// If we're expanded, tell the animation controller to prepare to drag this bubble,
// dispatching to the individual bubble magnet listener.
mExpandedAnimationController.prepareForBubbleDrag(
v /* bubble */,
mMagneticTarget,
mIndividualBubbleMagnetListener);
hideCurrentInputMethod();
// Save the magnetized individual bubble so we can dispatch touch events to it.
mMagnetizedObject = mExpandedAnimationController.getMagnetizedBubbleDraggingOut();
} else {
// If we're collapsed, prepare to drag the stack. Cancel active animations, set the
// animation controller, and hide the flyout.
mStackAnimationController.cancelStackPositionAnimations();
mBubbleContainer.setActiveController(mStackAnimationController);
hideFlyoutImmediate();
if (mPositioner.showingInTaskbar()) {
// In taskbar, the stack isn't draggable so we shouldn't dispatch touch events.
mMagnetizedObject = null;
} else {
// Save the magnetized stack so we can dispatch touch events to it.
mMagnetizedObject = mStackAnimationController.getMagnetizedStack();
mMagnetizedObject.clearAllTargets();
mMagnetizedObject.addTarget(mMagneticTarget);
mMagnetizedObject.setMagnetListener(mStackMagnetListener);
}
mIsDraggingStack = true;
// Cancel animations to make the stack temporarily invisible, since we're now
// dragging it.
updateTemporarilyInvisibleAnimation(false /* hideImmediately */);
}
passEventToMagnetizedObject(ev);
// Bubbles are always interested in all touch events!
return true;
}
@Override
public void onMove(@NonNull View v, @NonNull MotionEvent ev, float viewInitialX,
float viewInitialY, float dx, float dy) {
// If we're expanding or collapsing, ignore all touch events.
if (mIsExpansionAnimating
// Also ignore events if we shouldn't be draggable.
|| (mPositioner.showingInTaskbar() && !mIsExpanded)
|| mShowedUserEducationInTouchListenerActive) {
return;
}
// Show the dismiss target, if we haven't already.
mDismissView.show();
if (mIsExpanded && mExpandedBubble != null && v.equals(mExpandedBubble.getIconView())) {
// Hide the expanded view if we're dragging out the expanded bubble, and we haven't
// already hidden it.
hideExpandedViewIfNeeded();
}
// First, see if the magnetized object consumes the event - if so, we shouldn't move the
// bubble since it's stuck to the target.
if (!passEventToMagnetizedObject(ev)) {
updateBubbleShadows(true /* showForAllBubbles */);
if (mBubbleData.isExpanded() || mPositioner.showingInTaskbar()) {
mExpandedAnimationController.dragBubbleOut(
v, viewInitialX + dx, viewInitialY + dy);
} else {
if (isStackEduShowing()) {
mStackEduView.hide(false /* fromExpansion */);
}
mStackAnimationController.moveStackFromTouch(
viewInitialX + dx, viewInitialY + dy);
}
}
}
@Override
public void onUp(@NonNull View v, @NonNull MotionEvent ev, float viewInitialX,
float viewInitialY, float dx, float dy, float velX, float velY) {
// If we're expanding or collapsing, ignore all touch events.
if (mIsExpansionAnimating
// Also ignore events if we shouldn't be draggable.
|| (mPositioner.showingInTaskbar() && !mIsExpanded)) {
return;
}
if (mShowedUserEducationInTouchListenerActive) {
mShowedUserEducationInTouchListenerActive = false;
return;
}
// First, see if the magnetized object consumes the event - if so, the bubble was
// released in the target or flung out of it, and we should ignore the event.
if (!passEventToMagnetizedObject(ev)) {
if (mBubbleData.isExpanded()) {
mExpandedAnimationController.snapBubbleBack(v, velX, velY);
// Re-show the expanded view if we hid it.
showExpandedViewIfNeeded();
} else {
// Fling the stack to the edge, and save whether or not it's going to end up on
// the left side of the screen.
final boolean oldOnLeft = mStackOnLeftOrWillBe;
mStackOnLeftOrWillBe =
mStackAnimationController.flingStackThenSpringToEdge(
viewInitialX + dx, velX, velY) <= 0;
final boolean updateForCollapsedStack = oldOnLeft != mStackOnLeftOrWillBe;
updateBadges(updateForCollapsedStack);
logBubbleEvent(null /* no bubble associated with bubble stack move */,
FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__STACK_MOVED);
}
mDismissView.hide();
}
mIsDraggingStack = false;
// Hide the stack after a delay, if needed.
updateTemporarilyInvisibleAnimation(false /* hideImmediately */);
}
};
/** Touch listener set on the whole view that forwards event to the swipe up listener. */
private final RelativeTouchListener mContainerSwipeListener = new RelativeTouchListener() {
@Override
public boolean onDown(@NonNull View v, @NonNull MotionEvent ev) {
// Pass move event on to swipe listener
mSwipeUpListener.onDown(ev.getX(), ev.getY());
return true;
}
@Override
public void onMove(@NonNull View v, @NonNull MotionEvent ev, float viewInitialX,
float viewInitialY, float dx, float dy) {
// Pass move event on to swipe listener
mSwipeUpListener.onMove(dx, dy);
}
@Override
public void onUp(@NonNull View v, @NonNull MotionEvent ev, float viewInitialX,
float viewInitialY, float dx, float dy, float velX, float velY) {
// Pass up even on to swipe listener
mSwipeUpListener.onUp(velX, velY);
}
};
/** MotionEventListener that listens from home gesture swipe event. */
private final MotionEventListener mSwipeUpListener = new MotionEventListener() {
@Override
public void onDown(float x, float y) {}
@Override
public void onMove(float dx, float dy) {
if ((mManageEduView != null && mManageEduView.getVisibility() == VISIBLE)
|| isStackEduShowing()) {
return;
}
if (mShowingManage) {
showManageMenu(false /* show */);
}
// Only allow up
float collapsed = Math.min(dy, 0);
mExpandedViewAnimationController.updateDrag((int) -collapsed);
}
@Override
public void onCancel() {
mExpandedViewAnimationController.animateBackToExpanded();
}
@Override
public void onUp(float velX, float velY) {
mExpandedViewAnimationController.setSwipeVelocity(velY);
if (mExpandedViewAnimationController.shouldCollapse()) {
// Update data first and start the animation when we are processing change
mBubbleData.setExpanded(false);
} else {
mExpandedViewAnimationController.animateBackToExpanded();
}
}
};
/** Click listener set on the flyout, which expands the stack when the flyout is tapped. */
private OnClickListener mFlyoutClickListener = new OnClickListener() {
@Override
public void onClick(View view) {
if (maybeShowStackEdu()) {
// If we're showing user education, don't open the bubble show the education first
mBubbleToExpandAfterFlyoutCollapse = null;
} else {
mBubbleToExpandAfterFlyoutCollapse = mBubbleData.getSelectedBubble();
}
mFlyout.removeCallbacks(mHideFlyout);
mHideFlyout.run();
}
};
/** Touch listener for the flyout. This enables the drag-to-dismiss gesture on the flyout. */
private RelativeTouchListener mFlyoutTouchListener = new RelativeTouchListener() {
@Override
public boolean onDown(@NonNull View v, @NonNull MotionEvent ev) {
mFlyout.removeCallbacks(mHideFlyout);
return true;
}
@Override
public void onMove(@NonNull View v, @NonNull MotionEvent ev, float viewInitialX,
float viewInitialY, float dx, float dy) {
setFlyoutStateForDragLength(dx);
}
@Override
public void onUp(@NonNull View v, @NonNull MotionEvent ev, float viewInitialX,
float viewInitialY, float dx, float dy, float velX, float velY) {
final boolean onLeft = mStackAnimationController.isStackOnLeftSide();
final boolean metRequiredVelocity =
onLeft ? velX < -FLYOUT_DISMISS_VELOCITY : velX > FLYOUT_DISMISS_VELOCITY;
final boolean metRequiredDeltaX =
onLeft
? dx < -mFlyout.getWidth() * FLYOUT_DRAG_PERCENT_DISMISS
: dx > mFlyout.getWidth() * FLYOUT_DRAG_PERCENT_DISMISS;
final boolean isCancelFling = onLeft ? velX > 0 : velX < 0;
final boolean shouldDismiss = metRequiredVelocity
|| (metRequiredDeltaX && !isCancelFling);
mFlyout.removeCallbacks(mHideFlyout);
animateFlyoutCollapsed(shouldDismiss, velX);
maybeShowStackEdu();
}
};
private BubbleOverflow mBubbleOverflow;
private StackEducationView mStackEduView;
private ManageEducationView mManageEduView;
private DismissView mDismissView;
private ViewGroup mManageMenu;
private ImageView mManageSettingsIcon;
private TextView mManageSettingsText;
private boolean mShowingManage = false;
private boolean mShowedUserEducationInTouchListenerActive = false;
private PhysicsAnimator.SpringConfig mManageSpringConfig = new PhysicsAnimator.SpringConfig(
SpringForce.STIFFNESS_MEDIUM, SpringForce.DAMPING_RATIO_LOW_BOUNCY);
private BubblePositioner mPositioner;
@SuppressLint("ClickableViewAccessibility")
public BubbleStackView(Context context, BubbleController bubbleController,
BubbleData data, @Nullable SurfaceSynchronizer synchronizer,
FloatingContentCoordinator floatingContentCoordinator,
ShellExecutor mainExecutor) {
super(context);
mMainExecutor = mainExecutor;
mBubbleController = bubbleController;
mBubbleData = data;
Resources res = getResources();
mBubbleSize = res.getDimensionPixelSize(R.dimen.bubble_size);
mBubbleElevation = res.getDimensionPixelSize(R.dimen.bubble_elevation);
mBubbleTouchPadding = res.getDimensionPixelSize(R.dimen.bubble_touch_padding);
mExpandedViewPadding = res.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding);
int elevation = res.getDimensionPixelSize(R.dimen.bubble_elevation);
mPositioner = mBubbleController.getPositioner();
final TypedArray ta = mContext.obtainStyledAttributes(
new int[]{android.R.attr.dialogCornerRadius});
mCornerRadius = ta.getDimensionPixelSize(0, 0);
ta.recycle();
final Runnable onBubbleAnimatedOut = () -> {
if (getBubbleCount() == 0) {
mBubbleController.onAllBubblesAnimatedOut();
}
};
mStackAnimationController = new StackAnimationController(
floatingContentCoordinator, this::getBubbleCount, onBubbleAnimatedOut,
this::animateShadows /* onStackAnimationFinished */, mPositioner);
mExpandedAnimationController = new ExpandedAnimationController(mPositioner,
onBubbleAnimatedOut, this);
if (HOME_GESTURE_ENABLED) {
mExpandedViewAnimationController =
new ExpandedViewAnimationControllerImpl(context, mPositioner);
} else {
mExpandedViewAnimationController = new ExpandedViewAnimationControllerStub();
}
mSurfaceSynchronizer = synchronizer != null ? synchronizer : DEFAULT_SURFACE_SYNCHRONIZER;
// Force LTR by default since most of the Bubbles UI is positioned manually by the user, or
// is centered. It greatly simplifies translation positioning/animations. Views that will
// actually lay out differently in RTL, such as the flyout and expanded view, will set their
// layout direction to LOCALE.
setLayoutDirection(LAYOUT_DIRECTION_LTR);
mBubbleContainer = new PhysicsAnimationLayout(context);
mBubbleContainer.setActiveController(mStackAnimationController);
mBubbleContainer.setElevation(elevation);
mBubbleContainer.setClipChildren(false);
addView(mBubbleContainer, new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT));
mExpandedViewContainer = new FrameLayout(context);
mExpandedViewContainer.setElevation(elevation);
mExpandedViewContainer.setClipChildren(false);
addView(mExpandedViewContainer);
mAnimatingOutSurfaceContainer = new FrameLayout(getContext());
mAnimatingOutSurfaceContainer.setLayoutParams(
new ViewGroup.LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
addView(mAnimatingOutSurfaceContainer);
mAnimatingOutSurfaceView = new SurfaceView(getContext());
mAnimatingOutSurfaceView.setUseAlpha();
mAnimatingOutSurfaceView.setZOrderOnTop(true);
boolean supportsRoundedCorners = ScreenDecorationsUtils.supportsRoundedCornersOnWindows(
mContext.getResources());
mAnimatingOutSurfaceView.setCornerRadius(supportsRoundedCorners ? mCornerRadius : 0);
mAnimatingOutSurfaceView.setLayoutParams(new ViewGroup.LayoutParams(0, 0));
mAnimatingOutSurfaceView.getHolder().addCallback(new SurfaceHolder.Callback() {
@Override
public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int i1, int i2) {}
@Override
public void surfaceCreated(SurfaceHolder surfaceHolder) {
mAnimatingOutSurfaceReady = true;
}
@Override
public void surfaceDestroyed(SurfaceHolder surfaceHolder) {
mAnimatingOutSurfaceReady = false;
}
});
mAnimatingOutSurfaceContainer.addView(mAnimatingOutSurfaceView);
mAnimatingOutSurfaceContainer.setPadding(
mExpandedViewContainer.getPaddingLeft(),
mExpandedViewContainer.getPaddingTop(),
mExpandedViewContainer.getPaddingRight(),
mExpandedViewContainer.getPaddingBottom());
setUpManageMenu();
setUpFlyout();
mFlyoutTransitionSpring.setSpring(new SpringForce()
.setStiffness(SpringForce.STIFFNESS_LOW)
.setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY));
mFlyoutTransitionSpring.addEndListener(mAfterFlyoutTransitionSpring);
setUpDismissView();
setClipChildren(false);
setFocusable(true);
mBubbleContainer.bringToFront();
mBubbleOverflow = mBubbleData.getOverflow();
mBubbleContainer.addView(mBubbleOverflow.getIconView(),
mBubbleContainer.getChildCount() /* index */,
new FrameLayout.LayoutParams(mPositioner.getBubbleSize(),
mPositioner.getBubbleSize()));
updateOverflow();
mBubbleOverflow.getIconView().setOnClickListener((View v) -> {
mBubbleData.setShowingOverflow(true);
mBubbleData.setSelectedBubble(mBubbleOverflow);
mBubbleData.setExpanded(true);
});
mScrim = new View(getContext());
mScrim.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
mScrim.setBackgroundDrawable(new ColorDrawable(
getResources().getColor(android.R.color.system_neutral1_1000)));
addView(mScrim);
mScrim.setAlpha(0f);
mManageMenuScrim = new View(getContext());
mManageMenuScrim.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
mManageMenuScrim.setBackgroundDrawable(new ColorDrawable(
getResources().getColor(android.R.color.system_neutral1_1000)));
addView(mManageMenuScrim, new LayoutParams(MATCH_PARENT, MATCH_PARENT));
mManageMenuScrim.setAlpha(0f);
mManageMenuScrim.setVisibility(INVISIBLE);
mOrientationChangedListener =
(v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
mPositioner.update();
onDisplaySizeChanged();
mExpandedAnimationController.updateResources();
mStackAnimationController.updateResources();
mBubbleOverflow.updateResources();
if (!isStackEduShowing() && mRelativeStackPositionBeforeRotation != null) {
mStackAnimationController.setStackPosition(
mRelativeStackPositionBeforeRotation);
mRelativeStackPositionBeforeRotation = null;
}
if (mIsExpanded) {
// Re-draw bubble row and pointer for new orientation.
beforeExpandedViewAnimation();
updateOverflowVisibility();
updatePointerPosition(false /* forIme */);
mExpandedAnimationController.expandFromStack(() -> {
afterExpandedViewAnimation();
showManageMenu(mShowingManage);
} /* after */);
PointF p = mPositioner.getExpandedBubbleXY(getBubbleIndex(mExpandedBubble),
getState());
final float translationY = mPositioner.getExpandedViewY(mExpandedBubble,
mPositioner.showBubblesVertically() ? p.y : p.x);
mExpandedViewContainer.setTranslationX(0f);
mExpandedViewContainer.setTranslationY(translationY);
mExpandedViewContainer.setAlpha(1f);
}
removeOnLayoutChangeListener(mOrientationChangedListener);
};
final float maxDismissSize = getResources().getDimensionPixelSize(
R.dimen.dismiss_circle_size);
final float minDismissSize = getResources().getDimensionPixelSize(
R.dimen.dismiss_circle_small);
final float sizePercent = minDismissSize / maxDismissSize;
mDismissBubbleAnimator = ValueAnimator.ofFloat(1f, 0f);
mDismissBubbleAnimator.addUpdateListener(animation -> {
final float animatedValue = (float) animation.getAnimatedValue();
if (mDismissView != null) {
mDismissView.setPivotX((mDismissView.getRight() - mDismissView.getLeft()) / 2f);
mDismissView.setPivotY((mDismissView.getBottom() - mDismissView.getTop()) / 2f);
final float scaleValue = Math.max(animatedValue, sizePercent);
mDismissView.getCircle().setScaleX(scaleValue);
mDismissView.getCircle().setScaleY(scaleValue);
}
if (mViewBeingDismissed != null) {
mViewBeingDismissed.setAlpha(Math.max(animatedValue, 0.7f));
}
});
// If the stack itself is clicked, it means none of its touchable views (bubbles, flyouts,
// TaskView, etc.) were touched. Collapse the stack if it's expanded.
setOnClickListener(view -> {
if (mShowingManage) {
showManageMenu(false /* show */);
} else if (mManageEduView != null && mManageEduView.getVisibility() == VISIBLE) {
mManageEduView.hide();
} else if (isStackEduShowing()) {
mStackEduView.hide(false /* isExpanding */);
} else if (mBubbleData.isExpanded()) {
mBubbleData.setExpanded(false);
} else {
maybeShowStackEdu();
}
});
animate()
.setInterpolator(Interpolators.PANEL_CLOSE_ACCELERATED)
.setDuration(FADE_IN_DURATION);
mExpandedViewAlphaAnimator.setDuration(EXPANDED_VIEW_ALPHA_ANIMATION_DURATION);
mExpandedViewAlphaAnimator.setInterpolator(Interpolators.PANEL_CLOSE_ACCELERATED);
mExpandedViewAlphaAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) {
// We need to be Z ordered on top in order for alpha animations to work.
mExpandedBubble.getExpandedView().setSurfaceZOrderedOnTop(true);
mExpandedBubble.getExpandedView().setAnimating(true);
}
}
@Override
public void onAnimationEnd(Animator animation) {
if (mExpandedBubble != null
&& mExpandedBubble.getExpandedView() != null
// The surface needs to be Z ordered on top for alpha values to work on the
// TaskView, and if we're temporarily hidden, we are still on the screen
// with alpha = 0f until we animate back. Stay Z ordered on top so the alpha
// = 0f remains in effect.
&& !mExpandedViewTemporarilyHidden) {
mExpandedBubble.getExpandedView().setSurfaceZOrderedOnTop(false);
mExpandedBubble.getExpandedView().setAnimating(false);
}
}
});
mExpandedViewAlphaAnimator.addUpdateListener(valueAnimator -> {
if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) {
float alpha = (float) valueAnimator.getAnimatedValue();
mExpandedBubble.getExpandedView().setContentAlpha(alpha);
mExpandedBubble.getExpandedView().setAlpha(alpha);
}
});
mAnimatingOutSurfaceAlphaAnimator.setDuration(EXPANDED_VIEW_ALPHA_ANIMATION_DURATION);
mAnimatingOutSurfaceAlphaAnimator.setInterpolator(Interpolators.PANEL_CLOSE_ACCELERATED);
mAnimatingOutSurfaceAlphaAnimator.addUpdateListener(valueAnimator -> {
if (!mExpandedViewTemporarilyHidden) {
mAnimatingOutSurfaceView.setAlpha((float) valueAnimator.getAnimatedValue());
}
});
mAnimatingOutSurfaceAlphaAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
releaseAnimatingOutBubbleBuffer();
}
});
}
/**
* Sets whether or not the stack should become temporarily invisible by moving off the side of
* the screen.
*
* If a flyout comes in while it's invisible, it will animate back in while the flyout is
* showing but disappear again when the flyout is gone.
*/
public void setTemporarilyInvisible(boolean invisible) {
mTemporarilyInvisible = invisible;
// If we are animating out, hide immediately if possible so we animate out with the status
// bar.
updateTemporarilyInvisibleAnimation(invisible /* hideImmediately */);
}
/**
* Animates the stack to be temporarily invisible, if needed.
*
* If we're currently dragging the stack, or a flyout is visible, the stack will remain visible.
* regardless of the value of {@link #mTemporarilyInvisible}. This method is called on ACTION_UP
* as well as whenever a flyout hides, so we will animate invisible at that point if needed.
*/
private void updateTemporarilyInvisibleAnimation(boolean hideImmediately) {
removeCallbacks(mAnimateTemporarilyInvisibleImmediate);
if (mIsDraggingStack) {
// If we're dragging the stack, don't animate it invisible.
return;
}
final boolean shouldHide =
mTemporarilyInvisible && mFlyout.getVisibility() != View.VISIBLE;
postDelayed(mAnimateTemporarilyInvisibleImmediate,
shouldHide && !hideImmediately ? ANIMATE_TEMPORARILY_INVISIBLE_DELAY : 0);
}
private final Runnable mAnimateTemporarilyInvisibleImmediate = () -> {
if (mTemporarilyInvisible && mFlyout.getVisibility() != View.VISIBLE) {
// To calculate a distance, bubble stack needs to be moved to become hidden,
// we need to take into account that the bubble stack is positioned on the edge
// of the available screen rect, which can be offset by system bars and cutouts.
if (mStackAnimationController.isStackOnLeftSide()) {
int availableRectOffsetX =
mPositioner.getAvailableRect().left - mPositioner.getScreenRect().left;
animate().translationX(-(mBubbleSize + availableRectOffsetX)).start();
} else {
int availableRectOffsetX =
mPositioner.getAvailableRect().right - mPositioner.getScreenRect().right;
animate().translationX(mBubbleSize - availableRectOffsetX).start();
}
} else {
animate().translationX(0).start();
}
};
private void setUpDismissView() {
if (mDismissView != null) {
removeView(mDismissView);
}
mDismissView = new DismissView(getContext());
int elevation = getResources().getDimensionPixelSize(R.dimen.bubble_elevation);
addView(mDismissView);
mDismissView.setElevation(elevation);
final ContentResolver contentResolver = getContext().getContentResolver();
final int dismissRadius = Settings.Secure.getInt(
contentResolver, "bubble_dismiss_radius", mBubbleSize * 2 /* default */);
// Save the MagneticTarget instance for the newly set up view - we'll add this to the
// MagnetizedObjects when the dismiss view gets shown.
mMagneticTarget = new MagnetizedObject.MagneticTarget(
mDismissView.getCircle(), dismissRadius);
mBubbleContainer.bringToFront();
}
// TODO: Create ManageMenuView and move setup / animations there
private void setUpManageMenu() {
if (mManageMenu != null) {
removeView(mManageMenu);
}
mManageMenu = (ViewGroup) LayoutInflater.from(getContext()).inflate(
R.layout.bubble_manage_menu, this, false);
mManageMenu.setVisibility(View.INVISIBLE);
PhysicsAnimator.getInstance(mManageMenu).setDefaultSpringConfig(mManageSpringConfig);
mManageMenu.setOutlineProvider(new ViewOutlineProvider() {
@Override
public void getOutline(View view, Outline outline) {
outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), mCornerRadius);
}
});
mManageMenu.setClipToOutline(true);
mManageMenu.findViewById(R.id.bubble_manage_menu_dismiss_container).setOnClickListener(
view -> {
showManageMenu(false /* show */);
dismissBubbleIfExists(mBubbleData.getSelectedBubble());
});
mManageMenu.findViewById(R.id.bubble_manage_menu_dont_bubble_container).setOnClickListener(
view -> {
showManageMenu(false /* show */);
mUnbubbleConversationCallback.accept(mBubbleData.getSelectedBubble().getKey());
});
mManageMenu.findViewById(R.id.bubble_manage_menu_settings_container).setOnClickListener(
view -> {
showManageMenu(false /* show */);
final BubbleViewProvider bubble = mBubbleData.getSelectedBubble();
if (bubble != null && mBubbleData.hasBubbleInStackWithKey(bubble.getKey())) {
// If it's in the stack it's a proper Bubble.
final Intent intent = ((Bubble) bubble).getSettingsIntent(mContext);
mBubbleData.setExpanded(false);
mContext.startActivityAsUser(intent, ((Bubble) bubble).getUser());
logBubbleEvent(bubble,
FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__HEADER_GO_TO_SETTINGS);
}
});
mManageSettingsIcon = mManageMenu.findViewById(R.id.bubble_manage_menu_settings_icon);
mManageSettingsText = mManageMenu.findViewById(R.id.bubble_manage_menu_settings_name);
// The menu itself should respect locale direction so the icons are on the correct side.
mManageMenu.setLayoutDirection(LAYOUT_DIRECTION_LOCALE);
addView(mManageMenu);
updateManageButtonListener();
}
/**
* Whether the educational view should show for the expanded view "manage" menu.
*/
private boolean shouldShowManageEdu() {
if (ActivityManager.isRunningInTestHarness()) {
return false;
}
final boolean seen = getPrefBoolean(ManageEducationViewKt.PREF_MANAGED_EDUCATION);
final boolean shouldShow = (!seen || BubbleDebugConfig.forceShowUserEducation(mContext))
&& mExpandedBubble != null;
if (BubbleDebugConfig.DEBUG_USER_EDUCATION) {
Log.d(TAG, "Show manage edu: " + shouldShow);
}
return shouldShow;
}
private void maybeShowManageEdu() {
if (!shouldShowManageEdu()) {
return;
}
if (mManageEduView == null) {
mManageEduView = new ManageEducationView(mContext, mPositioner);
addView(mManageEduView);
}
mManageEduView.show(mExpandedBubble.getExpandedView());
}
/**
* Whether education view should show for the collapsed stack.
*/
private boolean shouldShowStackEdu() {
if (ActivityManager.isRunningInTestHarness()) {
return false;
}
final boolean seen = getPrefBoolean(StackEducationViewKt.PREF_STACK_EDUCATION);
final boolean shouldShow = !seen || BubbleDebugConfig.forceShowUserEducation(mContext);
if (BubbleDebugConfig.DEBUG_USER_EDUCATION) {
Log.d(TAG, "Show stack edu: " + shouldShow);
}
return shouldShow;
}
private boolean getPrefBoolean(String key) {
return mContext.getSharedPreferences(mContext.getPackageName(), Context.MODE_PRIVATE)
.getBoolean(key, false /* default */);
}
/**
* @return true if education view for collapsed stack should show and was not showing before.
*/
private boolean maybeShowStackEdu() {
if (!shouldShowStackEdu() || isExpanded()) {
return false;
}
if (mStackEduView == null) {
mStackEduView = new StackEducationView(mContext, mPositioner, mBubbleController);
addView(mStackEduView);
}
mBubbleContainer.bringToFront();
// Ensure the stack is in the correct spot
mStackAnimationController.setStackPosition(mPositioner.getDefaultStartPosition());
return mStackEduView.show(mPositioner.getDefaultStartPosition());
}
private boolean isStackEduShowing() {
return mStackEduView != null && mStackEduView.getVisibility() == VISIBLE;
}
// Recreates & shows the education views. Call when a theme/config change happens.
private void updateUserEdu() {
if (isStackEduShowing()) {
removeView(mStackEduView);
mStackEduView = new StackEducationView(mContext, mPositioner, mBubbleController);
addView(mStackEduView);
mBubbleContainer.bringToFront(); // Stack appears on top of the stack education
// Ensure the stack is in the correct spot
mStackAnimationController.setStackPosition(mPositioner.getDefaultStartPosition());
mStackEduView.show(mPositioner.getDefaultStartPosition());
}
if (mManageEduView != null && mManageEduView.getVisibility() == VISIBLE) {
removeView(mManageEduView);
mManageEduView = new ManageEducationView(mContext, mPositioner);
addView(mManageEduView);
mManageEduView.show(mExpandedBubble.getExpandedView());
}
}
@SuppressLint("ClickableViewAccessibility")
private void setUpFlyout() {
if (mFlyout != null) {
removeView(mFlyout);
}
mFlyout = new BubbleFlyoutView(getContext(), mPositioner);
mFlyout.setVisibility(GONE);
mFlyout.setOnClickListener(mFlyoutClickListener);
mFlyout.setOnTouchListener(mFlyoutTouchListener);
addView(mFlyout, new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
}
void updateFontScale() {
setUpManageMenu();
mFlyout.updateFontSize();
for (Bubble b : mBubbleData.getBubbles()) {
if (b.getExpandedView() != null) {
b.getExpandedView().updateFontSize();
}
}
if (mBubbleOverflow != null && mBubbleOverflow.getExpandedView() != null) {
mBubbleOverflow.getExpandedView().updateFontSize();
}
}
private void updateOverflow() {
mBubbleOverflow.update();
mBubbleContainer.reorderView(mBubbleOverflow.getIconView(),
mBubbleContainer.getChildCount() - 1 /* index */);
updateOverflowVisibility();
}
void updateOverflowButtonDot() {
for (Bubble b : mBubbleData.getOverflowBubbles()) {
if (b.showDot()) {
mBubbleOverflow.setShowDot(true);
return;
}
}
mBubbleOverflow.setShowDot(false);
}
/**
* Handle theme changes.
*/
public void onThemeChanged() {
setUpFlyout();
setUpManageMenu();
setUpDismissView();
updateOverflow();
updateUserEdu();
updateExpandedViewTheme();
mScrim.setBackgroundDrawable(new ColorDrawable(
getResources().getColor(android.R.color.system_neutral1_1000)));
mManageMenuScrim.setBackgroundDrawable(new ColorDrawable(
getResources().getColor(android.R.color.system_neutral1_1000)));
}
/**
* Respond to the phone being rotated by repositioning the stack and hiding any flyouts.
* This is called prior to the rotation occurring, any values that should be updated
* based on the new rotation should occur in {@link #mOrientationChangedListener}.
*/
public void onOrientationChanged() {
mRelativeStackPositionBeforeRotation = new RelativeStackPosition(
mPositioner.getRestingPosition(),
mPositioner.getAllowableStackPositionRegion(getBubbleCount()));
addOnLayoutChangeListener(mOrientationChangedListener);
hideFlyoutImmediate();
}
/** Tells the views with locale-dependent layout direction to resolve the new direction. */
public void onLayoutDirectionChanged(int direction) {
mManageMenu.setLayoutDirection(direction);
mFlyout.setLayoutDirection(direction);
if (mStackEduView != null) {
mStackEduView.setLayoutDirection(direction);
}
if (mManageEduView != null) {
mManageEduView.setLayoutDirection(direction);
}
updateExpandedViewDirection(direction);
}
/** Respond to the display size change by recalculating view size and location. */
public void onDisplaySizeChanged() {
updateOverflow();
setUpFlyout();
setUpDismissView();
updateUserEdu();
mBubbleSize = mPositioner.getBubbleSize();
for (Bubble b : mBubbleData.getBubbles()) {
if (b.getIconView() == null) {
Log.d(TAG, "Display size changed. Icon null: " + b);
continue;
}
b.getIconView().setLayoutParams(new LayoutParams(mBubbleSize, mBubbleSize));
if (b.getExpandedView() != null) {
b.getExpandedView().updateDimensions();
}
}
mBubbleOverflow.getIconView().setLayoutParams(new LayoutParams(mBubbleSize, mBubbleSize));
mExpandedAnimationController.updateResources();
mStackAnimationController.updateResources();
mDismissView.updateResources();
mMagneticTarget.setMagneticFieldRadiusPx(mBubbleSize * 2);
if (!isStackEduShowing()) {
mStackAnimationController.setStackPosition(
new RelativeStackPosition(
mPositioner.getRestingPosition(),
mPositioner.getAllowableStackPositionRegion(getBubbleCount())));
}
if (mIsExpanded) {
updateExpandedView();
}
setUpManageMenu();
}
@Override
public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo) {
inoutInfo.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
mTempRect.setEmpty();
getTouchableRegion(mTempRect);
inoutInfo.touchableRegion.set(mTempRect);
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
mPositioner.update();
getViewTreeObserver().addOnComputeInternalInsetsListener(this);
getViewTreeObserver().addOnDrawListener(mSystemGestureExcludeUpdater);
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
getViewTreeObserver().removeOnPreDrawListener(mViewUpdater);
getViewTreeObserver().removeOnDrawListener(mSystemGestureExcludeUpdater);
getViewTreeObserver().removeOnComputeInternalInsetsListener(this);
if (mBubbleOverflow != null) {
mBubbleOverflow.cleanUpExpandedState();
}
}
@Override
public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
super.onInitializeAccessibilityNodeInfoInternal(info);
setupLocalMenu(info);
}
void updateExpandedViewTheme() {
final List<Bubble> bubbles = mBubbleData.getBubbles();
if (bubbles.isEmpty()) {
return;
}
bubbles.forEach(bubble -> {
if (bubble.getExpandedView() != null) {
bubble.getExpandedView().applyThemeAttrs();
}
});
}
void updateExpandedViewDirection(int direction) {
final List<Bubble> bubbles = mBubbleData.getBubbles();
if (bubbles.isEmpty()) {
return;
}
bubbles.forEach(bubble -> {
if (bubble.getExpandedView() != null) {
bubble.getExpandedView().setLayoutDirection(direction);
}
});
}
void setupLocalMenu(AccessibilityNodeInfo info) {
Resources res = mContext.getResources();
// Custom local actions.
AccessibilityAction moveTopLeft = new AccessibilityAction(R.id.action_move_top_left,
res.getString(R.string.bubble_accessibility_action_move_top_left));
info.addAction(moveTopLeft);
AccessibilityAction moveTopRight = new AccessibilityAction(R.id.action_move_top_right,
res.getString(R.string.bubble_accessibility_action_move_top_right));
info.addAction(moveTopRight);
AccessibilityAction moveBottomLeft = new AccessibilityAction(R.id.action_move_bottom_left,
res.getString(R.string.bubble_accessibility_action_move_bottom_left));
info.addAction(moveBottomLeft);
AccessibilityAction moveBottomRight = new AccessibilityAction(R.id.action_move_bottom_right,
res.getString(R.string.bubble_accessibility_action_move_bottom_right));
info.addAction(moveBottomRight);
// Default actions.
info.addAction(AccessibilityAction.ACTION_DISMISS);
if (mIsExpanded) {
info.addAction(AccessibilityAction.ACTION_COLLAPSE);
} else {
info.addAction(AccessibilityAction.ACTION_EXPAND);
}
}
@Override
public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
if (super.performAccessibilityActionInternal(action, arguments)) {
return true;
}
final RectF stackBounds = mPositioner.getAllowableStackPositionRegion(getBubbleCount());
// R constants are not final so we cannot use switch-case here.
if (action == AccessibilityNodeInfo.ACTION_DISMISS) {
mBubbleData.dismissAll(Bubbles.DISMISS_ACCESSIBILITY_ACTION);
announceForAccessibility(
getResources().getString(R.string.accessibility_bubble_dismissed));
return true;
} else if (action == AccessibilityNodeInfo.ACTION_COLLAPSE) {
mBubbleData.setExpanded(false);
return true;
} else if (action == AccessibilityNodeInfo.ACTION_EXPAND) {
mBubbleData.setExpanded(true);
return true;
} else if (action == R.id.action_move_top_left) {
mStackAnimationController.springStackAfterFling(stackBounds.left, stackBounds.top);
return true;
} else if (action == R.id.action_move_top_right) {
mStackAnimationController.springStackAfterFling(stackBounds.right, stackBounds.top);
return true;
} else if (action == R.id.action_move_bottom_left) {
mStackAnimationController.springStackAfterFling(stackBounds.left, stackBounds.bottom);
return true;
} else if (action == R.id.action_move_bottom_right) {
mStackAnimationController.springStackAfterFling(stackBounds.right, stackBounds.bottom);
return true;
}
return false;
}
/**
* Update content description for a11y TalkBack.
*/
public void updateContentDescription() {
if (mBubbleData.getBubbles().isEmpty()) {
return;
}
for (int i = 0; i < mBubbleData.getBubbles().size(); i++) {
final Bubble bubble = mBubbleData.getBubbles().get(i);
final String appName = bubble.getAppName();
String titleStr = bubble.getTitle();
if (titleStr == null) {
titleStr = getResources().getString(R.string.notification_bubble_title);
}
if (bubble.getIconView() != null) {
if (mIsExpanded || i > 0) {
bubble.getIconView().setContentDescription(getResources().getString(
R.string.bubble_content_description_single, titleStr, appName));
} else {
final int moreCount = mBubbleContainer.getChildCount() - 1;
bubble.getIconView().setContentDescription(getResources().getString(
R.string.bubble_content_description_stack,
titleStr, appName, moreCount));
}
}
}
}
/**
* Update bubbles' icon views accessibility states.
*/
public void updateBubblesAcessibillityStates() {
for (int i = 0; i < mBubbleData.getBubbles().size(); i++) {
Bubble prevBubble = i > 0 ? mBubbleData.getBubbles().get(i - 1) : null;
Bubble bubble = mBubbleData.getBubbles().get(i);
View bubbleIconView = bubble.getIconView();
if (bubbleIconView == null) {
continue;
}
if (mIsExpanded) {
// when stack is expanded
// all bubbles are important for accessibility
bubbleIconView
.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
View prevBubbleIconView = prevBubble != null ? prevBubble.getIconView() : null;
if (prevBubbleIconView != null) {
bubbleIconView.setAccessibilityDelegate(new View.AccessibilityDelegate() {
@Override
public void onInitializeAccessibilityNodeInfo(View v,
AccessibilityNodeInfo info) {
super.onInitializeAccessibilityNodeInfo(v, info);
info.setTraversalAfter(prevBubbleIconView);
}
});
}
} else {
// when stack is collapsed, only the top bubble is important for accessibility,
bubbleIconView.setImportantForAccessibility(
i == 0 ? View.IMPORTANT_FOR_ACCESSIBILITY_YES :
View.IMPORTANT_FOR_ACCESSIBILITY_NO);
}
}
if (mIsExpanded) {
// make the overflow bubble last in the accessibility traversal order
View bubbleOverflowIconView =
mBubbleOverflow != null ? mBubbleOverflow.getIconView() : null;
if (bubbleOverflowIconView != null && !mBubbleData.getBubbles().isEmpty()) {
Bubble lastBubble =
mBubbleData.getBubbles().get(mBubbleData.getBubbles().size() - 1);
View lastBubbleIconView = lastBubble.getIconView();
if (lastBubbleIconView != null) {
bubbleOverflowIconView.setAccessibilityDelegate(
new View.AccessibilityDelegate() {
@Override
public void onInitializeAccessibilityNodeInfo(View v,
AccessibilityNodeInfo info) {
super.onInitializeAccessibilityNodeInfo(v, info);
info.setTraversalAfter(lastBubbleIconView);
}
});
}
}
}
}
private void updateSystemGestureExcludeRects() {
// Exclude the region occupied by the first BubbleView in the stack
Rect excludeZone = mSystemGestureExclusionRects.get(0);
if (getBubbleCount() > 0) {
View firstBubble = mBubbleContainer.getChildAt(0);
excludeZone.set(firstBubble.getLeft(), firstBubble.getTop(), firstBubble.getRight(),
firstBubble.getBottom());
excludeZone.offset((int) (firstBubble.getTranslationX() + 0.5f),
(int) (firstBubble.getTranslationY() + 0.5f));
mBubbleContainer.setSystemGestureExclusionRects(mSystemGestureExclusionRects);
} else {
excludeZone.setEmpty();
mBubbleContainer.setSystemGestureExclusionRects(Collections.emptyList());
}
}
/**
* Sets the listener to notify when the bubble stack is expanded.
*/
public void setExpandListener(Bubbles.BubbleExpandListener listener) {
mExpandListener = listener;
}
/** Sets the function to call to un-bubble the given conversation. */
public void setUnbubbleConversationCallback(
Consumer<String> unbubbleConversationCallback) {
mUnbubbleConversationCallback = unbubbleConversationCallback;
}
/**
* Whether the stack of bubbles is expanded or not.
*/
public boolean isExpanded() {
return mIsExpanded;
}
/**
* Whether the stack of bubbles is animating to or from expansion.
*/
public boolean isExpansionAnimating() {
return mIsExpansionAnimating;
}
/**
* Whether the stack of bubbles is animating a switch between bubbles.
*/
public boolean isSwitchAnimating() {
return mIsBubbleSwitchAnimating;
}
/**
* The {@link Bubble} that is expanded, null if one does not exist.
*/
@VisibleForTesting
@Nullable
public BubbleViewProvider getExpandedBubble() {
return mExpandedBubble;
}
// via BubbleData.Listener
@SuppressLint("ClickableViewAccessibility")
void addBubble(Bubble bubble) {
if (DEBUG_BUBBLE_STACK_VIEW) {
Log.d(TAG, "addBubble: " + bubble);
}
final boolean firstBubble = getBubbleCount() == 0;
if (firstBubble && shouldShowStackEdu()) {
// Override the default stack position if we're showing user education.
mStackAnimationController.setStackPosition(mPositioner.getDefaultStartPosition());
}
if (bubble.getIconView() == null) {
return;
}
mBubbleContainer.addView(bubble.getIconView(), 0,
new FrameLayout.LayoutParams(mPositioner.getBubbleSize(),
mPositioner.getBubbleSize()));
if (firstBubble) {
mStackOnLeftOrWillBe = mStackAnimationController.isStackOnLeftSide();
}
// Set the dot position to the opposite of the side the stack is resting on, since the stack
// resting slightly off-screen would result in the dot also being off-screen.
bubble.getIconView().setDotBadgeOnLeft(!mStackOnLeftOrWillBe /* onLeft */);
bubble.getIconView().setOnClickListener(mBubbleClickListener);
bubble.getIconView().setOnTouchListener(mBubbleTouchListener);
updateBubbleShadows(false /* showForAllBubbles */);
animateInFlyoutForBubble(bubble);
requestUpdate();
logBubbleEvent(bubble, FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__POSTED);
}
// via BubbleData.Listener
void removeBubble(Bubble bubble) {
if (DEBUG_BUBBLE_STACK_VIEW) {
Log.d(TAG, "removeBubble: " + bubble);
}
// Remove it from the views
for (int i = 0; i < getBubbleCount(); i++) {
View v = mBubbleContainer.getChildAt(i);
if (v instanceof BadgedImageView
&& ((BadgedImageView) v).getKey().equals(bubble.getKey())) {
mBubbleContainer.removeViewAt(i);
if (mBubbleData.hasOverflowBubbleWithKey(bubble.getKey())) {
bubble.cleanupExpandedView();
} else {
bubble.cleanupViews();
}
updateExpandedView();
if (getBubbleCount() == 0 && !isExpanded()) {
// This is the last bubble and the stack is collapsed
updateStackPosition();
}
logBubbleEvent(bubble, FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__DISMISSED);
return;
}
}
// If a bubble is suppressed, it is not attached to the container. Clean it up.
if (bubble.isSuppressed()) {
bubble.cleanupViews();
} else {
Log.d(TAG, "was asked to remove Bubble, but didn't find the view! " + bubble);
}
}
private void updateOverflowVisibility() {
mBubbleOverflow.setVisible((mIsExpanded || mBubbleData.isShowingOverflow())
? VISIBLE
: GONE);
}
// via BubbleData.Listener
void updateBubble(Bubble bubble) {
animateInFlyoutForBubble(bubble);
requestUpdate();
logBubbleEvent(bubble, FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__UPDATED);
}
/**
* Update bubble order and pointer position.
*/
public void updateBubbleOrder(List<Bubble> bubbles) {
final Runnable reorder = () -> {
for (int i = 0; i < bubbles.size(); i++) {
Bubble bubble = bubbles.get(i);
mBubbleContainer.reorderView(bubble.getIconView(), i);
}
};
if (mIsExpanded || isExpansionAnimating()) {
reorder.run();
updateBadges(false /* setBadgeForCollapsedStack */);
updateZOrder();
} else if (!isExpansionAnimating()) {
List<View> bubbleViews = bubbles.stream()
.map(b -> b.getIconView()).collect(Collectors.toList());
mStackAnimationController.animateReorder(bubbleViews, reorder);
}
updatePointerPosition(false /* forIme */);
}
/**
* Changes the currently selected bubble. If the stack is already expanded, the newly selected
* bubble will be shown immediately. This does not change the expanded state or change the
* position of any bubble.
*/
// via BubbleData.Listener
public void setSelectedBubble(@Nullable BubbleViewProvider bubbleToSelect) {
if (DEBUG_BUBBLE_STACK_VIEW) {
Log.d(TAG, "setSelectedBubble: " + bubbleToSelect);
}
if (bubbleToSelect == null) {
mBubbleData.setShowingOverflow(false);
return;
}
// Ignore this new bubble only if it is the exact same bubble object. Otherwise, we'll want
// to re-render it even if it has the same key (equals() returns true). If the currently
// expanded bubble is removed and instantly re-added, we'll get back a new Bubble instance
// with the same key (with newly inflated expanded views), and we need to render those new
// views.
if (mExpandedBubble == bubbleToSelect) {
return;
}
if (bubbleToSelect.getKey().equals(BubbleOverflow.KEY)) {
mBubbleData.setShowingOverflow(true);
} else {
mBubbleData.setShowingOverflow(false);
}
if (mIsExpanded && mIsExpansionAnimating) {
// If the bubble selection changed during the expansion animation, the expanding bubble
// probably crashed or immediately removed itself (or, we just got unlucky with a new
// auto-expanding bubble showing up at just the right time). Cancel the animations so we
// can start fresh.
cancelAllExpandCollapseSwitchAnimations();
}
showManageMenu(false /* show */);
// If we're expanded, screenshot the currently expanded bubble (before expanding the newly
// selected bubble) so we can animate it out.
if (mIsExpanded && mExpandedBubble != null && mExpandedBubble.getExpandedView() != null
&& !mExpandedViewTemporarilyHidden) {
if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) {
// Before screenshotting, have the real TaskView show on top of other surfaces
// so that the screenshot doesn't flicker on top of it.
mExpandedBubble.getExpandedView().setSurfaceZOrderedOnTop(true);
}
try {
screenshotAnimatingOutBubbleIntoSurface((success) -> {
mAnimatingOutSurfaceContainer.setVisibility(
success ? View.VISIBLE : View.INVISIBLE);
showNewlySelectedBubble(bubbleToSelect);
});
} catch (Exception e) {
showNewlySelectedBubble(bubbleToSelect);
e.printStackTrace();
}
} else {
showNewlySelectedBubble(bubbleToSelect);
}
}
private void showNewlySelectedBubble(BubbleViewProvider bubbleToSelect) {
final BubbleViewProvider previouslySelected = mExpandedBubble;
mExpandedBubble = bubbleToSelect;
mExpandedViewAnimationController.setExpandedView(mExpandedBubble.getExpandedView());
if (mIsExpanded) {
hideCurrentInputMethod();
// Make the container of the expanded view transparent before removing the expanded view
// from it. Otherwise a punch hole created by {@link android.view.SurfaceView} in the
// expanded view becomes visible on the screen. See b/126856255
mExpandedViewContainer.setAlpha(0.0f);
mSurfaceSynchronizer.syncSurfaceAndRun(() -> {
if (previouslySelected != null) {
previouslySelected.setTaskViewVisibility(false);
}
updateExpandedBubble();
requestUpdate();
logBubbleEvent(previouslySelected,
FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__COLLAPSED);
logBubbleEvent(bubbleToSelect,
FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__EXPANDED);
notifyExpansionChanged(previouslySelected, false /* expanded */);
notifyExpansionChanged(bubbleToSelect, true /* expanded */);
});
}
}
/**
* Changes the expanded state of the stack.
* Don't call this directly, call mBubbleData#setExpanded.
*
* @param shouldExpand whether the bubble stack should appear expanded
*/
// via BubbleData.Listener
public void setExpanded(boolean shouldExpand) {
if (DEBUG_BUBBLE_STACK_VIEW) {
Log.d(TAG, "setExpanded: " + shouldExpand);
}
if (!shouldExpand) {
// If we're collapsing, release the animating-out surface immediately since we have no
// need for it, and this ensures it cannot remain visible as we collapse.
releaseAnimatingOutBubbleBuffer();
}
if (shouldExpand == mIsExpanded) {
return;
}
boolean wasExpanded = mIsExpanded;
hideCurrentInputMethod();
mBubbleController.getSysuiProxy().onStackExpandChanged(shouldExpand);
if (wasExpanded) {
stopMonitoringSwipeUpGesture();
if (HOME_GESTURE_ENABLED) {
animateCollapse();
} else {
animateCollapseWithScale();
}
logBubbleEvent(mExpandedBubble, FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__COLLAPSED);
} else {
animateExpansion();
// TODO: move next line to BubbleData
logBubbleEvent(mExpandedBubble, FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__EXPANDED);
logBubbleEvent(mExpandedBubble,
FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__STACK_EXPANDED);
if (HOME_GESTURE_ENABLED) {
mBubbleController.isNotificationPanelExpanded(notifPanelExpanded -> {
if (!notifPanelExpanded && mIsExpanded) {
startMonitoringSwipeUpGesture();
}
});
}
}
notifyExpansionChanged(mExpandedBubble, mIsExpanded);
}
/**
* Monitor for swipe up gesture that is used to collapse expanded view
*/
void startMonitoringSwipeUpGesture() {
if (DEBUG_BUBBLE_GESTURE) {
Log.d(TAG, "startMonitoringSwipeUpGesture");
}
stopMonitoringSwipeUpGestureInternal();
if (isGestureNavEnabled()) {
mBubblesNavBarGestureTracker = new BubblesNavBarGestureTracker(mContext, mPositioner);
mBubblesNavBarGestureTracker.start(mSwipeUpListener);
setOnTouchListener(mContainerSwipeListener);
}
}
private boolean isGestureNavEnabled() {
return mContext.getResources().getInteger(
com.android.internal.R.integer.config_navBarInteractionMode)
== WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL;
}
/**
* Stop monitoring for swipe up gesture
*/
void stopMonitoringSwipeUpGesture() {
if (DEBUG_BUBBLE_GESTURE) {
Log.d(TAG, "stopMonitoringSwipeUpGesture");
}
stopMonitoringSwipeUpGestureInternal();
}
private void stopMonitoringSwipeUpGestureInternal() {
if (mBubblesNavBarGestureTracker != null) {
mBubblesNavBarGestureTracker.stop();
mBubblesNavBarGestureTracker = null;
setOnTouchListener(null);
}
}
/**
* Called when back press occurs while bubbles are expanded.
*/
public void onBackPressed() {
if (mIsExpanded) {
if (mShowingManage) {
showManageMenu(false);
} else if (mManageEduView != null && mManageEduView.getVisibility() == VISIBLE) {
mManageEduView.hide();
} else {
mBubbleData.setExpanded(false);
}
}
}
void setBubbleSuppressed(Bubble bubble, boolean suppressed) {
if (DEBUG_BUBBLE_STACK_VIEW) {
Log.d(TAG, "setBubbleSuppressed: suppressed=" + suppressed + " bubble=" + bubble);
}
if (suppressed) {
int index = getBubbleIndex(bubble);
mBubbleContainer.removeViewAt(index);
updateExpandedView();
} else {
if (bubble.getIconView() == null) {
return;
}
if (bubble.getIconView().getParent() != null) {
Log.e(TAG, "Bubble is already added to parent. Can't unsuppress: " + bubble);
return;
}
int index = mBubbleData.getBubbles().indexOf(bubble);
// Add the view back to the correct position
mBubbleContainer.addView(bubble.getIconView(), index,
new LayoutParams(mPositioner.getBubbleSize(),
mPositioner.getBubbleSize()));
updateBubbleShadows(false /* showForAllBubbles */);
requestUpdate();
}
}
/**
* Asks the BubbleController to hide the IME from anywhere, whether it's focused on Bubbles or
* not.
*/
void hideCurrentInputMethod() {
mPositioner.setImeVisible(false, 0);
mBubbleController.hideCurrentInputMethod();
}
/** Set the stack position to whatever the positioner says. */
void updateStackPosition() {
mStackAnimationController.setStackPosition(mPositioner.getRestingPosition());
mDismissView.hide();
}
private void beforeExpandedViewAnimation() {
mIsExpansionAnimating = true;
hideFlyoutImmediate();
updateExpandedBubble();
updateExpandedView();
}
private void afterExpandedViewAnimation() {
mIsExpansionAnimating = false;
updateExpandedView();
requestUpdate();
}
/** Animate the expanded view hidden. This is done while we're dragging out a bubble. */
private void hideExpandedViewIfNeeded() {
if (mExpandedViewTemporarilyHidden
|| mExpandedBubble == null
|| mExpandedBubble.getExpandedView() == null) {
return;
}
mExpandedViewTemporarilyHidden = true;
// Scale down.
PhysicsAnimator.getInstance(mExpandedViewContainerMatrix)
.spring(AnimatableScaleMatrix.SCALE_X,
AnimatableScaleMatrix.getAnimatableValueForScaleFactor(
1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT),
mScaleOutSpringConfig)
.spring(AnimatableScaleMatrix.SCALE_Y,
AnimatableScaleMatrix.getAnimatableValueForScaleFactor(
1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT),
mScaleOutSpringConfig)
.addUpdateListener((target, values) ->
mExpandedViewContainer.setAnimationMatrix(mExpandedViewContainerMatrix))
.start();
// Animate alpha from 1f to 0f.
mExpandedViewAlphaAnimator.reverse();
}
/**
* Animate the expanded view visible again. This is done when we're done dragging out a bubble.
*/
private void showExpandedViewIfNeeded() {
if (!mExpandedViewTemporarilyHidden) {
return;
}
mExpandedViewTemporarilyHidden = false;
PhysicsAnimator.getInstance(mExpandedViewContainerMatrix)
.spring(AnimatableScaleMatrix.SCALE_X,
AnimatableScaleMatrix.getAnimatableValueForScaleFactor(1f),
mScaleOutSpringConfig)
.spring(AnimatableScaleMatrix.SCALE_Y,
AnimatableScaleMatrix.getAnimatableValueForScaleFactor(1f),
mScaleOutSpringConfig)
.addUpdateListener((target, values) ->
mExpandedViewContainer.setAnimationMatrix(mExpandedViewContainerMatrix))
.start();
mExpandedViewAlphaAnimator.start();
}
private void showScrim(boolean show) {
if (show) {
mScrim.animate()
.setInterpolator(ALPHA_IN)
.alpha(SCRIM_ALPHA)
.start();
} else {
mScrim.animate()
.alpha(0f)
.setInterpolator(ALPHA_OUT)
.start();
}
}
private void animateExpansion() {
cancelDelayedExpandCollapseSwitchAnimations();
final boolean showVertically = mPositioner.showBubblesVertically();
mIsExpanded = true;
if (isStackEduShowing()) {
mStackEduView.hide(true /* fromExpansion */);
}
beforeExpandedViewAnimation();
showScrim(true);
updateZOrder();
updateBadges(false /* setBadgeForCollapsedStack */);
mBubbleContainer.setActiveController(mExpandedAnimationController);
updateOverflowVisibility();
updatePointerPosition(false /* forIme */);
mExpandedAnimationController.expandFromStack(() -> {
if (mIsExpanded && mExpandedBubble.getExpandedView() != null) {
maybeShowManageEdu();
}
} /* after */);
int index;
if (mExpandedBubble != null && BubbleOverflow.KEY.equals(mExpandedBubble.getKey())) {
index = mBubbleData.getBubbles().size();
} else {
index = getBubbleIndex(mExpandedBubble);
}
PointF p = mPositioner.getExpandedBubbleXY(index, getState());
final float translationY = mPositioner.getExpandedViewY(mExpandedBubble,
mPositioner.showBubblesVertically() ? p.y : p.x);
mExpandedViewContainer.setTranslationX(0f);
mExpandedViewContainer.setTranslationY(translationY);
mExpandedViewContainer.setAlpha(1f);
// How far horizontally the bubble will be animating. We'll wait a bit longer for bubbles
// that are animating farther, so that the expanded view doesn't move as much.
final float relevantStackPosition = showVertically
? mStackAnimationController.getStackPosition().y
: mStackAnimationController.getStackPosition().x;
final float bubbleWillBeAt = showVertically
? p.y
: p.x;
final float distanceAnimated = Math.abs(bubbleWillBeAt - relevantStackPosition);
// Wait for the path animation target to reach its end, and add a small amount of extra time
// if the bubble is moving a lot horizontally.
long startDelay = 0L;
// Should not happen since we lay out before expanding, but just in case...
if (getWidth() > 0) {
startDelay = (long)
(ExpandedAnimationController.EXPAND_COLLAPSE_TARGET_ANIM_DURATION * 1.2f
+ (distanceAnimated / getWidth()) * 30);
}
// Set the pivot point for the scale, so the expanded view animates out from the bubble.
if (showVertically) {
float pivotX;
if (mStackOnLeftOrWillBe) {
pivotX = p.x + mBubbleSize + mExpandedViewPadding;
} else {
pivotX = p.x - mExpandedViewPadding;
}
mExpandedViewContainerMatrix.setScale(
1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT,
1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT,
pivotX,
p.y + mBubbleSize / 2f);
} else {
mExpandedViewContainerMatrix.setScale(
1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT,
1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT,
p.x + mBubbleSize / 2f,
p.y + mBubbleSize + mExpandedViewPadding);
}
mExpandedViewContainer.setAnimationMatrix(mExpandedViewContainerMatrix);
if (mExpandedBubble.getExpandedView() != null) {
mExpandedBubble.getExpandedView().setContentAlpha(0f);
mExpandedBubble.getExpandedView().setAlpha(0f);
// We'll be starting the alpha animation after a slight delay, so set this flag early
// here.
mExpandedBubble.getExpandedView().setAnimating(true);
}
mDelayedAnimation = () -> {
mExpandedViewAlphaAnimator.start();
PhysicsAnimator.getInstance(mExpandedViewContainerMatrix).cancel();
PhysicsAnimator.getInstance(mExpandedViewContainerMatrix)
.spring(AnimatableScaleMatrix.SCALE_X,
AnimatableScaleMatrix.getAnimatableValueForScaleFactor(1f),
mScaleInSpringConfig)
.spring(AnimatableScaleMatrix.SCALE_Y,
AnimatableScaleMatrix.getAnimatableValueForScaleFactor(1f),
mScaleInSpringConfig)
.addUpdateListener((target, values) -> {
if (mExpandedBubble == null || mExpandedBubble.getIconView() == null) {
return;
}
float translation = showVertically
? mExpandedBubble.getIconView().getTranslationY()
: mExpandedBubble.getIconView().getTranslationX();
mExpandedViewContainerMatrix.postTranslate(
translation - bubbleWillBeAt,
0);
mExpandedViewContainer.setAnimationMatrix(
mExpandedViewContainerMatrix);
})
.withEndActions(() -> {
mExpandedViewContainer.setAnimationMatrix(null);
afterExpandedViewAnimation();
if (mExpandedBubble != null
&& mExpandedBubble.getExpandedView() != null) {
mExpandedBubble.getExpandedView()
.setSurfaceZOrderedOnTop(false);
}
})
.start();
};
mMainExecutor.executeDelayed(mDelayedAnimation, startDelay);
}
private void animateCollapseWithScale() {
cancelDelayedExpandCollapseSwitchAnimations();
if (mManageEduView != null && mManageEduView.getVisibility() == VISIBLE) {
mManageEduView.hide();
}
// Hide the menu if it's visible.
showManageMenu(false);
mIsExpanded = false;
mIsExpansionAnimating = true;
showScrim(false);
mBubbleContainer.cancelAllAnimations();
// If we were in the middle of swapping, the animating-out surface would have been scaling
// to zero - finish it off.
PhysicsAnimator.getInstance(mAnimatingOutSurfaceContainer).cancel();
mAnimatingOutSurfaceContainer.setScaleX(0f);
mAnimatingOutSurfaceContainer.setScaleY(0f);
// Let the expanded animation controller know that it shouldn't animate child adds/reorders
// since we're about to animate collapsed.
mExpandedAnimationController.notifyPreparingToCollapse();
mExpandedAnimationController.collapseBackToStack(
mStackAnimationController.getStackPositionAlongNearestHorizontalEdge()
/* collapseTo */,
() -> mBubbleContainer.setActiveController(mStackAnimationController));
int index;
if (mExpandedBubble != null && BubbleOverflow.KEY.equals(mExpandedBubble.getKey())) {
index = mBubbleData.getBubbles().size();
} else {
index = mBubbleData.getBubbles().indexOf(mExpandedBubble);
}
// Value the bubble is animating from (back into the stack).
final PointF p = mPositioner.getExpandedBubbleXY(index, getState());
if (mPositioner.showBubblesVertically()) {
float pivotX;
float pivotY = p.y + mBubbleSize / 2f;
if (mStackOnLeftOrWillBe) {
pivotX = mPositioner.getAvailableRect().left + mBubbleSize + mExpandedViewPadding;
} else {
pivotX = mPositioner.getAvailableRect().right - mBubbleSize - mExpandedViewPadding;
}
mExpandedViewContainerMatrix.setScale(
1f, 1f,
pivotX, pivotY);
} else {
mExpandedViewContainerMatrix.setScale(
1f, 1f,
p.x + mBubbleSize / 2f,
p.y + mBubbleSize + mExpandedViewPadding);
}
mExpandedViewAlphaAnimator.reverse();
// When the animation completes, we should no longer be showing the content.
if (mExpandedBubble.getExpandedView() != null) {
mExpandedBubble.getExpandedView().setContentVisibility(false);
}
PhysicsAnimator.getInstance(mExpandedViewContainerMatrix).cancel();
PhysicsAnimator.getInstance(mExpandedViewContainerMatrix)
.spring(AnimatableScaleMatrix.SCALE_X,
AnimatableScaleMatrix.getAnimatableValueForScaleFactor(
1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT),
mScaleOutSpringConfig)
.spring(AnimatableScaleMatrix.SCALE_Y,
AnimatableScaleMatrix.getAnimatableValueForScaleFactor(
1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT),
mScaleOutSpringConfig)
.addUpdateListener((target, values) -> {
mExpandedViewContainer.setAnimationMatrix(mExpandedViewContainerMatrix);
})
.withEndActions(() -> {
final BubbleViewProvider previouslySelected = mExpandedBubble;
beforeExpandedViewAnimation();
if (mManageEduView != null) {
mManageEduView.hide();
}
if (DEBUG_BUBBLE_STACK_VIEW) {
Log.d(TAG, "animateCollapse");
Log.d(TAG, BubbleDebugConfig.formatBubblesString(getBubblesOnScreen(),
mExpandedBubble));
}
updateOverflowVisibility();
updateZOrder();
updateBadges(true /* setBadgeForCollapsedStack */);
afterExpandedViewAnimation();
if (previouslySelected != null) {
previouslySelected.setTaskViewVisibility(false);
}
})
.start();
}
private void animateCollapse() {
cancelDelayedExpandCollapseSwitchAnimations();
if (mManageEduView != null && mManageEduView.getVisibility() == VISIBLE) {
mManageEduView.hide();
}
mIsExpanded = false;
mIsExpansionAnimating = true;
showScrim(false);
mBubbleContainer.cancelAllAnimations();
// If we were in the middle of swapping, the animating-out surface would have been scaling
// to zero - finish it off.
PhysicsAnimator.getInstance(mAnimatingOutSurfaceContainer).cancel();
mAnimatingOutSurfaceContainer.setScaleX(0f);
mAnimatingOutSurfaceContainer.setScaleY(0f);
// Let the expanded animation controller know that it shouldn't animate child adds/reorders
// since we're about to animate collapsed.
mExpandedAnimationController.notifyPreparingToCollapse();
final Runnable collapseBackToStack = () -> mExpandedAnimationController.collapseBackToStack(
mStackAnimationController
.getStackPositionAlongNearestHorizontalEdge()
/* collapseTo */,
() -> mBubbleContainer.setActiveController(mStackAnimationController));
final Runnable after = () -> {
final BubbleViewProvider previouslySelected = mExpandedBubble;
// TODO(b/231350255): investigate why this call is needed here
beforeExpandedViewAnimation();
if (mManageEduView != null) {
mManageEduView.hide();
}
if (DEBUG_BUBBLE_STACK_VIEW) {
Log.d(TAG, "animateCollapse");
Log.d(TAG, BubbleDebugConfig.formatBubblesString(getBubblesOnScreen(),
mExpandedBubble));
}
updateOverflowVisibility();
updateZOrder();
updateBadges(true /* setBadgeForCollapsedStack */);
afterExpandedViewAnimation();
if (previouslySelected != null) {
previouslySelected.setTaskViewVisibility(false);
}
mExpandedViewAnimationController.reset();
};
mExpandedViewAnimationController.animateCollapse(collapseBackToStack, after);
if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) {
// When the animation completes, we should no longer be showing the content.
// This won't actually update content visibility immediately, if we are currently
// animating. But updates the internal state for the content to be hidden after
// animation completes.
mExpandedBubble.getExpandedView().setContentVisibility(false);
}
}
private void animateSwitchBubbles() {
// If we're no longer expanded, this is meaningless.
if (!mIsExpanded) {
return;
}
mIsBubbleSwitchAnimating = true;
// The surface contains a screenshot of the animating out bubble, so we just need to animate
// it out (and then release the GraphicBuffer).
PhysicsAnimator.getInstance(mAnimatingOutSurfaceContainer).cancel();
mAnimatingOutSurfaceAlphaAnimator.reverse();
mExpandedViewAlphaAnimator.start();
if (mPositioner.showBubblesVertically()) {
float translationX = mStackAnimationController.isStackOnLeftSide()
? mAnimatingOutSurfaceContainer.getTranslationX() + mBubbleSize * 2
: mAnimatingOutSurfaceContainer.getTranslationX();
PhysicsAnimator.getInstance(mAnimatingOutSurfaceContainer)
.spring(DynamicAnimation.TRANSLATION_X, translationX, mTranslateSpringConfig)
.start();
} else {
PhysicsAnimator.getInstance(mAnimatingOutSurfaceContainer)
.spring(DynamicAnimation.TRANSLATION_Y,
mAnimatingOutSurfaceContainer.getTranslationY() - mBubbleSize,
mTranslateSpringConfig)
.start();
}
boolean isOverflow = mExpandedBubble != null
&& mExpandedBubble.getKey().equals(BubbleOverflow.KEY);
PointF p = mPositioner.getExpandedBubbleXY(isOverflow
? mBubbleContainer.getChildCount() - 1
: mBubbleData.getBubbles().indexOf(mExpandedBubble),
getState());
mExpandedViewContainer.setAlpha(1f);
mExpandedViewContainer.setVisibility(View.VISIBLE);
if (mPositioner.showBubblesVertically()) {
float pivotX;
float pivotY = p.y + mBubbleSize / 2f;
if (mStackOnLeftOrWillBe) {
pivotX = p.x + mBubbleSize + mExpandedViewPadding;
} else {
pivotX = p.x - mExpandedViewPadding;
}
mExpandedViewContainerMatrix.setScale(
1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT,
1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT,
pivotX, pivotY);
} else {
mExpandedViewContainerMatrix.setScale(
1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT,
1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT,
p.x + mBubbleSize / 2f,
p.y + mBubbleSize + mExpandedViewPadding);
}
mExpandedViewContainer.setAnimationMatrix(mExpandedViewContainerMatrix);
mMainExecutor.executeDelayed(() -> {
if (!mIsExpanded) {
mIsBubbleSwitchAnimating = false;
return;
}
PhysicsAnimator.getInstance(mExpandedViewContainerMatrix).cancel();
PhysicsAnimator.getInstance(mExpandedViewContainerMatrix)
.spring(AnimatableScaleMatrix.SCALE_X,
AnimatableScaleMatrix.getAnimatableValueForScaleFactor(1f),
mScaleInSpringConfig)
.spring(AnimatableScaleMatrix.SCALE_Y,
AnimatableScaleMatrix.getAnimatableValueForScaleFactor(1f),
mScaleInSpringConfig)
.addUpdateListener((target, values) -> {
mExpandedViewContainer.setAnimationMatrix(mExpandedViewContainerMatrix);
})
.withEndActions(() -> {
mExpandedViewTemporarilyHidden = false;
mIsBubbleSwitchAnimating = false;
mExpandedViewContainer.setAnimationMatrix(null);
})
.start();
}, 25);
}
/**
* Cancels any delayed steps for expand/collapse and bubble switch animations, and resets the is
* animating flags for those animations.
*/
private void cancelDelayedExpandCollapseSwitchAnimations() {
mMainExecutor.removeCallbacks(mDelayedAnimation);
mIsExpansionAnimating = false;
mIsBubbleSwitchAnimating = false;
}
private void cancelAllExpandCollapseSwitchAnimations() {
cancelDelayedExpandCollapseSwitchAnimations();
PhysicsAnimator.getInstance(mAnimatingOutSurfaceView).cancel();
PhysicsAnimator.getInstance(mExpandedViewContainerMatrix).cancel();
mExpandedViewContainer.setAnimationMatrix(null);
}
private void notifyExpansionChanged(BubbleViewProvider bubble, boolean expanded) {
if (mExpandListener != null && bubble != null) {
mExpandListener.onBubbleExpandChanged(expanded, bubble.getKey());
}
}
/**
* Updates the stack based for IME changes. When collapsed it'll move the stack if it
* overlaps where they IME would be. When expanded it'll shift the expanded bubbles
* if they might overlap with the IME (this only happens for large screens)
* and clip the expanded view.
*/
public void setImeVisible(boolean visible) {
if (HOME_GESTURE_ENABLED) {
setImeVisibleInternal(visible);
} else {
setImeVisibleWithoutClipping(visible);
}
}
private void setImeVisibleWithoutClipping(boolean visible) {
if ((mIsExpansionAnimating || mIsBubbleSwitchAnimating) && mIsExpanded) {
// This will update the animation so the bubbles move to position for the IME
mExpandedAnimationController.expandFromStack(() -> {
updatePointerPosition(false /* forIme */);
afterExpandedViewAnimation();
} /* after */);
return;
}
if (!mIsExpanded && getBubbleCount() > 0) {
final float stackDestinationY =
mStackAnimationController.animateForImeVisibility(visible);
// How far the stack is animating due to IME, we'll just animate the flyout by that
// much too.
final float stackDy =
stackDestinationY - mStackAnimationController.getStackPosition().y;
// If the flyout is visible, translate it along with the bubble stack.
if (mFlyout.getVisibility() == VISIBLE) {
PhysicsAnimator.getInstance(mFlyout)
.spring(DynamicAnimation.TRANSLATION_Y,
mFlyout.getTranslationY() + stackDy,
FLYOUT_IME_ANIMATION_SPRING_CONFIG)
.start();
}
} else if (mPositioner.showBubblesVertically() && mIsExpanded
&& mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) {
float selectedY = mPositioner.getExpandedBubbleXY(getState().selectedIndex,
getState()).y;
float newExpandedViewTop = mPositioner.getExpandedViewY(mExpandedBubble, selectedY);
mExpandedBubble.getExpandedView().setImeVisible(visible);
if (!mExpandedBubble.getExpandedView().isUsingMaxHeight()) {
mExpandedViewContainer.animate().translationY(newExpandedViewTop);
}
List<Animator> animList = new ArrayList();
for (int i = 0; i < mBubbleContainer.getChildCount(); i++) {
View child = mBubbleContainer.getChildAt(i);
float transY = mPositioner.getExpandedBubbleXY(i, getState()).y;
ObjectAnimator anim = ObjectAnimator.ofFloat(child, TRANSLATION_Y, transY);
animList.add(anim);
}
updatePointerPosition(true /* forIme */);
AnimatorSet set = new AnimatorSet();
set.playTogether(animList);
set.start();
}
}
private void setImeVisibleInternal(boolean visible) {
if ((mIsExpansionAnimating || mIsBubbleSwitchAnimating) && mIsExpanded) {
// This will update the animation so the bubbles move to position for the IME
mExpandedAnimationController.expandFromStack(() -> {
updatePointerPosition(false /* forIme */);
afterExpandedViewAnimation();
mExpandedViewAnimationController.animateForImeVisibilityChange(visible);
} /* after */);
return;
}
if (!mIsExpanded && getBubbleCount() > 0) {
final float stackDestinationY =
mStackAnimationController.animateForImeVisibility(visible);
// How far the stack is animating due to IME, we'll just animate the flyout by that
// much too.
final float stackDy =
stackDestinationY - mStackAnimationController.getStackPosition().y;
// If the flyout is visible, translate it along with the bubble stack.
if (mFlyout.getVisibility() == VISIBLE) {
PhysicsAnimator.getInstance(mFlyout)
.spring(DynamicAnimation.TRANSLATION_Y,
mFlyout.getTranslationY() + stackDy,
FLYOUT_IME_ANIMATION_SPRING_CONFIG)
.start();
}
}
if (mIsExpanded) {
mExpandedViewAnimationController.animateForImeVisibilityChange(visible);
if (mPositioner.showBubblesVertically()
&& mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) {
float selectedY = mPositioner.getExpandedBubbleXY(getState().selectedIndex,
getState()).y;
float newExpandedViewTop = mPositioner.getExpandedViewY(mExpandedBubble, selectedY);
mExpandedBubble.getExpandedView().setImeVisible(visible);
if (!mExpandedBubble.getExpandedView().isUsingMaxHeight()) {
mExpandedViewContainer.animate().translationY(newExpandedViewTop);
}
List<Animator> animList = new ArrayList();
for (int i = 0; i < mBubbleContainer.getChildCount(); i++) {
View child = mBubbleContainer.getChildAt(i);
float transY = mPositioner.getExpandedBubbleXY(i, getState()).y;
ObjectAnimator anim = ObjectAnimator.ofFloat(child, TRANSLATION_Y, transY);
animList.add(anim);
}
updatePointerPosition(true /* forIme */);
AnimatorSet set = new AnimatorSet();
set.playTogether(animList);
set.start();
}
}
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() != MotionEvent.ACTION_DOWN && ev.getActionIndex() != mPointerIndexDown) {
// Ignore touches from additional pointer indices.
return false;
}
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
mPointerIndexDown = ev.getActionIndex();
} else if (ev.getAction() == MotionEvent.ACTION_UP
|| ev.getAction() == MotionEvent.ACTION_CANCEL) {
mPointerIndexDown = -1;
}
boolean dispatched = super.dispatchTouchEvent(ev);
// If a new bubble arrives while the collapsed stack is being dragged, it will be positioned
// at the front of the stack (under the touch position). Subsequent ACTION_MOVE events will
// then be passed to the new bubble, which will not consume them since it hasn't received an
// ACTION_DOWN yet. Work around this by passing MotionEvents directly to the touch handler
// until the current gesture ends with an ACTION_UP event.
if (!dispatched && !mIsExpanded && mIsGestureInProgress) {
dispatched = mBubbleTouchListener.onTouch(this /* view */, ev);
}
mIsGestureInProgress =
ev.getAction() != MotionEvent.ACTION_UP
&& ev.getAction() != MotionEvent.ACTION_CANCEL;
return dispatched;
}
void setFlyoutStateForDragLength(float deltaX) {
// This shouldn't happen, but if it does, just wait until the flyout lays out. This method
// is continually called.
if (mFlyout.getWidth() <= 0) {
return;
}
final boolean onLeft = mStackAnimationController.isStackOnLeftSide();
mFlyoutDragDeltaX = deltaX;
final float collapsePercent =
onLeft ? -deltaX / mFlyout.getWidth() : deltaX / mFlyout.getWidth();
mFlyout.setCollapsePercent(Math.min(1f, Math.max(0f, collapsePercent)));
// Calculate how to translate the flyout if it has been dragged too far in either direction.
float overscrollTranslation = 0f;
if (collapsePercent < 0f || collapsePercent > 1f) {
// Whether we are more than 100% transitioned to the dot.
final boolean overscrollingPastDot = collapsePercent > 1f;
// Whether we are overscrolling physically to the left - this can either be pulling the
// flyout away from the stack (if the stack is on the right) or pushing it to the left
// after it has already become the dot.
final boolean overscrollingLeft =
(onLeft && collapsePercent > 1f) || (!onLeft && collapsePercent < 0f);
overscrollTranslation =
(overscrollingPastDot ? collapsePercent - 1f : collapsePercent * -1)
* (overscrollingLeft ? -1 : 1)
* (mFlyout.getWidth() / (FLYOUT_OVERSCROLL_ATTENUATION_FACTOR
// Attenuate the smaller dot less than the larger flyout.
/ (overscrollingPastDot ? 2 : 1)));
}
mFlyout.setTranslationX(mFlyout.getRestingTranslationX() + overscrollTranslation);
}
/** Passes the MotionEvent to the magnetized object and returns true if it was consumed. */
private boolean passEventToMagnetizedObject(MotionEvent event) {
return mMagnetizedObject != null && mMagnetizedObject.maybeConsumeMotionEvent(event);
}
/**
* Dismisses the magnetized object - either an individual bubble, if we're expanded, or the
* stack, if we're collapsed.
*/
private void dismissMagnetizedObject() {
if (mIsExpanded) {
final View draggedOutBubbleView = (View) mMagnetizedObject.getUnderlyingObject();
dismissBubbleIfExists(mBubbleData.getBubbleWithView(draggedOutBubbleView));
} else {
mBubbleData.dismissAll(Bubbles.DISMISS_USER_GESTURE);
}
}
private void dismissBubbleIfExists(@Nullable BubbleViewProvider bubble) {
if (bubble != null && mBubbleData.hasBubbleInStackWithKey(bubble.getKey())) {
if (mIsExpanded && mBubbleData.getBubbles().size() > 1
&& Objects.equals(bubble, mExpandedBubble)) {
// If we have more than 1 bubble and it's the current bubble being dismissed,
// we will perform the switch animation
mIsBubbleSwitchAnimating = true;
}
mBubbleData.dismissBubbleWithKey(bubble.getKey(), Bubbles.DISMISS_USER_GESTURE);
}
}
/** Prepares and starts the dismiss animation on the bubble stack. */
private void animateDismissBubble(View targetView, boolean applyAlpha) {
mViewBeingDismissed = targetView;
if (mViewBeingDismissed == null) {
return;
}
if (applyAlpha) {
mDismissBubbleAnimator.removeAllListeners();
mDismissBubbleAnimator.start();
} else {
mDismissBubbleAnimator.removeAllListeners();
mDismissBubbleAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
resetDismissAnimator();
}
@Override
public void onAnimationCancel(Animator animation) {
super.onAnimationCancel(animation);
resetDismissAnimator();
}
});
mDismissBubbleAnimator.reverse();
}
}
private void resetDismissAnimator() {
mDismissBubbleAnimator.removeAllListeners();
mDismissBubbleAnimator.cancel();
if (mViewBeingDismissed != null) {
mViewBeingDismissed.setAlpha(1f);
mViewBeingDismissed = null;
}
if (mDismissView != null) {
mDismissView.getCircle().setScaleX(1f);
mDismissView.getCircle().setScaleY(1f);
}
}
/** Animates the flyout collapsed (to dot), or the reverse, starting with the given velocity. */
private void animateFlyoutCollapsed(boolean collapsed, float velX) {
final boolean onLeft = mStackAnimationController.isStackOnLeftSide();
// If the flyout was tapped, we want a higher stiffness for the collapse animation so it's
// faster.
mFlyoutTransitionSpring.getSpring().setStiffness(
(mBubbleToExpandAfterFlyoutCollapse != null)
? SpringForce.STIFFNESS_MEDIUM
: SpringForce.STIFFNESS_LOW);
mFlyoutTransitionSpring
.setStartValue(mFlyoutDragDeltaX)
.setStartVelocity(velX)
.animateToFinalPosition(collapsed
? (onLeft ? -mFlyout.getWidth() : mFlyout.getWidth())
: 0f);
}
private boolean shouldShowFlyout(Bubble bubble) {
Bubble.FlyoutMessage flyoutMessage = bubble.getFlyoutMessage();
final BadgedImageView bubbleView = bubble.getIconView();
if (flyoutMessage == null
|| flyoutMessage.message == null
|| !bubble.showFlyout()
|| isStackEduShowing()
|| isExpanded()
|| mIsExpansionAnimating
|| mIsGestureInProgress
|| mBubbleToExpandAfterFlyoutCollapse != null
|| bubbleView == null) {
if (bubbleView != null && mFlyout.getVisibility() != VISIBLE) {
bubbleView.removeDotSuppressionFlag(BadgedImageView.SuppressionFlag.FLYOUT_VISIBLE);
}
// Skip the message if none exists, we're expanded or animating expansion, or we're
// about to expand a bubble from the previous tapped flyout, or if bubble view is null.
return false;
}
return true;
}
/**
* Animates in the flyout for the given bubble, if available, and then hides it after some time.
*/
@VisibleForTesting
void animateInFlyoutForBubble(Bubble bubble) {
if (!shouldShowFlyout(bubble)) {
return;
}
mFlyoutDragDeltaX = 0f;
clearFlyoutOnHide();
mAfterFlyoutHidden = () -> {
// Null it out to ensure it runs once.
mAfterFlyoutHidden = null;
if (mBubbleToExpandAfterFlyoutCollapse != null) {
// User tapped on the flyout and we should expand
mBubbleData.setSelectedBubble(mBubbleToExpandAfterFlyoutCollapse);
mBubbleData.setExpanded(true);
mBubbleToExpandAfterFlyoutCollapse = null;
}
// Stop suppressing the dot now that the flyout has morphed into the dot.
if (bubble.getIconView() != null) {
bubble.getIconView().removeDotSuppressionFlag(
BadgedImageView.SuppressionFlag.FLYOUT_VISIBLE);
}
// Hide the stack after a delay, if needed.
updateTemporarilyInvisibleAnimation(false /* hideImmediately */);
};
// Suppress the dot when we are animating the flyout.
bubble.getIconView().addDotSuppressionFlag(
BadgedImageView.SuppressionFlag.FLYOUT_VISIBLE);
// Start flyout expansion. Post in case layout isn't complete and getWidth returns 0.
post(() -> {
// An auto-expanding bubble could have been posted during the time it takes to
// layout.
if (isExpanded() || bubble.getIconView() == null) {
return;
}
final Runnable expandFlyoutAfterDelay = () -> {
mAnimateInFlyout = () -> {
mFlyout.setVisibility(VISIBLE);
updateTemporarilyInvisibleAnimation(false /* hideImmediately */);
mFlyoutDragDeltaX =
mStackAnimationController.isStackOnLeftSide()
? -mFlyout.getWidth()
: mFlyout.getWidth();
animateFlyoutCollapsed(false /* collapsed */, 0 /* velX */);
mFlyout.postDelayed(mHideFlyout, FLYOUT_HIDE_AFTER);
};
mFlyout.postDelayed(mAnimateInFlyout, 200);
};
if (mFlyout.getVisibility() == View.VISIBLE) {
mFlyout.animateUpdate(bubble.getFlyoutMessage(),
mStackAnimationController.getStackPosition(), !bubble.showDot(),
bubble.getIconView().getDotCenter(),
mAfterFlyoutHidden /* onHide */);
} else {
mFlyout.setVisibility(INVISIBLE);
mFlyout.setupFlyoutStartingAsDot(bubble.getFlyoutMessage(),
mStackAnimationController.getStackPosition(),
mStackAnimationController.isStackOnLeftSide(),
bubble.getIconView().getDotColor() /* dotColor */,
expandFlyoutAfterDelay /* onLayoutComplete */,
mAfterFlyoutHidden /* onHide */,
bubble.getIconView().getDotCenter(),
!bubble.showDot());
}
mFlyout.bringToFront();
});
mFlyout.removeCallbacks(mHideFlyout);
mFlyout.postDelayed(mHideFlyout, FLYOUT_HIDE_AFTER);
logBubbleEvent(bubble, FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__FLYOUT);
}
/** Hide the flyout immediately and cancel any pending hide runnables. */
private void hideFlyoutImmediate() {
clearFlyoutOnHide();
mFlyout.removeCallbacks(mAnimateInFlyout);
mFlyout.removeCallbacks(mHideFlyout);
mFlyout.hideFlyout();
}
private void clearFlyoutOnHide() {
mFlyout.removeCallbacks(mAnimateInFlyout);
if (mAfterFlyoutHidden == null) {
return;
}
mAfterFlyoutHidden.run();
mAfterFlyoutHidden = null;
}
/**
* Fills the Rect with the touchable region of the bubbles. This will be used by WindowManager
* to decide which touch events go to Bubbles.
*
* Bubbles is below the status bar/notification shade but above application windows. If you're
* trying to get touch events from the status bar or another higher-level window layer, you'll
* need to re-order TYPE_BUBBLES in WindowManagerPolicy so that we have the opportunity to steal
* them.
*/
public void getTouchableRegion(Rect outRect) {
if (isStackEduShowing()) {
// When user education shows then capture all touches
outRect.set(0, 0, getWidth(), getHeight());
return;
}
if (!mIsExpanded) {
if (getBubbleCount() > 0 || mBubbleData.isShowingOverflow()) {
mBubbleContainer.getChildAt(0).getBoundsOnScreen(outRect);
// Increase the touch target size of the bubble
outRect.top -= mBubbleTouchPadding;
outRect.left -= mBubbleTouchPadding;
outRect.right += mBubbleTouchPadding;
outRect.bottom += mBubbleTouchPadding;
}
} else {
mBubbleContainer.getBoundsOnScreen(outRect);
// Account for the IME in the touchable region so that the touchable region of the
// Bubble window doesn't obscure the IME. The touchable region affects which areas
// of the screen can be excluded by lower windows (IME is just above the embedded task)
outRect.bottom -= mPositioner.getImeHeight();
}
if (mFlyout.getVisibility() == View.VISIBLE) {
final Rect flyoutBounds = new Rect();
mFlyout.getBoundsOnScreen(flyoutBounds);
outRect.union(flyoutBounds);
}
}
private void requestUpdate() {
if (mViewUpdatedRequested || mIsExpansionAnimating) {
return;
}
mViewUpdatedRequested = true;
getViewTreeObserver().addOnPreDrawListener(mViewUpdater);
invalidate();
}
/** Hide or show the manage menu for the currently expanded bubble. */
@VisibleForTesting
public void showManageMenu(boolean show) {
mShowingManage = show;
// This should not happen, since the manage menu is only visible when there's an expanded
// bubble. If we end up in this state, just hide the menu immediately.
if (mExpandedBubble == null || mExpandedBubble.getExpandedView() == null) {
mManageMenu.setVisibility(View.INVISIBLE);
mManageMenuScrim.setVisibility(INVISIBLE);
mBubbleController.getSysuiProxy().onManageMenuExpandChanged(false /* show */);
return;
}
if (show) {
mManageMenuScrim.setVisibility(VISIBLE);
mManageMenuScrim.setTranslationZ(mManageMenu.getElevation() - 1f);
}
Runnable endAction = () -> {
if (!show) {
mManageMenuScrim.setVisibility(INVISIBLE);
mManageMenuScrim.setTranslationZ(0f);
}
};
mBubbleController.getSysuiProxy().onManageMenuExpandChanged(show);
mManageMenuScrim.animate()
.setInterpolator(show ? ALPHA_IN : ALPHA_OUT)
.alpha(show ? SCRIM_ALPHA : 0f)
.withEndAction(endAction)
.start();
// If available, update the manage menu's settings option with the expanded bubble's app
// name and icon.
if (show) {
final Bubble bubble = mBubbleData.getBubbleInStackWithKey(mExpandedBubble.getKey());
if (bubble != null) {
mManageSettingsIcon.setImageBitmap(bubble.getRawAppBadge());
mManageSettingsText.setText(getResources().getString(
R.string.bubbles_app_settings, bubble.getAppName()));
}
}
if (mExpandedBubble.getExpandedView().getTaskView() != null) {
mExpandedBubble.getExpandedView().getTaskView().setObscuredTouchRect(mShowingManage
? new Rect(0, 0, getWidth(), getHeight())
: null);
}
final boolean isLtr =
getResources().getConfiguration().getLayoutDirection() == LAYOUT_DIRECTION_LTR;
// When the menu is open, it should be at these coordinates. The menu pops out to the right
// in LTR and to the left in RTL.
mExpandedBubble.getExpandedView().getManageButtonBoundsOnScreen(mTempRect);
final float margin = mExpandedBubble.getExpandedView().getManageButtonMargin();
final float targetX = isLtr
? mTempRect.left - margin
: mTempRect.right + margin - mManageMenu.getWidth();
final float targetY = mTempRect.bottom - mManageMenu.getHeight();
final float xOffsetForAnimation = (isLtr ? 1 : -1) * mManageMenu.getWidth() / 4f;
if (show) {
mManageMenu.setScaleX(0.5f);
mManageMenu.setScaleY(0.5f);
mManageMenu.setTranslationX(targetX - xOffsetForAnimation);
mManageMenu.setTranslationY(targetY + mManageMenu.getHeight() / 4f);
mManageMenu.setAlpha(0f);
PhysicsAnimator.getInstance(mManageMenu)
.spring(DynamicAnimation.ALPHA, 1f)
.spring(DynamicAnimation.SCALE_X, 1f)
.spring(DynamicAnimation.SCALE_Y, 1f)
.spring(DynamicAnimation.TRANSLATION_X, targetX)
.spring(DynamicAnimation.TRANSLATION_Y, targetY)
.withEndActions(() -> {
View child = mManageMenu.getChildAt(0);
child.requestAccessibilityFocus();
// Update the AV's obscured touchable region for the new visibility state.
mExpandedBubble.getExpandedView().updateObscuredTouchableRegion();
})
.start();
mManageMenu.setVisibility(View.VISIBLE);
} else {
PhysicsAnimator.getInstance(mManageMenu)
.spring(DynamicAnimation.ALPHA, 0f)
.spring(DynamicAnimation.SCALE_X, 0.5f)
.spring(DynamicAnimation.SCALE_Y, 0.5f)
.spring(DynamicAnimation.TRANSLATION_X, targetX - xOffsetForAnimation)
.spring(DynamicAnimation.TRANSLATION_Y, targetY + mManageMenu.getHeight() / 4f)
.withEndActions(() -> {
mManageMenu.setVisibility(View.INVISIBLE);
if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) {
// Update the AV's obscured touchable region for the new state.
mExpandedBubble.getExpandedView().updateObscuredTouchableRegion();
}
})
.start();
}
}
private void updateExpandedBubble() {
if (DEBUG_BUBBLE_STACK_VIEW) {
Log.d(TAG, "updateExpandedBubble()");
}
mExpandedViewContainer.removeAllViews();
if (mIsExpanded && mExpandedBubble != null
&& mExpandedBubble.getExpandedView() != null) {
BubbleExpandedView bev = mExpandedBubble.getExpandedView();
bev.setContentVisibility(false);
bev.setAnimating(!mIsExpansionAnimating);
mExpandedViewContainerMatrix.setScaleX(0f);
mExpandedViewContainerMatrix.setScaleY(0f);
mExpandedViewContainerMatrix.setTranslate(0f, 0f);
mExpandedViewContainer.setVisibility(View.INVISIBLE);
mExpandedViewContainer.setAlpha(0f);
mExpandedViewContainer.addView(bev);
postDelayed(() -> {
// Set the Manage button click handler from postDelayed. This appears to resolve
// a race condition with adding the BubbleExpandedView view to the expanded view
// container. Due to the race condition the click handler sometimes is not set up
// correctly and is never called.
updateManageButtonListener();
}, 0);
if (!mIsExpansionAnimating) {
mSurfaceSynchronizer.syncSurfaceAndRun(() -> {
post(this::animateSwitchBubbles);
});
}
}
}
private void updateManageButtonListener() {
if (mIsExpanded && mExpandedBubble != null
&& mExpandedBubble.getExpandedView() != null) {
BubbleExpandedView bev = mExpandedBubble.getExpandedView();
bev.setManageClickListener((view) -> {
showManageMenu(true /* show */);
});
}
}
/**
* Requests a snapshot from the currently expanded bubble's TaskView and displays it in a
* SurfaceView. This allows us to load a newly expanded bubble's Activity into the TaskView,
* while animating the (screenshot of the) previously selected bubble's content away.
*
* @param onComplete Callback to run once we're done here - called with 'false' if something
* went wrong, or 'true' if the SurfaceView is now showing a screenshot of the
* expanded bubble.
*/
private void screenshotAnimatingOutBubbleIntoSurface(Consumer<Boolean> onComplete) {
if (!mIsExpanded || mExpandedBubble == null || mExpandedBubble.getExpandedView() == null) {
// You can't animate null.
onComplete.accept(false);
return;
}
final BubbleExpandedView animatingOutExpandedView = mExpandedBubble.getExpandedView();
// Release the previous screenshot if it hasn't been released already.
if (mAnimatingOutBubbleBuffer != null) {
releaseAnimatingOutBubbleBuffer();
}
try {
mAnimatingOutBubbleBuffer = animatingOutExpandedView.snapshotActivitySurface();
} catch (Exception e) {
// If we fail for any reason, print the stack trace and then notify the callback of our
// failure. This is not expected to occur, but it's not worth crashing over.
Log.wtf(TAG, e);
onComplete.accept(false);
}
if (mAnimatingOutBubbleBuffer == null
|| mAnimatingOutBubbleBuffer.getHardwareBuffer() == null) {
// While no exception was thrown, we were unable to get a snapshot.
onComplete.accept(false);
return;
}
// Make sure the surface container's properties have been reset.
PhysicsAnimator.getInstance(mAnimatingOutSurfaceContainer).cancel();
mAnimatingOutSurfaceContainer.setScaleX(1f);
mAnimatingOutSurfaceContainer.setScaleY(1f);
final float translationX = mPositioner.showBubblesVertically() && mStackOnLeftOrWillBe
? mExpandedViewContainer.getPaddingLeft() + mPositioner.getPointerSize()
: mExpandedViewContainer.getPaddingLeft();
mAnimatingOutSurfaceContainer.setTranslationX(translationX);
mAnimatingOutSurfaceContainer.setTranslationY(0);
final int[] taskViewLocation =
mExpandedBubble.getExpandedView().getTaskViewLocationOnScreen();
final int[] surfaceViewLocation = mAnimatingOutSurfaceView.getLocationOnScreen();
// Translate the surface to overlap the real TaskView.
mAnimatingOutSurfaceContainer.setTranslationY(
taskViewLocation[1] - surfaceViewLocation[1]);
// Set the width/height of the SurfaceView to match the snapshot.
mAnimatingOutSurfaceView.getLayoutParams().width =
mAnimatingOutBubbleBuffer.getHardwareBuffer().getWidth();
mAnimatingOutSurfaceView.getLayoutParams().height =
mAnimatingOutBubbleBuffer.getHardwareBuffer().getHeight();
mAnimatingOutSurfaceView.requestLayout();
// Post to wait for layout.
post(() -> {
// The buffer might have been destroyed if the user is mashing on bubbles, that's okay.
if (mAnimatingOutBubbleBuffer == null
|| mAnimatingOutBubbleBuffer.getHardwareBuffer() == null
|| mAnimatingOutBubbleBuffer.getHardwareBuffer().isClosed()) {
onComplete.accept(false);
return;
}
if (!mIsExpanded || !mAnimatingOutSurfaceReady) {
onComplete.accept(false);
return;
}
// Attach the buffer! We're now displaying the snapshot.
mAnimatingOutSurfaceView.getHolder().getSurface().attachAndQueueBufferWithColorSpace(
mAnimatingOutBubbleBuffer.getHardwareBuffer(),
mAnimatingOutBubbleBuffer.getColorSpace());
mAnimatingOutSurfaceView.setAlpha(1f);
mExpandedViewContainer.setVisibility(View.GONE);
mSurfaceSynchronizer.syncSurfaceAndRun(() -> {
post(() -> {
onComplete.accept(true);
});
});
});
}
/**
* Releases the buffer containing the screenshot of the animating-out bubble, if it exists and
* isn't yet destroyed.
*/
private void releaseAnimatingOutBubbleBuffer() {
if (mAnimatingOutBubbleBuffer != null
&& !mAnimatingOutBubbleBuffer.getHardwareBuffer().isClosed()) {
mAnimatingOutBubbleBuffer.getHardwareBuffer().close();
}
}
private void updateExpandedView() {
if (DEBUG_BUBBLE_STACK_VIEW) {
Log.d(TAG, "updateExpandedView: mIsExpanded=" + mIsExpanded);
}
boolean isOverflowExpanded = mExpandedBubble != null
&& BubbleOverflow.KEY.equals(mExpandedBubble.getKey());
int[] paddings = mPositioner.getExpandedViewContainerPadding(
mStackAnimationController.isStackOnLeftSide(), isOverflowExpanded);
mExpandedViewContainer.setPadding(paddings[0], paddings[1], paddings[2], paddings[3]);
if (mIsExpansionAnimating) {
mExpandedViewContainer.setVisibility(mIsExpanded ? VISIBLE : GONE);
}
if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) {
PointF p = mPositioner.getExpandedBubbleXY(getBubbleIndex(mExpandedBubble),
getState());
mExpandedViewContainer.setTranslationY(mPositioner.getExpandedViewY(mExpandedBubble,
mPositioner.showBubblesVertically() ? p.y : p.x));
mExpandedViewContainer.setTranslationX(0f);
mExpandedBubble.getExpandedView().updateView(
mExpandedViewContainer.getLocationOnScreen());
updatePointerPosition(false /* forIme */);
}
mStackOnLeftOrWillBe = mStackAnimationController.isStackOnLeftSide();
}
/**
* Updates whether each of the bubbles should show shadows. When collapsed & resting, only the
* visible bubbles (top 2) will show a shadow. When the stack is being dragged, everything
* shows a shadow. When an individual bubble is dragged out, it should show a shadow.
*/
private void updateBubbleShadows(boolean showForAllBubbles) {
int bubbleCount = getBubbleCount();
for (int i = 0; i < bubbleCount; i++) {
final float z = (mPositioner.getMaxBubbles() * mBubbleElevation) - i;
BadgedImageView bv = (BadgedImageView) mBubbleContainer.getChildAt(i);
boolean isDraggedOut = mMagnetizedObject != null
&& mMagnetizedObject.getUnderlyingObject().equals(bv);
if (showForAllBubbles || isDraggedOut) {
bv.setZ(z);
} else {
final float tz = i < NUM_VISIBLE_WHEN_RESTING ? z : 0f;
bv.setZ(tz);
}
}
}
/**
* When the bubbles are flung and then rest, the shadows stack up for the bubbles hidden
* beneath the top two bubbles, to avoid this we animate the Z translations once the stack
* is resting so that they fade away nicely.
*/
private void animateShadows() {
int bubbleCount = getBubbleCount();
for (int i = 0; i < bubbleCount; i++) {
BadgedImageView bv = (BadgedImageView) mBubbleContainer.getChildAt(i);
boolean fullShadow = i < NUM_VISIBLE_WHEN_RESTING;
if (!fullShadow) {
bv.animate().translationZ(0).start();
}
}
}
private void updateZOrder() {
int bubbleCount = getBubbleCount();
for (int i = 0; i < bubbleCount; i++) {
BadgedImageView bv = (BadgedImageView) mBubbleContainer.getChildAt(i);
bv.setZ(i < NUM_VISIBLE_WHEN_RESTING
? (mPositioner.getMaxBubbles() * mBubbleElevation) - i
: 0f);
}
}
private void updateBadges(boolean setBadgeForCollapsedStack) {
int bubbleCount = getBubbleCount();
for (int i = 0; i < bubbleCount; i++) {
BadgedImageView bv = (BadgedImageView) mBubbleContainer.getChildAt(i);
if (mIsExpanded) {
// If we're not displaying vertically, we always show the badge on the left.
boolean onLeft = mPositioner.showBubblesVertically() && !mStackOnLeftOrWillBe;
bv.showDotAndBadge(onLeft);
} else if (setBadgeForCollapsedStack) {
if (i == 0) {
bv.showDotAndBadge(!mStackOnLeftOrWillBe);
} else {
bv.hideDotAndBadge(!mStackOnLeftOrWillBe);
}
}
}
}
/**
* Updates the position of the pointer based on the expanded bubble.
*
* @param forIme whether the position is being updated due to the ime appearing, in this case
* the pointer is animated to the location.
*/
private void updatePointerPosition(boolean forIme) {
if (mExpandedBubble == null || mExpandedBubble.getExpandedView() == null) {
return;
}
int index = getBubbleIndex(mExpandedBubble);
if (index == -1) {
return;
}
PointF position = mPositioner.getExpandedBubbleXY(index, getState());
float bubblePosition = mPositioner.showBubblesVertically()
? position.y
: position.x;
mExpandedBubble.getExpandedView().setPointerPosition(bubblePosition,
mStackOnLeftOrWillBe, forIme /* animate */);
}
/**
* @return the number of bubbles in the stack view.
*/
public int getBubbleCount() {
// Subtract 1 for the overflow button that is always in the bubble container.
return mBubbleContainer.getChildCount() - 1;
}
/**
* Finds the bubble index within the stack.
*
* @param provider the bubble view provider with the bubble to look up.
* @return the index of the bubble view within the bubble stack. The range of the position
* is between 0 and the bubble count minus 1.
*/
int getBubbleIndex(@Nullable BubbleViewProvider provider) {
if (provider == null) {
return 0;
}
return mBubbleContainer.indexOfChild(provider.getIconView());
}
/**
* @return the normalized x-axis position of the bubble stack rounded to 4 decimal places.
*/
public float getNormalizedXPosition() {
return new BigDecimal(getStackPosition().x / mPositioner.getAvailableRect().width())
.setScale(4, RoundingMode.CEILING.HALF_UP)
.floatValue();
}
/**
* @return the normalized y-axis position of the bubble stack rounded to 4 decimal places.
*/
public float getNormalizedYPosition() {
return new BigDecimal(getStackPosition().y / mPositioner.getAvailableRect().height())
.setScale(4, RoundingMode.CEILING.HALF_UP)
.floatValue();
}
/** @return the position of the bubble stack. */
public PointF getStackPosition() {
return mStackAnimationController.getStackPosition();
}
/**
* Logs the bubble UI event.
*
* @param provider the bubble view provider that is being interacted on. Null value indicates
* that the user interaction is not specific to one bubble.
* @param action the user interaction enum.
*/
private void logBubbleEvent(@Nullable BubbleViewProvider provider, int action) {
final String packageName =
(provider != null && provider instanceof Bubble)
? ((Bubble) provider).getPackageName()
: "null";
mBubbleData.logBubbleEvent(provider,
action,
packageName,
getBubbleCount(),
getBubbleIndex(provider),
getNormalizedXPosition(),
getNormalizedYPosition());
}
/** For debugging only */
List<Bubble> getBubblesOnScreen() {
List<Bubble> bubbles = new ArrayList<>();
for (int i = 0; i < getBubbleCount(); i++) {
View child = mBubbleContainer.getChildAt(i);
if (child instanceof BadgedImageView) {
String key = ((BadgedImageView) child).getKey();
Bubble bubble = mBubbleData.getBubbleInStackWithKey(key);
bubbles.add(bubble);
}
}
return bubbles;
}
/** @return the current stack state. */
public StackViewState getState() {
mStackViewState.numberOfBubbles = mBubbleContainer.getChildCount();
mStackViewState.selectedIndex = getBubbleIndex(mExpandedBubble);
mStackViewState.onLeft = mStackOnLeftOrWillBe;
return mStackViewState;
}
/**
* Handles vertical offset changes, e.g. when one handed mode is switched on/off.
*
* @param offset new vertical offset.
*/
void onVerticalOffsetChanged(int offset) {
// adjust dismiss view vertical position, so that it is still visible to the user
mDismissView.setPadding(/* left = */ 0, /* top = */ 0, /* right = */ 0, offset);
}
/**
* Holds some commonly queried information about the stack.
*/
public static class StackViewState {
// Number of bubbles (including the overflow itself) in the stack.
public int numberOfBubbles;
// The selected index if the stack is expanded.
public int selectedIndex;
// Whether the stack is resting on the left or right side of the screen when collapsed.
public boolean onLeft;
}
/**
* Representation of stack position that uses relative properties rather than absolute
* coordinates. This is used to maintain similar stack positions across configuration changes.
*/
public static class RelativeStackPosition {
/** Whether to place the stack at the leftmost allowed position. */
private boolean mOnLeft;
/**
* How far down the vertically allowed region to place the stack. For example, if the stack
* allowed region is between y = 100 and y = 1100 and this is 0.2f, we'll place the stack at
* 100 + (0.2f * 1000) = 300.
*/
private float mVerticalOffsetPercent;
public RelativeStackPosition(boolean onLeft, float verticalOffsetPercent) {
mOnLeft = onLeft;
mVerticalOffsetPercent = clampVerticalOffsetPercent(verticalOffsetPercent);
}
/** Constructs a relative position given a region and a point in that region. */
public RelativeStackPosition(PointF position, RectF region) {
mOnLeft = position.x < region.width() / 2;
mVerticalOffsetPercent =
clampVerticalOffsetPercent((position.y - region.top) / region.height());
}
/** Ensures that the offset percent is between 0f and 1f. */
private float clampVerticalOffsetPercent(float offsetPercent) {
return Math.max(0f, Math.min(1f, offsetPercent));
}
/**
* Given an allowable stack position region, returns the point within that region
* represented by this relative position.
*/
public PointF getAbsolutePositionInRegion(RectF region) {
return new PointF(
mOnLeft ? region.left : region.right,
region.top + mVerticalOffsetPercent * region.height());
}
}
}