Merge changes I65d2b1bc,I35bf9cb4 into sc-dev
* changes:
Animate App Widget activity launch from Quickstep launchers
Add a mechanism to temporarily defer App Widget updates
diff --git a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
index 5b1b59b..1a464b1 100644
--- a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
+++ b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
@@ -79,6 +79,7 @@
import com.android.launcher3.util.MultiValueAlpha.AlphaProperty;
import com.android.launcher3.util.RunnableList;
import com.android.launcher3.views.FloatingIconView;
+import com.android.launcher3.widget.LauncherAppWidgetHostView;
import com.android.quickstep.RemoteAnimationTargets;
import com.android.quickstep.SystemUiProxy;
import com.android.quickstep.TaskViewUtils;
@@ -86,6 +87,7 @@
import com.android.quickstep.util.RemoteAnimationProvider;
import com.android.quickstep.util.StaggeredWorkspaceAnim;
import com.android.quickstep.util.SurfaceTransactionApplier;
+import com.android.quickstep.views.FloatingWidgetView;
import com.android.quickstep.views.RecentsView;
import com.android.systemui.shared.system.ActivityCompat;
import com.android.systemui.shared.system.ActivityOptionsCompat;
@@ -160,6 +162,9 @@
private static final int MAX_NUM_TASKS = 5;
+ // Cross-fade duration between App Widget and App
+ private static final int WIDGET_CROSSFADE_DURATION_MILLIS = 125;
+
protected final BaseQuickstepLauncher mLauncher;
private final DragLayer mDragLayer;
@@ -349,6 +354,29 @@
}
}
+ private void composeWidgetLaunchAnimator(
+ @NonNull AnimatorSet anim,
+ @NonNull LauncherAppWidgetHostView v,
+ @NonNull RemoteAnimationTargetCompat[] appTargets,
+ @NonNull RemoteAnimationTargetCompat[] wallpaperTargets,
+ @NonNull RemoteAnimationTargetCompat[] nonAppTargets) {
+ mLauncher.getStateManager().setCurrentAnimation(anim);
+
+ Rect windowTargetBounds = getWindowTargetBounds(appTargets, getRotationChange(appTargets));
+ anim.play(getOpeningWindowAnimatorsForWidget(v, appTargets, wallpaperTargets, nonAppTargets,
+ windowTargetBounds, areAllTargetsTranslucent(appTargets)));
+
+ anim.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ mLauncher.addOnResumeCallback(() ->
+ ObjectAnimator.ofFloat(mLauncher.getDepthController(), DEPTH,
+ mLauncher.getStateManager().getState().getDepth(
+ mLauncher)).start());
+ }
+ });
+ }
+
/**
* Return the window bounds of the opening target.
* In multiwindow mode, we need to get the final size of the opening app window target to help
@@ -737,6 +765,112 @@
}
});
+ animatorSet.playTogether(appAnimator, getBackgroundAnimator(appTargets));
+ return animatorSet;
+ }
+
+ private Animator getOpeningWindowAnimatorsForWidget(LauncherAppWidgetHostView v,
+ RemoteAnimationTargetCompat[] appTargets,
+ RemoteAnimationTargetCompat[] wallpaperTargets,
+ RemoteAnimationTargetCompat[] nonAppTargets, Rect windowTargetBounds,
+ boolean appTargetsAreTranslucent) {
+ final RectF widgetBackgroundBounds = new RectF();
+ final Rect appWindowCrop = new Rect();
+ final Matrix matrix = new Matrix();
+
+ final float finalWindowRadius = mDeviceProfile.isMultiWindowMode
+ ? 0 : getWindowCornerRadius(mLauncher.getResources());
+ final FloatingWidgetView floatingView = FloatingWidgetView.getFloatingWidgetView(mLauncher,
+ v, widgetBackgroundBounds, windowTargetBounds, finalWindowRadius);
+ final float initialWindowRadius = supportsRoundedCornersOnWindows(mLauncher.getResources())
+ ? floatingView.getInitialCornerRadius() : 0;
+
+ RemoteAnimationTargets openingTargets = new RemoteAnimationTargets(appTargets,
+ wallpaperTargets, nonAppTargets, MODE_OPENING);
+ SurfaceTransactionApplier surfaceApplier = new SurfaceTransactionApplier(floatingView);
+ openingTargets.addReleaseCheck(surfaceApplier);
+
+ AnimatorSet animatorSet = new AnimatorSet();
+ ValueAnimator appAnimator = ValueAnimator.ofFloat(0, 1);
+ appAnimator.setDuration(APP_LAUNCH_DURATION);
+ appAnimator.setInterpolator(LINEAR);
+ appAnimator.addListener(floatingView);
+ appAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ openingTargets.release();
+ }
+ });
+ floatingView.setFastFinishRunnable(animatorSet::end);
+
+ appAnimator.addUpdateListener(new MultiValueUpdateListener() {
+ float mAppWindowScale = 1;
+ final FloatProp mWidgetForegroundAlpha = new FloatProp(1 /* start */,
+ 0 /* end */, 0 /* delay */,
+ WIDGET_CROSSFADE_DURATION_MILLIS / 2 /* duration */, LINEAR);
+ final FloatProp mWidgetFallbackBackgroundAlpha = new FloatProp(0 /* start */,
+ 1 /* end */, 0 /* delay */, 75 /* duration */, LINEAR);
+ final FloatProp mPreviewAlpha = new FloatProp(0 /* start */, 1 /* end */,
+ WIDGET_CROSSFADE_DURATION_MILLIS / 2 /* delay */,
+ WIDGET_CROSSFADE_DURATION_MILLIS / 2 /* duration */, LINEAR);
+ final FloatProp mWindowRadius = new FloatProp(initialWindowRadius, finalWindowRadius,
+ 0 /* start */, RADIUS_DURATION, LINEAR);
+ final FloatProp mCornerRadiusProgress = new FloatProp(0, 1, 0, RADIUS_DURATION, LINEAR);
+
+ // Window & widget background positioning bounds
+ final FloatProp mDx = new FloatProp(widgetBackgroundBounds.centerX(),
+ windowTargetBounds.centerX(), 0 /* delay */, APP_LAUNCH_CURVED_DURATION,
+ EXAGGERATED_EASE);
+ final FloatProp mDy = new FloatProp(widgetBackgroundBounds.centerY(),
+ windowTargetBounds.centerY(), 0 /* delay */, APP_LAUNCH_DURATION,
+ EXAGGERATED_EASE);
+ final FloatProp mWidth = new FloatProp(widgetBackgroundBounds.width(),
+ windowTargetBounds.width(), 0 /* delay */, APP_LAUNCH_DURATION,
+ EXAGGERATED_EASE);
+ final FloatProp mHeight = new FloatProp(widgetBackgroundBounds.height(),
+ windowTargetBounds.height(), 0 /* delay */, APP_LAUNCH_DURATION,
+ EXAGGERATED_EASE);
+
+ @Override
+ public void onUpdate(float percent) {
+ widgetBackgroundBounds.set(mDx.value - mWidth.value / 2f,
+ mDy.value - mHeight.value / 2f, mDx.value + mWidth.value / 2f,
+ mDy.value + mHeight.value / 2f);
+ // Set app window scaling factor to match widget background width
+ mAppWindowScale = widgetBackgroundBounds.width() / windowTargetBounds.width();
+ // Crop scaled app window to match widget
+ appWindowCrop.set(0 /* left */, 0 /* top */,
+ Math.round(windowTargetBounds.width()) /* right */,
+ Math.round(widgetBackgroundBounds.height() / mAppWindowScale) /* bottom */);
+ matrix.setTranslate(widgetBackgroundBounds.left, widgetBackgroundBounds.top);
+ matrix.postScale(mAppWindowScale, mAppWindowScale, widgetBackgroundBounds.left,
+ widgetBackgroundBounds.top);
+
+ SurfaceParams[] params = new SurfaceParams[appTargets.length];
+ float floatingViewAlpha = appTargetsAreTranslucent ? 1 - mPreviewAlpha.value : 1;
+ for (int i = appTargets.length - 1; i >= 0; i--) {
+ RemoteAnimationTargetCompat target = appTargets[i];
+ SurfaceParams.Builder builder = new SurfaceParams.Builder(target.leash);
+ if (target.mode == MODE_OPENING) {
+ floatingView.update(widgetBackgroundBounds, floatingViewAlpha,
+ mWidgetForegroundAlpha.value, mWidgetFallbackBackgroundAlpha.value,
+ mCornerRadiusProgress.value);
+ builder.withMatrix(matrix)
+ .withWindowCrop(appWindowCrop)
+ .withAlpha(mPreviewAlpha.value)
+ .withCornerRadius(mWindowRadius.value / mAppWindowScale);
+ }
+ params[i] = builder.build();
+ }
+ surfaceApplier.scheduleApply(params);
+ }
+ });
+
+ animatorSet.playTogether(appAnimator, getBackgroundAnimator(appTargets));
+ return animatorSet;
+ }
+
+ private ObjectAnimator getBackgroundAnimator(RemoteAnimationTargetCompat[] appTargets) {
// When launching an app from overview that doesn't map to a task, we still want to just
// blur the wallpaper instead of the launcher surface as well
boolean allowBlurringLauncher = mLauncher.getStateManager().getState() != OVERVIEW;
@@ -754,9 +888,7 @@
}
});
}
-
- animatorSet.playTogether(appAnimator, backgroundRadiusAnim);
- return animatorSet;
+ return backgroundRadiusAnim;
}
/**
@@ -1120,9 +1252,13 @@
boolean launcherClosing =
launcherIsATargetWithMode(appTargets, MODE_CLOSING);
+ final boolean launchingFromWidget = mV instanceof LauncherAppWidgetHostView;
final boolean launchingFromRecents = isLaunchingFromRecents(mV, appTargets);
final boolean launchingFromTaskbar = mLauncher.isViewInTaskbar(mV);
- if (launchingFromRecents) {
+ if (launchingFromWidget) {
+ composeWidgetLaunchAnimator(anim, (LauncherAppWidgetHostView) mV, appTargets,
+ wallpaperTargets, nonAppTargets);
+ } else if (launchingFromRecents) {
composeRecentsLaunchAnimator(anim, mV, appTargets, wallpaperTargets, nonAppTargets,
launcherClosing);
} else if (launchingFromTaskbar) {
diff --git a/quickstep/src/com/android/quickstep/views/FloatingWidgetBackgroundView.java b/quickstep/src/com/android/quickstep/views/FloatingWidgetBackgroundView.java
new file mode 100644
index 0000000..f74aa55
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/views/FloatingWidgetBackgroundView.java
@@ -0,0 +1,191 @@
+/*
+ * Copyright (C) 2021 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.quickstep.views;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.graphics.Outline;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.GradientDrawable;
+import android.os.Build;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewOutlineProvider;
+import android.widget.RemoteViews.RemoteViewOutlineProvider;
+
+import com.android.launcher3.util.Themes;
+import com.android.launcher3.widget.LauncherAppWidgetHostView;
+import com.android.launcher3.widget.RoundedCornerEnforcement;
+
+import java.util.stream.IntStream;
+
+/**
+ * Mimics the appearance of the background view of a {@link LauncherAppWidgetHostView} through a
+ * an App Widget activity launch animation.
+ */
+@TargetApi(Build.VERSION_CODES.S)
+final class FloatingWidgetBackgroundView extends View {
+ private final ColorDrawable mFallbackDrawable = new ColorDrawable();
+ private final DrawableProperties mForegroundProperties = new DrawableProperties();
+ private final DrawableProperties mBackgroundProperties = new DrawableProperties();
+
+ private Drawable mOriginalForeground;
+ private Drawable mOriginalBackground;
+ private float mFinalRadius;
+ private float mInitialOutlineRadius;
+ private float mOutlineRadius;
+ private boolean mIsUsingFallback;
+ private View mSourceView;
+
+ FloatingWidgetBackgroundView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ setOutlineProvider(new ViewOutlineProvider() {
+ @Override
+ public void getOutline(View view, Outline outline) {
+ outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), mOutlineRadius);
+ }
+ });
+ setClipToOutline(true);
+ }
+
+ void init(LauncherAppWidgetHostView hostView, View backgroundView, float finalRadius) {
+ mFinalRadius = finalRadius;
+ mSourceView = backgroundView;
+ mInitialOutlineRadius = getOutlineRadius(hostView, backgroundView);
+ mIsUsingFallback = false;
+ if (isSupportedDrawable(backgroundView.getForeground())) {
+ mOriginalForeground = backgroundView.getForeground();
+ mForegroundProperties.init(
+ mOriginalForeground.getConstantState().newDrawable().mutate());
+ setForeground(mForegroundProperties.mDrawable);
+ mSourceView.setForeground(null);
+ }
+ if (isSupportedDrawable(backgroundView.getBackground())) {
+ mOriginalBackground = backgroundView.getBackground();
+ mBackgroundProperties.init(
+ mOriginalBackground.getConstantState().newDrawable().mutate());
+ setBackground(mBackgroundProperties.mDrawable);
+ mSourceView.setBackground(null);
+ } else if (mOriginalForeground == null) {
+ mFallbackDrawable.setColor(Themes.getColorBackground(backgroundView.getContext()));
+ setBackground(mFallbackDrawable);
+ mIsUsingFallback = true;
+ }
+ }
+
+ /** Update the animated properties of the drawables. */
+ void update(float cornerRadiusProgress, float fallbackAlpha) {
+ if (isUninitialized()) return;
+ mOutlineRadius = mInitialOutlineRadius + (mFinalRadius - mInitialOutlineRadius)
+ * cornerRadiusProgress;
+ mForegroundProperties.updateDrawable(mFinalRadius, cornerRadiusProgress);
+ mBackgroundProperties.updateDrawable(mFinalRadius, cornerRadiusProgress);
+ setAlpha(mIsUsingFallback ? fallbackAlpha : 1f);
+ }
+
+ /** Restores the drawables to the source view. */
+ void finish() {
+ if (isUninitialized()) return;
+ mSourceView.setForeground(mOriginalForeground);
+ mSourceView.setBackground(mOriginalBackground);
+ }
+
+ void recycle() {
+ mSourceView = null;
+ mOriginalForeground = null;
+ mOriginalBackground = null;
+ mOutlineRadius = 0;
+ mFinalRadius = 0;
+ setForeground(null);
+ setBackground(null);
+ }
+
+ /** Get the largest of drawable corner radii or background view outline radius. */
+ float getMaximumRadius() {
+ if (isUninitialized()) return 0;
+ return Math.max(mInitialOutlineRadius, Math.max(getMaxRadius(mOriginalForeground),
+ getMaxRadius(mOriginalBackground)));
+ }
+
+ private boolean isUninitialized() {
+ return mSourceView == null;
+ }
+
+ /** Returns the maximum corner radius of {@param drawable}. */
+ private static float getMaxRadius(Drawable drawable) {
+ if (!(drawable instanceof GradientDrawable)) return 0;
+ float[] cornerRadii = ((GradientDrawable) drawable).getCornerRadii();
+ float cornerRadius = ((GradientDrawable) drawable).getCornerRadius();
+ double radiiMax = cornerRadii == null ? 0 : IntStream.range(0, cornerRadii.length)
+ .mapToDouble(i -> cornerRadii[i]).max().orElse(0);
+ return Math.max(cornerRadius, (float) radiiMax);
+ }
+
+ /** Returns whether the given drawable type is supported. */
+ private static boolean isSupportedDrawable(Drawable drawable) {
+ return drawable instanceof ColorDrawable || (drawable instanceof GradientDrawable
+ && ((GradientDrawable) drawable).getShape() == GradientDrawable.RECTANGLE);
+ }
+
+ /** Corner radius from source view's outline, or enforced view. */
+ private static float getOutlineRadius(LauncherAppWidgetHostView hostView, View v) {
+ if (RoundedCornerEnforcement.isRoundedCornerEnabled()
+ && hostView.hasEnforcedCornerRadius()) {
+ return hostView.getEnforcedCornerRadius();
+ } else if (v.getOutlineProvider() instanceof RemoteViewOutlineProvider
+ && v.getClipToOutline()) {
+ return ((RemoteViewOutlineProvider) v.getOutlineProvider()).getRadius();
+ }
+ return 0;
+ }
+
+ /** Stores and modifies a drawable's properties through an animation. */
+ private static class DrawableProperties {
+ private Drawable mDrawable;
+ private float mOriginalRadius;
+ private float[] mOriginalRadii;
+ private final float[] mTmpRadii = new float[8];
+
+ /** Store a drawable's animated properties. */
+ void init(Drawable drawable) {
+ mDrawable = drawable;
+ if (!(drawable instanceof GradientDrawable)) return;
+ mOriginalRadius = ((GradientDrawable) drawable).getCornerRadius();
+ mOriginalRadii = ((GradientDrawable) drawable).getCornerRadii();
+ }
+
+ /**
+ * Update the drawable for the given animation state.
+ *
+ * @param finalRadius the radius of each corner when {@param progress} is 1
+ * @param progress the linear progress of the corner radius from its original value to
+ * {@param finalRadius}
+ */
+ void updateDrawable(float finalRadius, float progress) {
+ if (!(mDrawable instanceof GradientDrawable)) return;
+ GradientDrawable d = (GradientDrawable) mDrawable;
+ if (mOriginalRadii != null) {
+ for (int i = 0; i < mOriginalRadii.length; i++) {
+ mTmpRadii[i] = mOriginalRadii[i] + (finalRadius - mOriginalRadii[i]) * progress;
+ }
+ d.setCornerRadii(mTmpRadii);
+ } else {
+ d.setCornerRadius(mOriginalRadius + (finalRadius - mOriginalRadius) * progress);
+ }
+ }
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/views/FloatingWidgetView.java b/quickstep/src/com/android/quickstep/views/FloatingWidgetView.java
new file mode 100644
index 0000000..d23884c
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/views/FloatingWidgetView.java
@@ -0,0 +1,250 @@
+/*
+ * Copyright (C) 2021 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.quickstep.views;
+
+import android.animation.Animator;
+import android.animation.Animator.AnimatorListener;
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.os.Build;
+import android.util.AttributeSet;
+import android.view.GhostView;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+
+import com.android.launcher3.Launcher;
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.dragndrop.DragLayer;
+import com.android.launcher3.views.ListenerView;
+import com.android.launcher3.widget.LauncherAppWidgetHostView;
+import com.android.launcher3.widget.RoundedCornerEnforcement;
+
+/** A view that mimics an App Widget through a launch animation. */
+@TargetApi(Build.VERSION_CODES.S)
+public class FloatingWidgetView extends FrameLayout implements AnimatorListener {
+ private static final Matrix sTmpMatrix = new Matrix();
+
+ private final Launcher mLauncher;
+ private final ListenerView mListenerView;
+ private final FloatingWidgetBackgroundView mBackgroundView;
+ private final RectF mBackgroundOffset = new RectF();
+
+ private LauncherAppWidgetHostView mAppWidgetView;
+ private View mAppWidgetBackgroundView;
+ private RectF mBackgroundPosition;
+ private GhostView mForegroundOverlayView;
+
+ private Runnable mEndRunnable;
+ private Runnable mFastFinishRunnable;
+
+ public FloatingWidgetView(Context context) {
+ this(context, null);
+ }
+
+ public FloatingWidgetView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public FloatingWidgetView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ mLauncher = Launcher.getLauncher(context);
+ mListenerView = new ListenerView(context, attrs);
+ mBackgroundView = new FloatingWidgetBackgroundView(context, attrs, defStyleAttr);
+ addView(mBackgroundView);
+ setWillNotDraw(false);
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animator) {
+ Runnable endRunnable = mEndRunnable;
+ mEndRunnable = null;
+ if (endRunnable != null) {
+ endRunnable.run();
+ }
+ }
+
+ @Override
+ public void onAnimationStart(Animator animator) {
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animator) {
+ }
+
+ @Override
+ public void onAnimationRepeat(Animator animator) {
+ }
+
+ /** Sets a runnable that is called after a call to {@link #fastFinish()}. */
+ public void setFastFinishRunnable(Runnable runnable) {
+ mFastFinishRunnable = runnable;
+ }
+
+ /** Callback at the end or early exit of the animation. */
+ public void fastFinish() {
+ if (isUninitialized()) return;
+ Runnable fastFinishRunnable = mFastFinishRunnable;
+ if (fastFinishRunnable != null) {
+ fastFinishRunnable.run();
+ }
+ Runnable endRunnable = mEndRunnable;
+ mEndRunnable = null;
+ if (endRunnable != null) {
+ endRunnable.run();
+ }
+ }
+
+ private void init(DragLayer dragLayer, LauncherAppWidgetHostView originalView,
+ RectF widgetBackgroundPosition, Rect windowTargetBounds, float windowCornerRadius) {
+ mAppWidgetView = originalView;
+ mAppWidgetView.beginDeferringUpdates();
+ mBackgroundPosition = widgetBackgroundPosition;
+ mEndRunnable = () -> finish(dragLayer);
+
+ mAppWidgetBackgroundView = RoundedCornerEnforcement.findBackground(mAppWidgetView);
+ if (mAppWidgetBackgroundView == null) {
+ mAppWidgetBackgroundView = mAppWidgetView;
+ }
+
+ getRelativePosition(mAppWidgetBackgroundView, dragLayer, mBackgroundPosition);
+ getRelativePosition(mAppWidgetBackgroundView, mAppWidgetView, mBackgroundOffset);
+ mBackgroundView.init(mAppWidgetView, mAppWidgetBackgroundView, windowCornerRadius);
+ // Layout call before GhostView creation so that the overlaid view isn't clipped
+ layout(0, 0, windowTargetBounds.width(), windowTargetBounds.height());
+ mForegroundOverlayView = GhostView.addGhost(mAppWidgetView, this);
+ positionViews();
+
+ mListenerView.setListener(this::fastFinish);
+ dragLayer.addView(mListenerView);
+ }
+
+ /**
+ * Updates the position and opacity of the floating widget's components.
+ *
+ * @param backgroundPosition the new position of the widget's background relative to the
+ * {@link FloatingWidgetView}'s parent
+ * @param floatingWidgetAlpha the overall opacity of the {@link FloatingWidgetView}
+ * @param foregroundAlpha the opacity of the foreground layer
+ * @param fallbackBackgroundAlpha the opacity of the fallback background used when the App
+ * Widget doesn't have a background
+ * @param cornerRadiusProgress progress of the corner radius animation, where 0 is the
+ * original radius and 1 is the window radius
+ */
+ public void update(RectF backgroundPosition, float floatingWidgetAlpha, float foregroundAlpha,
+ float fallbackBackgroundAlpha, float cornerRadiusProgress) {
+ if (isUninitialized()) return;
+ setAlpha(floatingWidgetAlpha);
+ mBackgroundView.update(cornerRadiusProgress, fallbackBackgroundAlpha);
+ mAppWidgetView.setAlpha(foregroundAlpha);
+ mBackgroundPosition = backgroundPosition;
+ positionViews();
+ }
+
+ /** Sets the layout parameters of the floating view and its background view child. */
+ private void positionViews() {
+ LayoutParams layoutParams = (LayoutParams) getLayoutParams();
+ layoutParams.setMargins(0, 0, 0, 0);
+ setLayoutParams(layoutParams);
+
+ // FloatingWidgetView layout is forced LTR
+ mBackgroundView.setTranslationX(mBackgroundPosition.left);
+ mBackgroundView.setTranslationY(mBackgroundPosition.top);
+ LayoutParams backgroundParams = (LayoutParams) mBackgroundView.getLayoutParams();
+ backgroundParams.leftMargin = 0;
+ backgroundParams.topMargin = 0;
+ backgroundParams.width = (int) mBackgroundPosition.width();
+ backgroundParams.height = (int) mBackgroundPosition.height();
+ mBackgroundView.setLayoutParams(backgroundParams);
+
+ sTmpMatrix.reset();
+ float foregroundScale = mBackgroundPosition.width() / mAppWidgetBackgroundView.getWidth();
+ sTmpMatrix.setTranslate(-mBackgroundOffset.left - mAppWidgetView.getLeft(),
+ -mBackgroundOffset.top - mAppWidgetView.getTop());
+ sTmpMatrix.postScale(foregroundScale, foregroundScale);
+ sTmpMatrix.postTranslate(mBackgroundPosition.left, mBackgroundPosition.top);
+ mForegroundOverlayView.setMatrix(sTmpMatrix);
+ }
+
+ private void finish(DragLayer dragLayer) {
+ mAppWidgetView.setAlpha(1f);
+ GhostView.removeGhost(mAppWidgetView);
+ ((ViewGroup) dragLayer.getParent()).removeView(this);
+ dragLayer.removeView(mListenerView);
+ mBackgroundView.finish();
+ mAppWidgetView.endDeferringUpdates();
+ recycle();
+ mLauncher.getViewCache().recycleView(R.layout.floating_widget_view, this);
+ }
+
+ public float getInitialCornerRadius() {
+ return mBackgroundView.getMaximumRadius();
+ }
+
+ private boolean isUninitialized() {
+ return mForegroundOverlayView == null;
+ }
+
+ private void recycle() {
+ mEndRunnable = null;
+ mFastFinishRunnable = null;
+ mBackgroundPosition = null;
+ mListenerView.setListener(null);
+ mAppWidgetView = null;
+ mForegroundOverlayView = null;
+ mAppWidgetBackgroundView = null;
+ mBackgroundView.recycle();
+ }
+
+ /**
+ * Configures and returns a an instance of {@link FloatingWidgetView} matching the appearance of
+ * {@param originalView}.
+ *
+ * @param widgetBackgroundPosition a {@link RectF} that will be updated with the widget's
+ * background bounds
+ * @param windowTargetBounds the bounds of the window when launched
+ * @param windowCornerRadius the corner radius of the window
+ */
+ public static FloatingWidgetView getFloatingWidgetView(Launcher launcher,
+ LauncherAppWidgetHostView originalView, RectF widgetBackgroundPosition,
+ Rect windowTargetBounds, float windowCornerRadius) {
+ final DragLayer dragLayer = launcher.getDragLayer();
+ ViewGroup parent = (ViewGroup) dragLayer.getParent();
+ FloatingWidgetView floatingView =
+ launcher.getViewCache().getView(R.layout.floating_widget_view, launcher, parent);
+ floatingView.recycle();
+
+ floatingView.init(dragLayer, originalView, widgetBackgroundPosition, windowTargetBounds,
+ windowCornerRadius);
+ parent.addView(floatingView);
+ return floatingView;
+ }
+
+ private static void getRelativePosition(View descendant, View ancestor, RectF position) {
+ float[] points = new float[]{0, 0, descendant.getWidth(), descendant.getHeight()};
+ Utilities.getDescendantCoordRelativeToAncestor(descendant, ancestor, points,
+ false /* includeRootScroll */);
+ position.set(
+ Math.min(points[0], points[2]),
+ Math.min(points[1], points[3]),
+ Math.max(points[0], points[2]),
+ Math.max(points[1], points[3]));
+ }
+}
diff --git a/res/layout/floating_widget_view.xml b/res/layout/floating_widget_view.xml
new file mode 100644
index 0000000..eea7a92
--- /dev/null
+++ b/res/layout/floating_widget_view.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 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.
+-->
+<com.android.quickstep.views.FloatingWidgetView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layoutDirection="ltr" />
diff --git a/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java b/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java
index f77c740..620604a 100644
--- a/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java
+++ b/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java
@@ -72,6 +72,8 @@
// Maintains a list of widget ids which are supposed to be auto advanced.
private static final SparseBooleanArray sAutoAdvanceWidgetIds = new SparseBooleanArray();
+ // Maximum duration for which updates can be deferred.
+ private static final long UPDATE_LOCK_TIMEOUT_MILLIS = 1000;
protected final LayoutInflater mInflater;
@@ -110,6 +112,9 @@
}
}
};
+ private final Object mUpdateLock = new Object();
+ private long mDeferUpdatesUntilMillis = 0;
+ private RemoteViews mMostRecentRemoteViews;
public LauncherAppWidgetHostView(Context context) {
super(context);
@@ -165,6 +170,11 @@
@Override
public void updateAppWidget(RemoteViews remoteViews) {
+ synchronized (mUpdateLock) {
+ mMostRecentRemoteViews = remoteViews;
+ if (SystemClock.uptimeMillis() < mDeferUpdatesUntilMillis) return;
+ }
+
super.updateAppWidget(remoteViews);
// The provider info or the views might have changed.
@@ -198,6 +208,34 @@
return false;
}
+ /**
+ * Begin deferring the application of any {@link RemoteViews} updates made through
+ * {@link #updateAppWidget(RemoteViews)} until {@link #endDeferringUpdates()} has been called or
+ * the next {@link #updateAppWidget(RemoteViews)} call after {@link #UPDATE_LOCK_TIMEOUT_MILLIS}
+ * have elapsed.
+ */
+ public void beginDeferringUpdates() {
+ synchronized (mUpdateLock) {
+ mDeferUpdatesUntilMillis = SystemClock.uptimeMillis() + UPDATE_LOCK_TIMEOUT_MILLIS;
+ }
+ }
+
+ /**
+ * Stop deferring the application of {@link RemoteViews} updates made through
+ * {@link #updateAppWidget(RemoteViews)} and apply the most recently received update.
+ */
+ public void endDeferringUpdates() {
+ RemoteViews remoteViews;
+ synchronized (mUpdateLock) {
+ mDeferUpdatesUntilMillis = 0;
+ remoteViews = mMostRecentRemoteViews;
+ mMostRecentRemoteViews = null;
+ }
+ if (remoteViews != null) {
+ updateAppWidget(remoteViews);
+ }
+ }
+
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
DragLayer dragLayer = mLauncher.getDragLayer();