/* * 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 androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.launcher3.R; 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(); @Nullable private Drawable mOriginalForeground; @Nullable private Drawable mOriginalBackground; private float mFinalRadius; private float mInitialOutlineRadius; private float mOutlineRadius; private boolean mIsUsingFallback; private View mSourceView; FloatingWidgetBackgroundView(Context context, @Nullable 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, @NonNull View backgroundView, float finalRadius, int fallbackBackgroundColor) { mFinalRadius = finalRadius; mSourceView = backgroundView; mInitialOutlineRadius = getOutlineRadius(hostView, backgroundView); mIsUsingFallback = false; if (isSupportedDrawable(backgroundView.getForeground())) { if (backgroundView.getTag(R.id.saved_floating_widget_foreground) == null) { mOriginalForeground = backgroundView.getForeground(); backgroundView.setTag(R.id.saved_floating_widget_foreground, mOriginalForeground); } else { mOriginalForeground = (Drawable) backgroundView.getTag( R.id.saved_floating_widget_foreground); } mForegroundProperties.init( mOriginalForeground.getConstantState().newDrawable().mutate()); setForeground(mForegroundProperties.mDrawable); Drawable clipPlaceholder = mOriginalForeground.getConstantState().newDrawable().mutate(); clipPlaceholder.setAlpha(0); mSourceView.setForeground(clipPlaceholder); } if (isSupportedDrawable(backgroundView.getBackground())) { if (backgroundView.getTag(R.id.saved_floating_widget_background) == null) { mOriginalBackground = backgroundView.getBackground(); backgroundView.setTag(R.id.saved_floating_widget_background, mOriginalBackground); } else { mOriginalBackground = (Drawable) backgroundView.getTag( R.id.saved_floating_widget_background); } mBackgroundProperties.init( mOriginalBackground.getConstantState().newDrawable().mutate()); setBackground(mBackgroundProperties.mDrawable); Drawable clipPlaceholder = mOriginalBackground.getConstantState().newDrawable().mutate(); clipPlaceholder.setAlpha(0); mSourceView.setBackground(clipPlaceholder); } else if (mOriginalForeground == null) { mFallbackDrawable.setColor(fallbackBackgroundColor); 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; if (mOriginalForeground != null) mSourceView.setForeground(mOriginalForeground); if (mOriginalBackground != null) mSourceView.setBackground(mOriginalBackground); } void recycle() { if (mSourceView != null) { mSourceView.setTag(R.id.saved_floating_widget_foreground, null); mSourceView.setTag(R.id.saved_floating_widget_background, null); } 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(@Nullable 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 { @Nullable private Drawable mDrawable; private float mOriginalRadius; @Nullable 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); } } } }