| /* |
| * 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<DropZoneView> INSETS = |
| new FloatProperty<DropZoneView>("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_dark); |
| 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_dark); |
| 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); |
| } |
| } |
| } |