Merge "Scale down PIP when it's dragged into the dismiss target." into rvc-dev am: 7a1cd3f757 am: 22c5e25a5b

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/11886098

Change-Id: I2ece9747075a3c6e5a0901664c37807c0ef91106
This commit is contained in:
Josh Tsuji
2020-06-18 14:43:12 +00:00
committed by Automerger Merge Worker
5 changed files with 227 additions and 89 deletions

View File

@@ -708,6 +708,12 @@ public class PipTaskOrganizer extends TaskOrganizer implements
Log.w(TAG, "Abort animation, invalid leash"); Log.w(TAG, "Abort animation, invalid leash");
return; return;
} }
if (startBounds.isEmpty() || destinationBounds.isEmpty()) {
Log.w(TAG, "Attempted to user resize PIP to or from empty bounds, aborting.");
return;
}
final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction(); final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction();
mSurfaceTransactionHelper.scale(tx, mLeash, startBounds, destinationBounds); mSurfaceTransactionHelper.scale(tx, mLeash, startBounds, destinationBounds);
tx.apply(); tx.apply();

View File

@@ -20,6 +20,7 @@ import android.annotation.NonNull;
import android.annotation.Nullable; import android.annotation.Nullable;
import android.content.ComponentName; import android.content.ComponentName;
import android.content.Context; import android.content.Context;
import android.graphics.PointF;
import android.graphics.Rect; import android.graphics.Rect;
import android.os.Debug; import android.os.Debug;
import android.util.Log; import android.util.Log;
@@ -38,6 +39,9 @@ import com.android.systemui.util.magnetictarget.MagnetizedObject;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.util.function.Consumer; import java.util.function.Consumer;
import kotlin.Unit;
import kotlin.jvm.functions.Function0;
/** /**
* A helper to animate and manipulate the PiP. * A helper to animate and manipulate the PiP.
*/ */
@@ -74,9 +78,15 @@ public class PipMotionHelper implements PipAppOpsListener.Callback,
new SfVsyncFrameCallbackProvider(); new SfVsyncFrameCallbackProvider();
/** /**
* Bounds that are animated using the physics animator. * Temporary bounds used when PIP is being dragged or animated. These bounds are applied to PIP
* using {@link PipTaskOrganizer#scheduleUserResizePip}, so that we can animate shrinking into
* and expanding out of the magnetic dismiss target.
*
* Once PIP is done being dragged or animated, we set {@link #mBounds} equal to these temporary
* bounds, and call {@link PipTaskOrganizer#scheduleFinishResizePip} to 'officially' move PIP to
* its new bounds.
*/ */
private final Rect mAnimatedBounds = new Rect(); private final Rect mTemporaryBounds = new Rect();
/** The destination bounds to which PIP is animating. */ /** The destination bounds to which PIP is animating. */
private final Rect mAnimatingToBounds = new Rect(); private final Rect mAnimatingToBounds = new Rect();
@@ -85,20 +95,20 @@ public class PipMotionHelper implements PipAppOpsListener.Callback,
private FloatingContentCoordinator mFloatingContentCoordinator; private FloatingContentCoordinator mFloatingContentCoordinator;
/** Callback that re-sizes PIP to the animated bounds. */ /** Callback that re-sizes PIP to the animated bounds. */
private final Choreographer.FrameCallback mResizePipVsyncCallback = private final Choreographer.FrameCallback mResizePipVsyncCallback;
l -> resizePipUnchecked(mAnimatedBounds);
/** /**
* PhysicsAnimator instance for animating {@link #mAnimatedBounds} using physics animations. * PhysicsAnimator instance for animating {@link #mTemporaryBounds} using physics animations.
*/ */
private PhysicsAnimator<Rect> mAnimatedBoundsPhysicsAnimator = PhysicsAnimator.getInstance( private PhysicsAnimator<Rect> mTemporaryBoundsPhysicsAnimator = PhysicsAnimator.getInstance(
mAnimatedBounds); mTemporaryBounds);
private MagnetizedObject<Rect> mMagnetizedPip;
/** /**
* Update listener that resizes the PIP to {@link #mAnimatedBounds}. * Update listener that resizes the PIP to {@link #mTemporaryBounds}.
*/ */
final PhysicsAnimator.UpdateListener<Rect> mResizePipUpdateListener = private final PhysicsAnimator.UpdateListener<Rect> mResizePipUpdateListener;
(target, values) -> mSfVsyncFrameProvider.postFrameCallback(mResizePipVsyncCallback);
/** FlingConfig instances provided to PhysicsAnimator for fling gestures. */ /** FlingConfig instances provided to PhysicsAnimator for fling gestures. */
private PhysicsAnimator.FlingConfig mFlingConfigX; private PhysicsAnimator.FlingConfig mFlingConfigX;
@@ -123,6 +133,12 @@ public class PipMotionHelper implements PipAppOpsListener.Callback,
*/ */
private boolean mSpringingToTouch = false; private boolean mSpringingToTouch = false;
/**
* Whether PIP was released in the dismiss target, and will be animated out and dismissed
* shortly.
*/
private boolean mDismissalPending = false;
/** /**
* Gets set in {@link #animateToExpandedState(Rect, Rect, Rect, Runnable)}, this callback is * Gets set in {@link #animateToExpandedState(Rect, Rect, Rect, Runnable)}, this callback is
* used to show menu activity when the expand animation is completed. * used to show menu activity when the expand animation is completed.
@@ -155,6 +171,16 @@ public class PipMotionHelper implements PipAppOpsListener.Callback,
mSnapAlgorithm = snapAlgorithm; mSnapAlgorithm = snapAlgorithm;
mFloatingContentCoordinator = floatingContentCoordinator; mFloatingContentCoordinator = floatingContentCoordinator;
mPipTaskOrganizer.registerPipTransitionCallback(mPipTransitionCallback); mPipTaskOrganizer.registerPipTransitionCallback(mPipTransitionCallback);
mResizePipVsyncCallback = l -> {
if (!mTemporaryBounds.isEmpty()) {
mPipTaskOrganizer.scheduleUserResizePip(
mBounds, mTemporaryBounds, null);
}
};
mResizePipUpdateListener = (target, values) ->
mSfVsyncFrameProvider.postFrameCallback(mResizePipVsyncCallback);
} }
@NonNull @NonNull
@@ -186,19 +212,8 @@ public class PipMotionHelper implements PipAppOpsListener.Callback,
} }
} }
/** boolean isAnimating() {
* Synchronizes the current bounds with either the pinned stack, or the ongoing animation. This return mTemporaryBoundsPhysicsAnimator.isRunning();
* is done to prepare for a touch gesture.
*/
void synchronizePinnedStackBoundsForTouchGesture() {
if (mAnimatingToBounds.isEmpty()) {
// If we're not animating anywhere, sync normally.
synchronizePinnedStackBounds();
} else {
// If we're animating, set the current bounds to the animated bounds. That way, the
// touch gesture will begin at the most recent animated location of the bounds.
mBounds.set(mAnimatedBounds);
}
} }
/** /**
@@ -224,32 +239,54 @@ public class PipMotionHelper implements PipAppOpsListener.Callback,
// If we are moving PIP directly to the touch event locations, cancel any animations and // If we are moving PIP directly to the touch event locations, cancel any animations and
// move PIP to the given bounds. // move PIP to the given bounds.
cancelAnimations(); cancelAnimations();
resizePipUnchecked(toBounds);
mBounds.set(toBounds); if (!isDragging) {
resizePipUnchecked(toBounds);
mBounds.set(toBounds);
} else {
mTemporaryBounds.set(toBounds);
mPipTaskOrganizer.scheduleUserResizePip(mBounds, mTemporaryBounds, null);
}
} else { } else {
// If PIP is 'catching up' after being stuck in the dismiss target, update the animation // If PIP is 'catching up' after being stuck in the dismiss target, update the animation
// to spring towards the new touch location. // to spring towards the new touch location.
mAnimatedBoundsPhysicsAnimator mTemporaryBoundsPhysicsAnimator
.spring(FloatProperties.RECT_WIDTH, mBounds.width(), mSpringConfig)
.spring(FloatProperties.RECT_HEIGHT, mBounds.height(), mSpringConfig)
.spring(FloatProperties.RECT_X, toBounds.left, mSpringConfig) .spring(FloatProperties.RECT_X, toBounds.left, mSpringConfig)
.spring(FloatProperties.RECT_Y, toBounds.top, mSpringConfig) .spring(FloatProperties.RECT_Y, toBounds.top, mSpringConfig);
.withEndActions(() -> mSpringingToTouch = false);
startBoundsAnimator(toBounds.left /* toX */, toBounds.top /* toY */, startBoundsAnimator(toBounds.left /* toX */, toBounds.top /* toY */,
false /* dismiss */); false /* dismiss */);
} }
} }
/** Set whether we're springing-to-touch to catch up after being stuck in the dismiss target. */ /** Animates the PIP into the dismiss target, scaling it down. */
void setSpringingToTouch(boolean springingToTouch) { void animateIntoDismissTarget(
if (springingToTouch) { MagnetizedObject.MagneticTarget target,
mAnimatedBounds.set(mBounds); float velX, float velY,
} boolean flung, Function0<Unit> after) {
final PointF targetCenter = target.getCenterOnScreen();
mSpringingToTouch = springingToTouch; final float desiredWidth = mBounds.width() / 2;
final float desiredHeight = mBounds.height() / 2;
final float destinationX = targetCenter.x - (desiredWidth / 2f);
final float destinationY = targetCenter.y - (desiredHeight / 2f);
mTemporaryBoundsPhysicsAnimator
.spring(FloatProperties.RECT_X, destinationX, velX, mSpringConfig)
.spring(FloatProperties.RECT_Y, destinationY, velY, mSpringConfig)
.spring(FloatProperties.RECT_WIDTH, desiredWidth, mSpringConfig)
.spring(FloatProperties.RECT_HEIGHT, desiredHeight, mSpringConfig)
.withEndActions(after);
startBoundsAnimator(destinationX, destinationY, false);
} }
void prepareForAnimation() { /** Set whether we're springing-to-touch to catch up after being stuck in the dismiss target. */
mAnimatedBounds.set(mBounds); void setSpringingToTouch(boolean springingToTouch) {
mSpringingToTouch = springingToTouch;
} }
/** /**
@@ -308,14 +345,23 @@ public class PipMotionHelper implements PipAppOpsListener.Callback,
return mBounds; return mBounds;
} }
/**
* Returns the PIP bounds if we're not animating, or the current, temporary animating bounds
* otherwise.
*/
Rect getPossiblyAnimatingBounds() {
return mTemporaryBounds.isEmpty() ? mBounds : mTemporaryBounds;
}
/** /**
* Flings the PiP to the closest snap target. * Flings the PiP to the closest snap target.
*/ */
void flingToSnapTarget( void flingToSnapTarget(
float velocityX, float velocityY, float velocityX, float velocityY,
@Nullable Runnable updateAction, @Nullable Runnable endAction) { @Nullable Runnable updateAction, @Nullable Runnable endAction) {
mAnimatedBounds.set(mBounds); mTemporaryBoundsPhysicsAnimator
mAnimatedBoundsPhysicsAnimator .spring(FloatProperties.RECT_WIDTH, mBounds.width(), mSpringConfig)
.spring(FloatProperties.RECT_HEIGHT, mBounds.height(), mSpringConfig)
.flingThenSpring( .flingThenSpring(
FloatProperties.RECT_X, velocityX, mFlingConfigX, mSpringConfig, FloatProperties.RECT_X, velocityX, mFlingConfigX, mSpringConfig,
true /* flingMustReachMinOrMax */) true /* flingMustReachMinOrMax */)
@@ -324,13 +370,14 @@ public class PipMotionHelper implements PipAppOpsListener.Callback,
.withEndActions(endAction); .withEndActions(endAction);
if (updateAction != null) { if (updateAction != null) {
mAnimatedBoundsPhysicsAnimator.addUpdateListener( mTemporaryBoundsPhysicsAnimator.addUpdateListener(
(target, values) -> updateAction.run()); (target, values) -> updateAction.run());
} }
final float xEndValue = velocityX < 0 ? mMovementBounds.left : mMovementBounds.right; final float xEndValue = velocityX < 0 ? mMovementBounds.left : mMovementBounds.right;
final float estimatedFlingYEndValue = final float estimatedFlingYEndValue =
PhysicsAnimator.estimateFlingEndValue(mBounds.top, velocityY, mFlingConfigY); PhysicsAnimator.estimateFlingEndValue(
mTemporaryBounds.top, velocityY, mFlingConfigY);
startBoundsAnimator(xEndValue /* toX */, estimatedFlingYEndValue /* toY */, startBoundsAnimator(xEndValue /* toX */, estimatedFlingYEndValue /* toY */,
false /* dismiss */); false /* dismiss */);
@@ -341,8 +388,12 @@ public class PipMotionHelper implements PipAppOpsListener.Callback,
* configuration * configuration
*/ */
void animateToBounds(Rect bounds, PhysicsAnimator.SpringConfig springConfig) { void animateToBounds(Rect bounds, PhysicsAnimator.SpringConfig springConfig) {
mAnimatedBounds.set(mBounds); if (!mTemporaryBoundsPhysicsAnimator.isRunning()) {
mAnimatedBoundsPhysicsAnimator // Animate from the current bounds if we're not already animating.
mTemporaryBounds.set(mBounds);
}
mTemporaryBoundsPhysicsAnimator
.spring(FloatProperties.RECT_X, bounds.left, springConfig) .spring(FloatProperties.RECT_X, bounds.left, springConfig)
.spring(FloatProperties.RECT_Y, bounds.top, springConfig); .spring(FloatProperties.RECT_Y, bounds.top, springConfig);
startBoundsAnimator(bounds.left /* toX */, bounds.top /* toY */, startBoundsAnimator(bounds.left /* toX */, bounds.top /* toY */,
@@ -353,18 +404,19 @@ public class PipMotionHelper implements PipAppOpsListener.Callback,
* Animates the dismissal of the PiP off the edge of the screen. * Animates the dismissal of the PiP off the edge of the screen.
*/ */
void animateDismiss() { void animateDismiss() {
mAnimatedBounds.set(mBounds);
// Animate off the bottom of the screen, then dismiss PIP. // Animate off the bottom of the screen, then dismiss PIP.
mAnimatedBoundsPhysicsAnimator mTemporaryBoundsPhysicsAnimator
.spring(FloatProperties.RECT_Y, .spring(FloatProperties.RECT_Y,
mBounds.bottom + mBounds.height(), mMovementBounds.bottom + mBounds.height() * 2,
0, 0,
mSpringConfig) mSpringConfig)
.withEndActions(this::dismissPip); .withEndActions(this::dismissPip);
startBoundsAnimator(mBounds.left /* toX */, mBounds.bottom + mBounds.height() /* toY */, startBoundsAnimator(
mBounds.left /* toX */, mBounds.bottom + mBounds.height() /* toY */,
true /* dismiss */); true /* dismiss */);
mDismissalPending = false;
} }
/** /**
@@ -415,7 +467,7 @@ public class PipMotionHelper implements PipAppOpsListener.Callback,
* Cancels all existing animations. * Cancels all existing animations.
*/ */
private void cancelAnimations() { private void cancelAnimations() {
mAnimatedBoundsPhysicsAnimator.cancel(); mTemporaryBoundsPhysicsAnimator.cancel();
mAnimatingToBounds.setEmpty(); mAnimatingToBounds.setEmpty();
mSpringingToTouch = false; mSpringingToTouch = false;
} }
@@ -449,15 +501,36 @@ public class PipMotionHelper implements PipAppOpsListener.Callback,
(int) toY + mBounds.height()); (int) toY + mBounds.height());
setAnimatingToBounds(mAnimatingToBounds); setAnimatingToBounds(mAnimatingToBounds);
mAnimatedBoundsPhysicsAnimator if (!mTemporaryBoundsPhysicsAnimator.isRunning()) {
.withEndActions(() -> { mTemporaryBoundsPhysicsAnimator
if (!dismiss) { .addUpdateListener(mResizePipUpdateListener)
mPipTaskOrganizer.scheduleFinishResizePip(mAnimatedBounds); .withEndActions(this::onBoundsAnimationEnd);
} }
mAnimatingToBounds.setEmpty();
}) mTemporaryBoundsPhysicsAnimator.start();
.addUpdateListener(mResizePipUpdateListener) }
.start();
/**
* Notify that PIP was released in the dismiss target and will be animated out and dismissed
* shortly.
*/
void notifyDismissalPending() {
mDismissalPending = true;
}
private void onBoundsAnimationEnd() {
if (!mDismissalPending
&& !mSpringingToTouch
&& !mMagnetizedPip.getObjectStuckToTarget()) {
mBounds.set(mTemporaryBounds);
mPipTaskOrganizer.scheduleFinishResizePip(mBounds);
mTemporaryBounds.setEmpty();
}
mAnimatingToBounds.setEmpty();
mSpringingToTouch = false;
mDismissalPending = false;
} }
/** /**
@@ -503,25 +576,29 @@ public class PipMotionHelper implements PipAppOpsListener.Callback,
* magnetic dismiss target so it can calculate PIP's size and position. * magnetic dismiss target so it can calculate PIP's size and position.
*/ */
MagnetizedObject<Rect> getMagnetizedPip() { MagnetizedObject<Rect> getMagnetizedPip() {
return new MagnetizedObject<Rect>( if (mMagnetizedPip == null) {
mContext, mAnimatedBounds, FloatProperties.RECT_X, FloatProperties.RECT_Y) { mMagnetizedPip = new MagnetizedObject<Rect>(
@Override mContext, mTemporaryBounds, FloatProperties.RECT_X, FloatProperties.RECT_Y) {
public float getWidth(@NonNull Rect animatedPipBounds) { @Override
return animatedPipBounds.width(); public float getWidth(@NonNull Rect animatedPipBounds) {
} return animatedPipBounds.width();
}
@Override @Override
public float getHeight(@NonNull Rect animatedPipBounds) { public float getHeight(@NonNull Rect animatedPipBounds) {
return animatedPipBounds.height(); return animatedPipBounds.height();
} }
@Override @Override
public void getLocationOnScreen( public void getLocationOnScreen(
@NonNull Rect animatedPipBounds, @NonNull int[] loc) { @NonNull Rect animatedPipBounds, @NonNull int[] loc) {
loc[0] = animatedPipBounds.left; loc[0] = animatedPipBounds.left;
loc[1] = animatedPipBounds.top; loc[1] = animatedPipBounds.top;
} }
}; };
}
return mMagnetizedPip;
} }
public void dump(PrintWriter pw, String prefix) { public void dump(PrintWriter pw, String prefix) {

View File

@@ -70,6 +70,8 @@ import com.android.systemui.util.magnetictarget.MagnetizedObject;
import java.io.PrintWriter; import java.io.PrintWriter;
import kotlin.Unit;
/** /**
* Manages all the touch handling for PIP on the Phone, including moving, dismissing and expanding * Manages all the touch handling for PIP on the Phone, including moving, dismissing and expanding
* the PIP. * the PIP.
@@ -262,12 +264,14 @@ public class PipTouchHandler {
mMagneticTarget = mMagnetizedPip.addTarget(mTargetView, 0); mMagneticTarget = mMagnetizedPip.addTarget(mTargetView, 0);
updateMagneticTargetSize(); updateMagneticTargetSize();
mMagnetizedPip.setPhysicsAnimatorUpdateListener(mMotionHelper.mResizePipUpdateListener); mMagnetizedPip.setAnimateStuckToTarget(
(target, velX, velY, flung, after) -> {
mMotionHelper.animateIntoDismissTarget(target, velX, velY, flung, after);
return Unit.INSTANCE;
});
mMagnetizedPip.setMagnetListener(new MagnetizedObject.MagnetListener() { mMagnetizedPip.setMagnetListener(new MagnetizedObject.MagnetListener() {
@Override @Override
public void onStuckToTarget(@NonNull MagnetizedObject.MagneticTarget target) { public void onStuckToTarget(@NonNull MagnetizedObject.MagneticTarget target) {
mMotionHelper.prepareForAnimation();
// Show the dismiss target, in case the initial touch event occurred within the // Show the dismiss target, in case the initial touch event occurred within the
// magnetic field radius. // magnetic field radius.
showDismissTargetMaybe(); showDismissTargetMaybe();
@@ -286,12 +290,13 @@ public class PipTouchHandler {
@Override @Override
public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target) { public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target) {
mMotionHelper.notifyDismissalPending();
mHandler.post(() -> { mHandler.post(() -> {
mMotionHelper.animateDismiss(); mMotionHelper.animateDismiss();
hideDismissTarget(); hideDismissTarget();
}); });
MetricsLoggerWrapper.logPictureInPictureDismissByDrag(mContext, MetricsLoggerWrapper.logPictureInPictureDismissByDrag(mContext,
PipUtils.getTopPipActivity(mContext, mActivityManager)); PipUtils.getTopPipActivity(mContext, mActivityManager));
} }
@@ -617,11 +622,16 @@ public class PipTouchHandler {
} }
MotionEvent ev = (MotionEvent) inputEvent; MotionEvent ev = (MotionEvent) inputEvent;
if (!mTouchState.isDragging()
if (mPipResizeGestureHandler.isWithinTouchRegion((int) ev.getRawX(), (int) ev.getRawY())) { && !mMagnetizedPip.getObjectStuckToTarget()
&& !mMotionHelper.isAnimating()
&& mPipResizeGestureHandler.isWithinTouchRegion(
(int) ev.getRawX(), (int) ev.getRawY())) {
return true; return true;
} }
if (mMagnetizedPip.maybeConsumeMotionEvent(ev)) {
if ((ev.getAction() == MotionEvent.ACTION_DOWN || mTouchState.isUserInteracting())
&& mMagnetizedPip.maybeConsumeMotionEvent(ev)) {
// If the first touch event occurs within the magnetic field, pass the ACTION_DOWN event // 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 // 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. // events it'll receive if the object is dragged out of the magnetic field.
@@ -643,7 +653,6 @@ public class PipTouchHandler {
switch (ev.getAction()) { switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN: { case MotionEvent.ACTION_DOWN: {
mMotionHelper.synchronizePinnedStackBoundsForTouchGesture();
mGesture.onDown(mTouchState); mGesture.onDown(mTouchState);
break; break;
} }
@@ -872,7 +881,7 @@ public class PipTouchHandler {
return; return;
} }
Rect bounds = mMotionHelper.getBounds(); Rect bounds = mMotionHelper.getPossiblyAnimatingBounds();
mDelta.set(0f, 0f); mDelta.set(0f, 0f);
mStartPosition.set(bounds.left, bounds.top); mStartPosition.set(bounds.left, bounds.top);
mMovementWithinDismiss = touchState.getDownTouchPosition().y >= mMovementBounds.bottom; mMovementWithinDismiss = touchState.getDownTouchPosition().y >= mMovementBounds.bottom;
@@ -914,7 +923,7 @@ public class PipTouchHandler {
mDelta.x += left - lastX; mDelta.x += left - lastX;
mDelta.y += top - lastY; mDelta.y += top - lastY;
mTmpBounds.set(mMotionHelper.getBounds()); mTmpBounds.set(mMotionHelper.getPossiblyAnimatingBounds());
mTmpBounds.offsetTo((int) left, (int) top); mTmpBounds.offsetTo((int) left, (int) top);
mMotionHelper.movePip(mTmpBounds, true /* isDragging */); mMotionHelper.movePip(mTmpBounds, true /* isDragging */);

View File

@@ -66,6 +66,40 @@ class FloatProperties {
} }
} }
/**
* Represents the width of a [Rect]. Typically used to animate resizing a Rect horizontally.
*
* This property's getter returns [Rect.width], and its setter changes the value of
* [Rect.right] by adding the animated width value to [Rect.left].
*/
@JvmField
val RECT_WIDTH = object : FloatPropertyCompat<Rect>("RectWidth") {
override fun getValue(rect: Rect): Float {
return rect.width().toFloat()
}
override fun setValue(rect: Rect, value: Float) {
rect.right = rect.left + value.toInt()
}
}
/**
* Represents the height of a [Rect]. Typically used to animate resizing a Rect vertically.
*
* This property's getter returns [Rect.height], and its setter changes the value of
* [Rect.bottom] by adding the animated height value to [Rect.top].
*/
@JvmField
val RECT_HEIGHT = object : FloatPropertyCompat<Rect>("RectHeight") {
override fun getValue(rect: Rect): Float {
return rect.height().toFloat()
}
override fun setValue(rect: Rect, value: Float) {
rect.bottom = rect.top + value.toInt()
}
}
/** /**
* Represents the x-coordinate of a [RectF]. Typically used to animate moving a RectF * Represents the x-coordinate of a [RectF]. Typically used to animate moving a RectF
* horizontally. * horizontally.

View File

@@ -177,6 +177,18 @@ abstract class MagnetizedObject<T : Any>(
*/ */
var physicsAnimatorEndListener: PhysicsAnimator.EndListener<T>? = null var physicsAnimatorEndListener: PhysicsAnimator.EndListener<T>? = null
/**
* Method that is called when the object should be animated stuck to the target. The default
* implementation uses the object's x and y properties to animate the object centered inside the
* target. You can override this if you need custom animation.
*
* The method is invoked with the MagneticTarget that the object is sticking to, the X and Y
* velocities of the gesture that brought the object into the magnetic radius, whether or not it
* was flung, and a callback you must call after your animation completes.
*/
var animateStuckToTarget: (MagneticTarget, Float, Float, Boolean, (() -> Unit)?) -> Unit =
::animateStuckToTargetInternal
/** /**
* Sets whether forcefully flinging the object vertically towards a target causes it to be * 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 * attracted to the target and then released immediately, despite never being dragged within the
@@ -373,7 +385,7 @@ abstract class MagnetizedObject<T : Any>(
targetObjectIsStuckTo = targetObjectIsInMagneticFieldOf targetObjectIsStuckTo = targetObjectIsInMagneticFieldOf
cancelAnimations() cancelAnimations()
magnetListener.onStuckToTarget(targetObjectIsInMagneticFieldOf!!) magnetListener.onStuckToTarget(targetObjectIsInMagneticFieldOf!!)
animateStuckToTarget(targetObjectIsInMagneticFieldOf, velX, velY, false) animateStuckToTarget(targetObjectIsInMagneticFieldOf, velX, velY, false, null)
vibrateIfEnabled(VibrationEffect.EFFECT_HEAVY_CLICK) vibrateIfEnabled(VibrationEffect.EFFECT_HEAVY_CLICK)
} else if (targetObjectIsInMagneticFieldOf == null && objectStuckToTarget) { } else if (targetObjectIsInMagneticFieldOf == null && objectStuckToTarget) {
@@ -430,8 +442,8 @@ abstract class MagnetizedObject<T : Any>(
targetObjectIsStuckTo = flungToTarget targetObjectIsStuckTo = flungToTarget
animateStuckToTarget(flungToTarget, velX, velY, true) { animateStuckToTarget(flungToTarget, velX, velY, true) {
targetObjectIsStuckTo = null
magnetListener.onReleasedInTarget(flungToTarget) magnetListener.onReleasedInTarget(flungToTarget)
targetObjectIsStuckTo = null
vibrateIfEnabled(VibrationEffect.EFFECT_HEAVY_CLICK) vibrateIfEnabled(VibrationEffect.EFFECT_HEAVY_CLICK)
} }
@@ -465,7 +477,7 @@ abstract class MagnetizedObject<T : Any>(
} }
/** Animates sticking the object to the provided target with the given start velocities. */ /** Animates sticking the object to the provided target with the given start velocities. */
private fun animateStuckToTarget( private fun animateStuckToTargetInternal(
target: MagneticTarget, target: MagneticTarget,
velX: Float, velX: Float,
velY: Float, velY: Float,
@@ -581,10 +593,10 @@ abstract class MagnetizedObject<T : Any>(
* multiple objects. * multiple objects.
*/ */
class MagneticTarget( class MagneticTarget(
internal val targetView: View, val targetView: View,
var magneticFieldRadiusPx: Int var magneticFieldRadiusPx: Int
) { ) {
internal val centerOnScreen = PointF() val centerOnScreen = PointF()
private val tempLoc = IntArray(2) private val tempLoc = IntArray(2)