Merge "Modify PIP to use the magnetic target from Bubbles." into rvc-dev am: 1ff0d350eb

Change-Id: I0ff7a81d24a9c6dbffcc712c5b5f6c6b17db3bc5
This commit is contained in:
TreeHugger Robot
2020-04-08 00:18:35 +00:00
committed by Automerger Merge Worker
10 changed files with 332 additions and 373 deletions

View File

@@ -17,7 +17,7 @@
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="@dimen/pip_dismiss_gradient_height"
android:layout_height="@dimen/floating_dismiss_gradient_height"
android:layout_gravity="bottom|center_horizontal">
<FrameLayout

View File

@@ -1,35 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2016 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.
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="@dimen/pip_dismiss_gradient_height"
android:alpha="0">
<TextView
android:id="@+id/pip_dismiss_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center_horizontal"
android:text="@string/pip_phone_dismiss_hint"
android:textColor="#FFFFFFFF"
android:textSize="14sp"
android:shadowColor="@android:color/black"
android:shadowDx="-2"
android:shadowDy="2"
android:shadowRadius="0.01" />
</FrameLayout>

View File

@@ -955,7 +955,7 @@
<dimen name="recents_quick_scrub_onboarding_margin_start">8dp</dimen>
<!-- The height of the gradient indicating the dismiss edge when moving a PIP. -->
<dimen name="pip_dismiss_gradient_height">176dp</dimen>
<dimen name="floating_dismiss_gradient_height">176dp</dimen>
<!-- The bottom margin of the PIP drag to dismiss info text shown when moving a PIP. -->
<dimen name="pip_dismiss_text_bottom_margin">24dp</dimen>

View File

