/* * 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.wm.shell.draganddrop; import static com.android.wm.shell.animation.Interpolators.FAST_OUT_SLOW_IN; import android.animation.Animator; import android.animation.ObjectAnimator; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Path; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.util.FloatProperty; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.ImageView; import androidx.annotation.Nullable; import com.android.internal.policy.ScreenDecorationsUtils; import com.android.wm.shell.R; /** * Renders a drop zone area for items being dragged. */ public class DropZoneView extends FrameLayout { private static final float SPLASHSCREEN_ALPHA = 0.90f; private static final float HIGHLIGHT_ALPHA = 1f; private static final int MARGIN_ANIMATION_ENTER_DURATION = 400; private static final int MARGIN_ANIMATION_EXIT_DURATION = 250; private static final FloatProperty INSETS = new FloatProperty("insets") { @Override public void setValue(DropZoneView v, float percent) { v.setMarginPercent(percent); } @Override public Float get(DropZoneView v) { return v.getMarginPercent(); } }; private final Path mPath = new Path(); private final float[] mContainerMargin = new float[4]; private float mCornerRadius; private float mBottomInset; private boolean mIgnoreBottomMargin; private int mMarginColor; // i.e. color used for negative space like the container insets private boolean mShowingHighlight; private boolean mShowingSplash; private boolean mShowingMargin; private int mSplashScreenColor; private int mHighlightColor; private ObjectAnimator mBackgroundAnimator; private ObjectAnimator mMarginAnimator; private float mMarginPercent; // Renders a highlight or neutral transparent color private ColorDrawable mColorDrawable; // Renders the translucent splashscreen with the app icon in the middle private ImageView mSplashScreenView; // Renders the margin / insets around the dropzone container private MarginView mMarginView; public DropZoneView(Context context) { this(context, null); } public DropZoneView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public DropZoneView(Context context, AttributeSet attrs, int defStyleAttr) { this(context, attrs, defStyleAttr, 0); } public DropZoneView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); setContainerMargin(0, 0, 0, 0); // make sure it's populated mCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context); mMarginColor = getResources().getColor(R.color.taskbar_background); int c = getResources().getColor(android.R.color.system_accent1_500); mHighlightColor = Color.argb(HIGHLIGHT_ALPHA, Color.red(c), Color.green(c), Color.blue(c)); mSplashScreenColor = Color.argb(SPLASHSCREEN_ALPHA, 0, 0, 0); mColorDrawable = new ColorDrawable(); setBackgroundDrawable(mColorDrawable); final int iconSize = context.getResources().getDimensionPixelSize(R.dimen.split_icon_size); mSplashScreenView = new ImageView(context); mSplashScreenView.setScaleType(ImageView.ScaleType.FIT_CENTER); addView(mSplashScreenView, new FrameLayout.LayoutParams(iconSize, iconSize, Gravity.CENTER)); mSplashScreenView.setAlpha(0f); mMarginView = new MarginView(context); addView(mMarginView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); } public void onThemeChange() { mCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(getContext()); mMarginColor = getResources().getColor(R.color.taskbar_background); mHighlightColor = getResources().getColor(android.R.color.system_accent1_500); if (mMarginPercent > 0) { mMarginView.invalidate(); } } /** Sets the desired margins around the drop zone container when fully showing. */ public void setContainerMargin(float left, float top, float right, float bottom) { mContainerMargin[0] = left; mContainerMargin[1] = top; mContainerMargin[2] = right; mContainerMargin[3] = bottom; if (mMarginPercent > 0) { mMarginView.invalidate(); } } /** Ignores the bottom margin provided by the insets. */ public void setForceIgnoreBottomMargin(boolean ignoreBottomMargin) { mIgnoreBottomMargin = ignoreBottomMargin; if (mMarginPercent > 0) { mMarginView.invalidate(); } } /** Sets the bottom inset so the drop zones are above bottom navigation. */ public void setBottomInset(float bottom) { mBottomInset = bottom; ((LayoutParams) mSplashScreenView.getLayoutParams()).bottomMargin = (int) bottom; if (mMarginPercent > 0) { mMarginView.invalidate(); } } /** Sets the color and icon to use for the splashscreen when shown. */ public void setAppInfo(int color, Drawable appIcon) { Color c = Color.valueOf(color); mSplashScreenColor = Color.argb(SPLASHSCREEN_ALPHA, c.red(), c.green(), c.blue()); mSplashScreenView.setImageDrawable(appIcon); } /** @return an active animator for this view if one exists. */ @Nullable public Animator getAnimator() { if (mMarginAnimator != null && mMarginAnimator.isRunning()) { return mMarginAnimator; } else if (mBackgroundAnimator != null && mBackgroundAnimator.isRunning()) { return mBackgroundAnimator; } return null; } /** Animates between highlight and splashscreen depending on current state. */ public void animateSwitch() { mShowingHighlight = !mShowingHighlight; mShowingSplash = !mShowingHighlight; final int newColor = mShowingHighlight ? mHighlightColor : mSplashScreenColor; animateBackground(mColorDrawable.getColor(), newColor); animateSplashScreenIcon(); } /** Animates the highlight indicating the zone is hovered on or not. */ public void setShowingHighlight(boolean showingHighlight) { mShowingHighlight = showingHighlight; mShowingSplash = !mShowingHighlight; final int newColor = mShowingHighlight ? mHighlightColor : mSplashScreenColor; animateBackground(Color.TRANSPARENT, newColor); animateSplashScreenIcon(); } /** Animates the margins around the drop zone to show or hide. */ public void setShowingMargin(boolean visible) { if (mShowingMargin != visible) { mShowingMargin = visible; animateMarginToState(); } if (!mShowingMargin) { mShowingHighlight = false; mShowingSplash = false; animateBackground(mColorDrawable.getColor(), Color.TRANSPARENT); animateSplashScreenIcon(); } } private void animateBackground(int startColor, int endColor) { if (mBackgroundAnimator != null) { mBackgroundAnimator.cancel(); } mBackgroundAnimator = ObjectAnimator.ofArgb(mColorDrawable, "color", startColor, endColor); if (!mShowingSplash && !mShowingHighlight) { mBackgroundAnimator.setInterpolator(FAST_OUT_SLOW_IN); } mBackgroundAnimator.start(); } private void animateSplashScreenIcon() { mSplashScreenView.animate().alpha(mShowingSplash ? 1f : 0f).start(); } private void animateMarginToState() { if (mMarginAnimator != null) { mMarginAnimator.cancel(); } mMarginAnimator = ObjectAnimator.ofFloat(this, INSETS, mMarginPercent, mShowingMargin ? 1f : 0f); mMarginAnimator.setInterpolator(FAST_OUT_SLOW_IN); mMarginAnimator.setDuration(mShowingMargin ? MARGIN_ANIMATION_ENTER_DURATION : MARGIN_ANIMATION_EXIT_DURATION); mMarginAnimator.start(); } private void setMarginPercent(float percent) { if (percent != mMarginPercent) { mMarginPercent = percent; mMarginView.invalidate(); } } private float getMarginPercent() { return mMarginPercent; } /** Simple view that draws a rounded rect margin around its contents. **/ private class MarginView extends View { MarginView(Context context) { super(context); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); mPath.reset(); mPath.addRoundRect(mContainerMargin[0] * mMarginPercent, mContainerMargin[1] * mMarginPercent, getWidth() - (mContainerMargin[2] * mMarginPercent), getHeight() - (mContainerMargin[3] * mMarginPercent) - (mIgnoreBottomMargin ? 0 : mBottomInset), mCornerRadius * mMarginPercent, mCornerRadius * mMarginPercent, Path.Direction.CW); mPath.setFillType(Path.FillType.INVERSE_EVEN_ODD); canvas.clipPath(mPath); canvas.drawColor(mMarginColor); } } }