diff --git a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java index 5b1b59b601..1a464b14b2 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; 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.MultiValueUpdateListener; 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 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener 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 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener } } + 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 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener } }); + 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 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener } }); } - - animatorSet.playTogether(appAnimator, backgroundRadiusAnim); - return animatorSet; + return backgroundRadiusAnim; } /** @@ -1120,9 +1252,13 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener 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 0000000000..f74aa55cbd --- /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 0000000000..d23884c9ef --- /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 0000000000..eea7a9220c --- /dev/null +++ b/res/layout/floating_widget_view.xml @@ -0,0 +1,20 @@ + + + diff --git a/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java b/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java index f77c740032..620604a4d1 100644 --- a/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java +++ b/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java @@ -72,6 +72,8 @@ public class LauncherAppWidgetHostView extends NavigableAppWidgetHostView // 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 @@ public class LauncherAppWidgetHostView extends NavigableAppWidgetHostView } } }; + private final Object mUpdateLock = new Object(); + private long mDeferUpdatesUntilMillis = 0; + private RemoteViews mMostRecentRemoteViews; public LauncherAppWidgetHostView(Context context) { super(context); @@ -165,6 +170,11 @@ public class LauncherAppWidgetHostView extends NavigableAppWidgetHostView @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 @@ public class LauncherAppWidgetHostView extends NavigableAppWidgetHostView 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();