@@ -514,7 +514,7 @@ public class BubbleStackView extends FrameLayout {
mDismissTargetContainer = new FrameLayout(context);
mDismissTargetContainer.setLayoutParams(new FrameLayout.LayoutParams(
MATCH_PARENT,
getResources().getDimensionPixelSize(R.dimen.pip_dismiss_gradient_height),
getResources().getDimensionPixelSize(R.dimen.floating_dismiss_gradient_height),
Gravity.BOTTOM));
mDismissTargetContainer.setClipChildren(false);
mDismissTargetContainer.addView(targetView);
@@ -523,7 +523,7 @@ public class BubbleStackView extends FrameLayout {
// Start translated down so the target springs up.
targetView.setTranslationY(
getResources().getDimensionPixelSize(R.dimen.pip_dismiss_gradient_height));
getResources().getDimensionPixelSize(R.dimen.floating_dismiss_gradient_height));
// Save the MagneticTarget instance for the newly set up view - we'll add this to the
// MagnetizedObjects.

View File

@@ -1,188 +0,0 @@
/*
* Copyright (C) 2016 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.systemui.pip.phone;
import android.content.Context;
import android.graphics.PixelFormat;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.VibrationEffect;
import android.os.Vibrator;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.WindowManager;
import android.view.WindowManager.LayoutParams;
import android.widget.FrameLayout;
import com.android.systemui.Interpolators;
import com.android.systemui.R;
import com.android.systemui.shared.system.WindowManagerWrapper;
/**
* Displays the dismiss UI and target for floating objects.
*/
public class PipDismissViewController {
// This delay controls how long to wait before we show the target when the user first moves
// the PIP, to prevent the target from animating if the user just wants to fling the PIP
public static final int SHOW_TARGET_DELAY = 100;
private static final int SHOW_TARGET_DURATION = 350;
private static final int HIDE_TARGET_DURATION = 225;
private Context mContext;
private WindowManager mWindowManager;
private View mDismissView;
// Used for dismissing a bubble -- bubble should be in the target to be considered a dismiss
private View mTargetView;
private int mTargetSlop;
private Point mWindowSize;
private int[] mLoc = new int[2];
private boolean mIntersecting;
private Vibrator mVibe;
public PipDismissViewController(Context context) {
mContext = context;
mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
mVibe = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
}
/**
* Creates the dismiss target for showing via {@link #showDismissTarget()}.
*/
public void createDismissTarget() {
if (mDismissView == null) {
// Determine sizes for the view
final Rect stableInsets = new Rect();
WindowManagerWrapper.getInstance().getStableInsets(stableInsets);
mWindowSize = new Point();
mWindowManager.getDefaultDisplay().getRealSize(mWindowSize);
final int gradientHeight = mContext.getResources().getDimensionPixelSize(
R.dimen.pip_dismiss_gradient_height);
final int bottomMargin = mContext.getResources().getDimensionPixelSize(
R.dimen.pip_dismiss_text_bottom_margin);
mTargetSlop = mContext.getResources().getDimensionPixelSize(
R.dimen.bubble_dismiss_slop);
// Create a new view for the dismiss target
LayoutInflater inflater = LayoutInflater.from(mContext);
mDismissView = inflater.inflate(R.layout.pip_dismiss_view, null);
mDismissView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
mDismissView.forceHasOverlappingRendering(false);
// Set the gradient background
Drawable gradient = mContext.getResources().getDrawable(R.drawable.pip_dismiss_scrim);
gradient.setAlpha((int) (255 * 0.85f));
mDismissView.setBackground(gradient);
// Adjust bottom margins of the text
mTargetView = mDismissView.findViewById(R.id.pip_dismiss_text);
FrameLayout.LayoutParams tlp = (FrameLayout.LayoutParams) mTargetView.getLayoutParams();
tlp.bottomMargin = stableInsets.bottom + bottomMargin;
mTargetView.setLayoutParams(tlp);
// Add the target to the window
LayoutParams lp = new LayoutParams(
LayoutParams.MATCH_PARENT, gradientHeight,
0, mWindowSize.y - gradientHeight,
LayoutParams.TYPE_NAVIGATION_BAR_PANEL,
LayoutParams.FLAG_LAYOUT_IN_SCREEN
| LayoutParams.FLAG_NOT_TOUCHABLE
| LayoutParams.FLAG_NOT_FOCUSABLE,
PixelFormat.TRANSLUCENT);
lp.setTitle("pip-dismiss-overlay");
lp.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
lp.gravity = Gravity.TOP | Gravity.CENTER_HORIZONTAL;
lp.setFitInsetsTypes(0 /* types */);
mWindowManager.addView(mDismissView, lp);
}
mDismissView.animate().cancel();
}
/**
* Updates the dismiss target based on location of the view, only used for bubbles not for PIP.
*
* @return whether the view is within the dismiss target.
*/
public boolean updateTarget(View view) {
if (mDismissView == null) {
return false;
}
if (mDismissView.getAlpha() > 0) {
view.getLocationOnScreen(mLoc);
Rect viewRect = new Rect(mLoc[0], mLoc[1], mLoc[0] + view.getWidth(),
mLoc[1] + view.getHeight());
mTargetView.getLocationOnScreen(mLoc);
Rect targetRect = new Rect(mLoc[0], mLoc[1], mLoc[0] + mTargetView.getWidth(),
mLoc[1] + mTargetView.getHeight());
expandRect(targetRect, mTargetSlop);
boolean intersecting = targetRect.intersect(viewRect);
if (intersecting != mIntersecting) {
// TODO: is this the right effect?
mVibe.vibrate(VibrationEffect.get(intersecting
? VibrationEffect.EFFECT_CLICK
: VibrationEffect.EFFECT_TICK));
}
mIntersecting = intersecting;
return intersecting;
}
return false;
}
/**
* Shows the dismiss target.
*/
public void showDismissTarget() {
mDismissView.animate()
.alpha(1f)
.setInterpolator(Interpolators.LINEAR)
.setStartDelay(SHOW_TARGET_DELAY)
.setDuration(SHOW_TARGET_DURATION)
.start();
}
/**
* Hides and destroys the dismiss target.
*/
public void destroyDismissTarget() {
if (mDismissView != null) {
mDismissView.animate()
.alpha(0f)
.setInterpolator(Interpolators.LINEAR)
.setStartDelay(0)
.setDuration(HIDE_TARGET_DURATION)
.withEndAction(new Runnable() {
@Override
public void run() {
mWindowManager.removeViewImmediate(mDismissView);
mDismissView = null;
}
})
.start();
}
}
private void expandRect(Rect outRect, int expandAmount) {
outRect.left = Math.max(0, outRect.left - expandAmount);
outRect.top = Math.max(0, outRect.top - expandAmount);
outRect.right = Math.min(mWindowSize.x, outRect.right + expandAmount);
outRect.bottom = Math.min(mWindowSize.y, outRect.bottom + expandAmount);
}
}

View File

@@ -24,8 +24,6 @@ import android.annotation.Nullable;
import android.app.ActivityManager.StackInfo;
import android.app.IActivityTaskManager;
import android.content.Context;
import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.Rect;
import android.os.Debug;
import android.os.RemoteException;
@@ -40,6 +38,7 @@ import com.android.systemui.statusbar.FlingAnimationUtils;
import com.android.systemui.util.FloatingContentCoordinator;
import com.android.systemui.util.animation.FloatProperties;
import com.android.systemui.util.animation.PhysicsAnimator;
import com.android.systemui.util.magnetictarget.MagnetizedObject;
import java.io.PrintWriter;
import java.util.function.Consumer;
@@ -61,9 +60,6 @@ public class PipMotionHelper implements PipAppOpsListener.Callback,
/** Friction to use for PIP when it moves via physics fling animations. */
private static final float DEFAULT_FRICTION = 2f;
// The fraction of the stack height that the user has to drag offscreen to dismiss the PiP
private static final float DISMISS_OFFSCREEN_FRACTION = 0.3f;
private final Context mContext;
private final IActivityTaskManager mActivityTaskManager;
private final PipTaskOrganizer mPipTaskOrganizer;
@@ -103,7 +99,7 @@ public class PipMotionHelper implements PipAppOpsListener.Callback,
/**
* Update listener that resizes the PIP to {@link #mAnimatedBounds}.
*/
private final PhysicsAnimator.UpdateListener<Rect> mResizePipUpdateListener =
final PhysicsAnimator.UpdateListener<Rect> mResizePipUpdateListener =
(target, values) -> resizePipUnchecked(mAnimatedBounds);
/** FlingConfig instances provided to PhysicsAnimator for fling gestures. */
@@ -122,6 +118,13 @@ public class PipMotionHelper implements PipAppOpsListener.Callback,
private final Consumer<Rect> mUpdateBoundsCallback = mBounds::set;
/**
* Whether we're springing to the touch event location (vs. moving it to that position
* instantly). We spring-to-touch after PIP is dragged out of the magnetic target, since it was
* 'stuck' in the target and needs to catch up to the touch location.
*/
private boolean mSpringingToTouch = false;
public PipMotionHelper(Context context, IActivityTaskManager activityTaskManager,
PipTaskOrganizer pipTaskOrganizer, PipMenuActivityController menuController,
PipSnapAlgorithm snapAlgorithm, FlingAnimationUtils flingAnimationUtils,
@@ -211,9 +214,35 @@ public class PipMotionHelper implements PipAppOpsListener.Callback,
mFloatingContentCoordinator.onContentMoved(this);
}
cancelAnimations();
resizePipUnchecked(toBounds);
mBounds.set(toBounds);
if (!mSpringingToTouch) {
// If we are moving PIP directly to the touch event locations, cancel any animations and
// move PIP to the given bounds.
cancelAnimations();
resizePipUnchecked(toBounds);
mBounds.set(toBounds);
} else {
// If PIP is 'catching up' after being stuck in the dismiss target, update the animation
// to spring towards the new touch location.
mAnimatedBoundsPhysicsAnimator
.spring(FloatProperties.RECT_X, toBounds.left, mSpringConfig)
.spring(FloatProperties.RECT_Y, toBounds.top, mSpringConfig)
.withEndActions(() -> mSpringingToTouch = false);
startBoundsAnimator(toBounds.left /* toX */, toBounds.top /* toY */);
}
}
/** Set whether we're springing-to-touch to catch up after being stuck in the dismiss target. */
void setSpringingToTouch(boolean springingToTouch) {
if (springingToTouch) {
mAnimatedBounds.set(mBounds);
}
mSpringingToTouch = springingToTouch;
}
void prepareForAnimation() {
mAnimatedBounds.set(mBounds);
}
/**
@@ -277,25 +306,12 @@ public class PipMotionHelper implements PipAppOpsListener.Callback,
return mBounds;
}
/**
* @return whether the PiP at the current bounds should be dismissed.
*/
boolean shouldDismissPip() {
Point displaySize = new Point();
mContext.getDisplay().getRealSize(displaySize);
final int y = displaySize.y - mStableInsets.bottom;
if (mBounds.bottom > y) {
float offscreenFraction = (float) (mBounds.bottom - y) / mBounds.height();
return offscreenFraction >= DISMISS_OFFSCREEN_FRACTION;
}
return false;
}
/**
* Flings the PiP to the closest snap target.
*/
void flingToSnapTarget(
float velocityX, float velocityY, Runnable updateAction, @Nullable Runnable endAction) {
float velocityX, float velocityY,
@Nullable Runnable updateAction, @Nullable Runnable endAction) {
mAnimatedBounds.set(mBounds);
mAnimatedBoundsPhysicsAnimator
.flingThenSpring(
@@ -303,9 +319,13 @@ public class PipMotionHelper implements PipAppOpsListener.Callback,
true /* flingMustReachMinOrMax */)
.flingThenSpring(
FloatProperties.RECT_Y, velocityY, mFlingConfigY, mSpringConfig)
.addUpdateListener((target, values) -> updateAction.run())
.withEndActions(endAction);
if (updateAction != null) {
mAnimatedBoundsPhysicsAnimator.addUpdateListener(
(target, values) -> updateAction.run());
}
final float xEndValue = velocityX < 0 ? mMovementBounds.left : mMovementBounds.right;
final float estimatedFlingYEndValue =
PhysicsAnimator.estimateFlingEndValue(mBounds.top, velocityY, mFlingConfigY);
@@ -338,16 +358,14 @@ public class PipMotionHelper implements PipAppOpsListener.Callback,
* Animates the dismissal of the PiP off the edge of the screen.
*/
void animateDismiss(float velocityX, float velocityY, @Nullable Runnable updateAction) {
final float velocity = PointF.length(velocityX, velocityY);
final boolean isFling = velocity > mFlingAnimationUtils.getMinVelocityPxPerSecond();
final Point dismissEndPoint = getDismissEndPoint(mBounds, velocityX, velocityY, isFling);
mAnimatedBounds.set(mBounds);
// Animate to the dismiss end point, and then dismiss PIP.
// Animate off the bottom of the screen, then dismiss PIP.
mAnimatedBoundsPhysicsAnimator
.spring(FloatProperties.RECT_X, dismissEndPoint.x, velocityX, mSpringConfig)
.spring(FloatProperties.RECT_Y, dismissEndPoint.y, velocityY, mSpringConfig)
.spring(FloatProperties.RECT_Y,
mBounds.bottom + mBounds.height(),
velocityY,
mSpringConfig)
.withEndActions(this::dismissPip);
// If we were provided with an update action, run it whenever there's an update.
@@ -356,7 +374,7 @@ public class PipMotionHelper implements PipAppOpsListener.Callback,
(target, values) -> updateAction.run());
}
startBoundsAnimator(dismissEndPoint.x /* toX */, dismissEndPoint.y /* toY */);
startBoundsAnimator(mBounds.left /* toX */, mBounds.bottom + mBounds.height() /* toY */);
}
/**
@@ -408,6 +426,7 @@ public class PipMotionHelper implements PipAppOpsListener.Callback,
private void cancelAnimations() {
mAnimatedBoundsPhysicsAnimator.cancel();
mAnimatingToBounds.setEmpty();
mSpringingToTouch = false;
}
/** Set new fling configs whose min/max values respect the given movement bounds. */
@@ -426,7 +445,9 @@ public class PipMotionHelper implements PipAppOpsListener.Callback,
* the 'real' bounds to equal the final animated bounds.
*/
private void startBoundsAnimator(float toX, float toY) {
cancelAnimations();
if (!mSpringingToTouch) {
cancelAnimations();
}
// Set animatingToBounds directly to avoid allocating a new Rect, but then call
// setAnimatingToBounds to run the normal logic for changing animatingToBounds.
@@ -484,47 +505,29 @@ public class PipMotionHelper implements PipAppOpsListener.Callback,
}
/**
* @return the coordinates the PIP should animate to based on the direction of velocity when
* dismissing.
* Returns a MagnetizedObject wrapper for PIP's animated bounds. This is provided to the
* magnetic dismiss target so it can calculate PIP's size and position.
*/
private Point getDismissEndPoint(Rect pipBounds, float velX, float velY, boolean isFling) {
Point displaySize = new Point();
mContext.getDisplay().getRealSize(displaySize);
final float bottomBound = displaySize.y + pipBounds.height() * .1f;
if (isFling && velX != 0 && velY != 0) {
// Line is defined by: y = mx + b, m = slope, b = y-intercept
// Find the slope
final float slope = velY / velX;
// Sub in slope and PiP position to solve for y-intercept: b = y - mx
final float yIntercept = pipBounds.top - slope * pipBounds.left;
// Now find the point on this line when y = bottom bound: x = (y - b) / m
final float x = (bottomBound - yIntercept) / slope;
return new Point((int) x, (int) bottomBound);
} else {
// If it wasn't a fling the velocity on 'up' is not reliable for direction of movement,
// just animate downwards.
return new Point(pipBounds.left, (int) bottomBound);
}
}
MagnetizedObject<Rect> getMagnetizedPip() {
return new MagnetizedObject<Rect>(
mContext, mAnimatedBounds, FloatProperties.RECT_X, FloatProperties.RECT_Y) {
@Override
public float getWidth(@NonNull Rect animatedPipBounds) {
return animatedPipBounds.width();
}
/**
* @return whether the gesture it towards the dismiss area based on the velocity when
* dismissing.
*/
public boolean isGestureToDismissArea(Rect pipBounds, float velX, float velY,
boolean isFling) {
Point endpoint = getDismissEndPoint(pipBounds, velX, velY, isFling);
// Center the point
endpoint.x += pipBounds.width() / 2;
endpoint.y += pipBounds.height() / 2;
@Override
public float getHeight(@NonNull Rect animatedPipBounds) {
return animatedPipBounds.height();
}
// The dismiss area is the middle third of the screen, half the PIP's height from the bottom
Point size = new Point();
mContext.getDisplay().getRealSize(size);
final int left = size.x / 3;
Rect dismissArea = new Rect(left, size.y - (pipBounds.height() / 2), left * 2,
size.y + pipBounds.height());
return dismissArea.contains(endpoint.x, endpoint.y);
@Override
public void getLocationOnScreen(
@NonNull Rect animatedPipBounds, @NonNull int[] loc) {
loc[0] = animatedPipBounds.left;
loc[1] = animatedPipBounds.top;
}
};
}
public void dump(PrintWriter pw, String prefix) {

View File

@@ -20,11 +20,13 @@ import static com.android.systemui.pip.phone.PipMenuActivityController.MENU_STAT
import static com.android.systemui.pip.phone.PipMenuActivityController.MENU_STATE_FULL;
import static com.android.systemui.pip.phone.PipMenuActivityController.MENU_STATE_NONE;
import android.annotation.SuppressLint;
import android.app.IActivityManager;
import android.app.IActivityTaskManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.PixelFormat;
import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.Rect;
@@ -33,14 +35,23 @@ import android.os.RemoteException;
import android.util.Log;
import android.util.Pair;
import android.util.Size;
import android.view.Gravity;
import android.view.IPinnedStackController;
import android.view.InputEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityWindowInfo;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.dynamicanimation.animation.DynamicAnimation;
import androidx.dynamicanimation.animation.SpringForce;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.os.logging.MetricsLoggerWrapper;
@@ -51,7 +62,10 @@ import com.android.systemui.pip.PipTaskOrganizer;
import com.android.systemui.shared.system.InputConsumerController;
import com.android.systemui.statusbar.FlingAnimationUtils;
import com.android.systemui.util.DeviceConfigProxy;
import com.android.systemui.util.DismissCircleView;
import com.android.systemui.util.FloatingContentCoordinator;
import com.android.systemui.util.animation.PhysicsAnimator;
import com.android.systemui.util.magnetictarget.MagnetizedObject;
import java.io.PrintWriter;
@@ -62,9 +76,6 @@ import java.io.PrintWriter;
public class PipTouchHandler {
private static final String TAG = "PipTouchHandler";
// Allow the PIP to be flung from anywhere on the screen to the bottom to be dismissed.
private static final boolean ENABLE_FLING_DISMISS = false;
private static final int SHOW_DISMISS_AFFORDANCE_DELAY = 225;
private static final int BOTTOM_OFFSET_BUFFER_DP = 1;
@@ -73,17 +84,45 @@ public class PipTouchHandler {
// Allow PIP to resize to a slightly bigger state upon touch
private final boolean mEnableResize;
private final Context mContext;
private final WindowManager mWindowManager;
private final IActivityManager mActivityManager;
private final PipBoundsHandler mPipBoundsHandler;
private PipResizeGestureHandler mPipResizeGestureHandler;
private IPinnedStackController mPinnedStackController;
private final PipMenuActivityController mMenuController;
private final PipDismissViewController mDismissViewController;
private final PipSnapAlgorithm mSnapAlgorithm;
private final AccessibilityManager mAccessibilityManager;
private boolean mShowPipMenuOnAnimationEnd = false;
/**
* MagnetizedObject wrapper for PIP. This allows the magnetic target library to locate and move
* PIP.
*/
private MagnetizedObject<Rect> mMagnetizedPip;
/**
* Container for the dismiss circle, so that it can be animated within the container via
* translation rather than within the WindowManager via slow layout animations.
*/
private ViewGroup mTargetViewContainer;
/** Circle view used to render the dismiss target. */
private DismissCircleView mTargetView;
/**
* MagneticTarget instance wrapping the target view and allowing us to set its magnetic radius.
*/
private MagnetizedObject.MagneticTarget mMagneticTarget;
/** PhysicsAnimator instance for animating the dismiss target in/out. */
private PhysicsAnimator<View> mMagneticTargetAnimator;
/** Default configuration to use for springing the dismiss target in/out. */
private final PhysicsAnimator.SpringConfig mTargetSpringConfig =
new PhysicsAnimator.SpringConfig(
SpringForce.STIFFNESS_MEDIUM, SpringForce.DAMPING_RATIO_NO_BOUNCY);
// The current movement bounds
private Rect mMovementBounds = new Rect();
// The current resized bounds, changed by user resize.
@@ -104,21 +143,20 @@ public class PipTouchHandler {
private int mDeferResizeToNormalBoundsUntilRotation = -1;
private int mDisplayRotation;
/**
* Runnable that can be posted delayed to show the target. This needs to be saved as a member
* variable so we can pass it to removeCallbacks.
*/
private Runnable mShowTargetAction = this::showDismissTargetMaybe;
private Handler mHandler = new Handler();
private Runnable mShowDismissAffordance = new Runnable() {
@Override
public void run() {
if (mEnableDismissDragToEdge) {
mDismissViewController.showDismissTarget();
}
}
};
// Behaviour states
private int mMenuState = MENU_STATE_NONE;
private boolean mIsImeShowing;
private int mImeHeight;
private int mImeOffset;
private int mDismissAreaHeight;
private boolean mIsShelfShowing;
private int mShelfHeight;
private int mMovementBoundsExtraOffsets;
@@ -168,6 +206,7 @@ public class PipTouchHandler {
}
}
@SuppressLint("InflateParams")
public PipTouchHandler(Context context, IActivityManager activityManager,
IActivityTaskManager activityTaskManager, PipMenuActivityController menuController,
InputConsumerController inputConsumerController,
@@ -180,9 +219,9 @@ public class PipTouchHandler {
mContext = context;
mActivityManager = activityManager;
mAccessibilityManager = context.getSystemService(AccessibilityManager.class);
mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
mMenuController = menuController;
mMenuController.addListener(new PipMenuListener());
mDismissViewController = new PipDismissViewController(context);
mSnapAlgorithm = pipSnapAlgorithm;
mFlingAnimationUtils = new FlingAnimationUtils(context.getResources().getDisplayMetrics(),
2.5f);
@@ -200,6 +239,7 @@ public class PipTouchHandler {
mExpandedShortestEdgeSize = res.getDimensionPixelSize(
R.dimen.pip_expanded_shortest_edge_size);
mImeOffset = res.getDimensionPixelSize(R.dimen.pip_ime_offset);
mDismissAreaHeight = res.getDimensionPixelSize(R.dimen.floating_dismiss_gradient_height);
mEnableDismissDragToEdge = res.getBoolean(R.bool.config_pipEnableDismissDragToEdge);
mEnableResize = res.getBoolean(R.bool.config_pipEnableResizeForMenu);
@@ -212,6 +252,56 @@ public class PipTouchHandler {
mFloatingContentCoordinator = floatingContentCoordinator;
mConnection = new PipAccessibilityInteractionConnection(mMotionHelper,
this::onAccessibilityShowMenu, mHandler);
final int targetSize = res.getDimensionPixelSize(R.dimen.dismiss_circle_size);
mTargetView = new DismissCircleView(context);
final FrameLayout.LayoutParams newParams =
new FrameLayout.LayoutParams(targetSize, targetSize);
newParams.gravity = Gravity.CENTER;
mTargetView.setLayoutParams(newParams);
mTargetViewContainer = new FrameLayout(context);
mTargetViewContainer.setClipChildren(false);
mTargetViewContainer.addView(mTargetView);
mMagnetizedPip = mMotionHelper.getMagnetizedPip();
mMagneticTarget = mMagnetizedPip.addTarget(mTargetView, 0);
mMagnetizedPip.setPhysicsAnimatorUpdateListener(mMotionHelper.mResizePipUpdateListener);
mMagnetizedPip.setMagnetListener(new MagnetizedObject.MagnetListener() {
@Override
public void onStuckToTarget(@NonNull MagnetizedObject.MagneticTarget target) {
mMotionHelper.prepareForAnimation();
// Show the dismiss target, in case the initial touch event occurred within the
// magnetic field radius.
showDismissTargetMaybe();
}
@Override
public void onUnstuckFromTarget(@NonNull MagnetizedObject.MagneticTarget target,
float velX, float velY, boolean wasFlungOut) {
if (wasFlungOut) {
mMotionHelper.flingToSnapTarget(velX, velY, null, null);
hideDismissTarget();
} else {
mMotionHelper.setSpringingToTouch(true);
}
}
@Override
public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target) {
mHandler.post(() -> {
mMotionHelper.animateDismiss(0, 0, null);
hideDismissTarget();
});
MetricsLoggerWrapper.logPictureInPictureDismissByDrag(mContext,
PipUtils.getTopPipActivity(mContext, mActivityManager));
}
});
mMagneticTargetAnimator = PhysicsAnimator.getInstance(mTargetView);
}
public void setTouchGesture(PipTouchGesture gesture) {
@@ -231,7 +321,8 @@ public class PipTouchHandler {
}
public void onActivityPinned() {
cleanUpDismissTarget();
createDismissTargetMaybe();
mShowPipMenuOnAnimationEnd = true;
mPipResizeGestureHandler.onActivityPinned();
mFloatingContentCoordinator.onContentAdded(mMotionHelper);
@@ -264,6 +355,10 @@ public class PipTouchHandler {
public void onConfigurationChanged() {
mMotionHelper.onConfigurationChanged();
mMotionHelper.synchronizePinnedStackBounds();
// Recreate the dismiss target for the new orientation.
cleanUpDismissTarget();
createDismissTargetMaybe();
}
public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) {
@@ -351,6 +446,74 @@ public class PipTouchHandler {
}
}
/** Adds the magnetic target view to the WindowManager so it's ready to be animated in. */
private void createDismissTargetMaybe() {
if (!mTargetViewContainer.isAttachedToWindow()) {
mHandler.removeCallbacks(mShowTargetAction);
mMagneticTargetAnimator.cancel();
final Point windowSize = new Point();
mWindowManager.getDefaultDisplay().getRealSize(windowSize);
WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
WindowManager.LayoutParams.MATCH_PARENT,
mDismissAreaHeight,
0, windowSize.y - mDismissAreaHeight,
WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL,
WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
| WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
PixelFormat.TRANSLUCENT);
lp.setTitle("pip-dismiss-overlay");
lp.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
lp.setFitInsetsTypes(0 /* types */);
mTargetViewContainer.setVisibility(View.INVISIBLE);
mWindowManager.addView(mTargetViewContainer, lp);
}
}
/** Makes the dismiss target visible and animates it in, if it isn't already visible. */
private void showDismissTargetMaybe() {
createDismissTargetMaybe();
if (mTargetViewContainer.getVisibility() != View.VISIBLE) {
mTargetView.setTranslationY(mTargetViewContainer.getHeight());
mTargetViewContainer.setVisibility(View.VISIBLE);
// Set the magnetic field radius to half of PIP's width.
mMagneticTarget.setMagneticFieldRadiusPx(mMotionHelper.getBounds().width());
// Cancel in case we were in the middle of animating it out.
mMagneticTargetAnimator.cancel();
mMagneticTargetAnimator
.spring(DynamicAnimation.TRANSLATION_Y, 0f, mTargetSpringConfig)
.start();
}
}
/** Animates the magnetic dismiss target out and then sets it to GONE. */
private void hideDismissTarget() {
mHandler.removeCallbacks(mShowTargetAction);
mMagneticTargetAnimator
.spring(DynamicAnimation.TRANSLATION_Y,
mTargetViewContainer.getHeight(),
mTargetSpringConfig)
.withEndActions(() -> mTargetViewContainer.setVisibility(View.GONE))
.start();
}
/**
* Removes the dismiss target and cancels any pending callbacks to show it.
*/
private void cleanUpDismissTarget() {
mHandler.removeCallbacks(mShowTargetAction);
if (mTargetViewContainer.isAttachedToWindow()) {
mWindowManager.removeView(mTargetViewContainer);
}
}
private void onRegistrationChanged(boolean isRegistered) {
mAccessibilityManager.setPictureInPictureActionReplacingConnection(isRegistered
? mConnection : null);
@@ -375,8 +538,24 @@ public class PipTouchHandler {
if (mPinnedStackController == null) {
return true;
}
MotionEvent ev = (MotionEvent) inputEvent;
if (mMagnetizedPip.maybeConsumeMotionEvent(ev)) {
// If the first touch event occurs within the magnetic field, pass the ACTION_DOWN event
// to the touch state. Touch state needs a DOWN event in order to later process MOVE
// events it'll receive if the object is dragged out of the magnetic field.
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
mTouchState.onTouchEvent(ev);
}
// Continue tracking velocity when the object is in the magnetic field, since we want to
// respect touch input velocity if the object is dragged out and then flung.
mTouchState.addMovementToVelocityTracker(ev);
return true;
}
// Update the touch state
mTouchState.onTouchEvent(ev);
@@ -600,17 +779,13 @@ public class PipTouchHandler {
mDelta.set(0f, 0f);
mStartPosition.set(bounds.left, bounds.top);
mMovementWithinDismiss = touchState.getDownTouchPosition().y >= mMovementBounds.bottom;
mMotionHelper.setSpringingToTouch(false);
// If the menu is still visible then just poke the menu
// so that it will timeout after the user stops touching it
if (mMenuState != MENU_STATE_NONE) {
mMenuController.pokeMenu();
}
if (mEnableDismissDragToEdge) {
mDismissViewController.createDismissTarget();
mHandler.postDelayed(mShowDismissAffordance, SHOW_DISMISS_AFFORDANCE_DELAY);
}
}
@Override
@@ -623,8 +798,10 @@ public class PipTouchHandler {
mSavedSnapFraction = -1f;
if (mEnableDismissDragToEdge) {
mHandler.removeCallbacks(mShowDismissAffordance);
mDismissViewController.showDismissTarget();
if (mTargetViewContainer.getVisibility() != View.VISIBLE) {
mHandler.removeCallbacks(mShowTargetAction);
showDismissTargetMaybe();
}
}
}
@@ -644,10 +821,6 @@ public class PipTouchHandler {
mTmpBounds.offsetTo((int) left, (int) top);
mMotionHelper.movePip(mTmpBounds, true /* isDragging */);
if (mEnableDismissDragToEdge) {
updateDismissFraction();
}
final PointF curPos = touchState.getLastTouchPosition();
if (mMovementWithinDismiss) {
// Track if movement remains near the bottom edge to identify swipe to dismiss
@@ -661,9 +834,7 @@ public class PipTouchHandler {
@Override
public boolean onUp(PipTouchState touchState) {
if (mEnableDismissDragToEdge) {
// Clean up the dismiss target regardless of the touch state in case the touch
// enabled state changes while the user is interacting
cleanUpDismissTarget();
hideDismissTarget();
}
if (!touchState.isUserInteracting()) {
@@ -671,26 +842,8 @@ public class PipTouchHandler {
}
final PointF vel = touchState.getVelocity();
final boolean isHorizontal = Math.abs(vel.x) > Math.abs(vel.y);
final float velocity = PointF.length(vel.x, vel.y);
final boolean isFling = velocity > mFlingAnimationUtils.getMinVelocityPxPerSecond();
final boolean isUpWithinDimiss = ENABLE_FLING_DISMISS
&& touchState.getLastTouchPosition().y >= mMovementBounds.bottom
&& mMotionHelper.isGestureToDismissArea(mMotionHelper.getBounds(), vel.x,
vel.y, isFling);
final boolean isFlingToBot = isFling && vel.y > 0 && !isHorizontal
&& (mMovementWithinDismiss || isUpWithinDimiss);
if (mEnableDismissDragToEdge) {
// Check if the user dragged or flung the PiP offscreen to dismiss it
if (mMotionHelper.shouldDismissPip() || isFlingToBot) {
MetricsLoggerWrapper.logPictureInPictureDismissByDrag(mContext,
PipUtils.getTopPipActivity(mContext, mActivityManager));
mMotionHelper.animateDismiss(
vel.x, vel.y,
PipTouchHandler.this::updateDismissFraction /* updateAction */);
return true;
}
}
if (touchState.isDragging()) {
Runnable endAction = null;
@@ -748,14 +901,6 @@ public class PipTouchHandler {
isMenuExpanded && willResizeMenu() ? mExpandedShortestEdgeSize : 0);
}
/**
* Removes the dismiss target and cancels any pending callbacks to show it.
*/
private void cleanUpDismissTarget() {
mHandler.removeCallbacks(mShowDismissAffordance);
mDismissViewController.destroyDismissTarget();
}
/**
* @return whether the menu will resize as a part of showing the full menu.
*/

View File

@@ -92,7 +92,7 @@ public class PipTouchState {
// Initialize the velocity tracker
initOrResetVelocityTracker();
addMovement(ev);
addMovementToVelocityTracker(ev);
mActivePointerId = ev.getPointerId(0);
if (DEBUG) {
@@ -120,7 +120,7 @@ public class PipTouchState {
}
// Update the velocity tracker
addMovement(ev);
addMovementToVelocityTracker(ev);
int pointerIndex = ev.findPointerIndex(mActivePointerId);
if (pointerIndex == -1) {
Log.e(TAG, "Invalid active pointer id on MOVE: " + mActivePointerId);
@@ -151,7 +151,7 @@ public class PipTouchState {
}
// Update the velocity tracker
addMovement(ev);
addMovementToVelocityTracker(ev);
int pointerIndex = ev.getActionIndex();
int pointerId = ev.getPointerId(pointerIndex);
@@ -174,7 +174,7 @@ public class PipTouchState {
}
// Update the velocity tracker
addMovement(ev);
addMovementToVelocityTracker(ev);
mVelocityTracker.computeCurrentVelocity(1000,
mViewConfig.getScaledMaximumFlingVelocity());
mVelocity.set(mVelocityTracker.getXVelocity(), mVelocityTracker.getYVelocity());
@@ -318,6 +318,20 @@ public class PipTouchState {
return -1;
}
void addMovementToVelocityTracker(MotionEvent event) {
if (mVelocityTracker == null) {
return;
}
// Add movement to velocity tracker using raw screen X and Y coordinates instead
// of window coordinates because the window frame may be moving at the same time.
float deltaX = event.getRawX() - event.getX();
float deltaY = event.getRawY() - event.getY();
event.offsetLocation(deltaX, deltaY);
mVelocityTracker.addMovement(event);
event.offsetLocation(-deltaX, -deltaY);
}
private void initOrResetVelocityTracker() {
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
@@ -333,16 +347,6 @@ public class PipTouchState {
}
}
private void addMovement(MotionEvent event) {
// Add movement to velocity tracker using raw screen X and Y coordinates instead
// of window coordinates because the window frame may be moving at the same time.
float deltaX = event.getRawX() - event.getX();
float deltaY = event.getRawY() - event.getY();
event.offsetLocation(deltaX, deltaY);
mVelocityTracker.addMovement(event);
event.offsetLocation(-deltaX, -deltaY);
}
public void dump(PrintWriter pw, String prefix) {
final String innerPrefix = prefix + " ";
pw.println(prefix + TAG);

View File

@@ -159,6 +159,18 @@ abstract class MagnetizedObject<T : Any>(
*/
lateinit var magnetListener: MagnetizedObject.MagnetListener
/**
* Optional update listener to provide to the PhysicsAnimator that is used to spring the object
* into the target.
*/
var physicsAnimatorUpdateListener: PhysicsAnimator.UpdateListener<T>? = null
/**
* Optional end listener to provide to the PhysicsAnimator that is used to spring the object
* into the target.
*/
var physicsAnimatorEndListener: PhysicsAnimator.EndListener<T>? = null
/**
* Sets whether forcefully flinging the object vertically towards a target causes it to be
* attracted to the target and then released immediately, despite never being dragged within the
@@ -479,6 +491,14 @@ abstract class MagnetizedObject<T : Any>(
.spring(yProperty, yProperty.getValue(underlyingObject) + yDiff, velY,
springConfig)
if (physicsAnimatorUpdateListener != null) {
animator.addUpdateListener(physicsAnimatorUpdateListener!!)
}
if (physicsAnimatorEndListener != null) {
animator.addEndListener(physicsAnimatorEndListener!!)
}
if (after != null) {
animator.withEndActions(after)
}
@@ -560,13 +580,15 @@ abstract class MagnetizedObject<T : Any>(
private val tempLoc = IntArray(2)
fun updateLocationOnScreen() {
targetView.getLocationOnScreen(tempLoc)
targetView.post {
targetView.getLocationOnScreen(tempLoc)
// Add half of the target size to get the center, and subtract translation since the
// target could be animating in while we're doing this calculation.
centerOnScreen.set(
tempLoc[0] + targetView.width / 2f - targetView.translationX,
tempLoc[1] + targetView.height / 2f - targetView.translationY)
// Add half of the target size to get the center, and subtract translation since the
// target could be animating in while we're doing this calculation.
centerOnScreen.set(
tempLoc[0] + targetView.width / 2f - targetView.translationX,
tempLoc[1] + targetView.height / 2f - targetView.translationY)
}
}
}

View File

@@ -106,6 +106,10 @@ class MagnetizedObjectTest : SysuiTestCase() {
location[1] = targetCenterY - targetSize / 2 // y = 800
}
}.`when`(targetView).getLocationOnScreen(ArgumentMatchers.any())
doAnswer { invocation ->
(invocation.arguments[0] as Runnable).run()
true
}.`when`(targetView).post(ArgumentMatchers.any())
`when`(targetView.context).thenReturn(context)
magneticTarget = MagnetizedObject.MagneticTarget(targetView, magneticFieldRadius)
@@ -407,6 +411,10 @@ class MagnetizedObjectTest : SysuiTestCase() {
`when`(secondTargetView.context).thenReturn(context)
`when`(secondTargetView.width).thenReturn(targetSize) // width = 200
`when`(secondTargetView.height).thenReturn(targetSize) // height = 200
doAnswer { invocation ->
(invocation.arguments[0] as Runnable).run()
true
}.`when`(secondTargetView).post(ArgumentMatchers.any())
doAnswer { invocation ->
(invocation.arguments[0] as IntArray).also { location ->
// Return the top left of the target.