1 /* 2 * Copyright (C) 2021 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.wm.shell.draganddrop; 18 19 import static com.android.wm.shell.animation.Interpolators.FAST_OUT_SLOW_IN; 20 21 import android.animation.Animator; 22 import android.animation.ObjectAnimator; 23 import android.content.Context; 24 import android.graphics.Canvas; 25 import android.graphics.Color; 26 import android.graphics.Path; 27 import android.graphics.drawable.ColorDrawable; 28 import android.graphics.drawable.Drawable; 29 import android.util.AttributeSet; 30 import android.util.FloatProperty; 31 import android.view.Gravity; 32 import android.view.View; 33 import android.view.ViewGroup; 34 import android.widget.FrameLayout; 35 import android.widget.ImageView; 36 37 import androidx.annotation.Nullable; 38 39 import com.android.internal.policy.ScreenDecorationsUtils; 40 import com.android.wm.shell.R; 41 42 /** 43 * Renders a drop zone area for items being dragged. 44 */ 45 public class DropZoneView extends FrameLayout { 46 47 private static final float SPLASHSCREEN_ALPHA = 0.90f; 48 private static final float HIGHLIGHT_ALPHA = 1f; 49 private static final int MARGIN_ANIMATION_ENTER_DURATION = 400; 50 private static final int MARGIN_ANIMATION_EXIT_DURATION = 250; 51 52 private static final FloatProperty<DropZoneView> INSETS = 53 new FloatProperty<DropZoneView>("insets") { 54 @Override 55 public void setValue(DropZoneView v, float percent) { 56 v.setMarginPercent(percent); 57 } 58 59 @Override 60 public Float get(DropZoneView v) { 61 return v.getMarginPercent(); 62 } 63 }; 64 65 private final Path mPath = new Path(); 66 private final float[] mContainerMargin = new float[4]; 67 private float mCornerRadius; 68 private float mBottomInset; 69 private boolean mIgnoreBottomMargin; 70 private int mMarginColor; // i.e. color used for negative space like the container insets 71 72 private boolean mShowingHighlight; 73 private boolean mShowingSplash; 74 private boolean mShowingMargin; 75 76 private int mSplashScreenColor; 77 private int mHighlightColor; 78 79 private ObjectAnimator mBackgroundAnimator; 80 private ObjectAnimator mMarginAnimator; 81 private float mMarginPercent; 82 83 // Renders a highlight or neutral transparent color 84 private ColorDrawable mColorDrawable; 85 // Renders the translucent splashscreen with the app icon in the middle 86 private ImageView mSplashScreenView; 87 // Renders the margin / insets around the dropzone container 88 private MarginView mMarginView; 89 DropZoneView(Context context)90 public DropZoneView(Context context) { 91 this(context, null); 92 } 93 DropZoneView(Context context, AttributeSet attrs)94 public DropZoneView(Context context, AttributeSet attrs) { 95 this(context, attrs, 0); 96 } 97 DropZoneView(Context context, AttributeSet attrs, int defStyleAttr)98 public DropZoneView(Context context, AttributeSet attrs, int defStyleAttr) { 99 this(context, attrs, defStyleAttr, 0); 100 } 101 DropZoneView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)102 public DropZoneView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 103 super(context, attrs, defStyleAttr, defStyleRes); 104 setContainerMargin(0, 0, 0, 0); // make sure it's populated 105 106 mCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context); 107 mMarginColor = getResources().getColor(R.color.taskbar_background); 108 int c = getResources().getColor(android.R.color.system_accent1_500); 109 mHighlightColor = Color.argb(HIGHLIGHT_ALPHA, Color.red(c), Color.green(c), Color.blue(c)); 110 mSplashScreenColor = Color.argb(SPLASHSCREEN_ALPHA, 0, 0, 0); 111 mColorDrawable = new ColorDrawable(); 112 setBackgroundDrawable(mColorDrawable); 113 114 final int iconSize = context.getResources().getDimensionPixelSize(R.dimen.split_icon_size); 115 mSplashScreenView = new ImageView(context); 116 mSplashScreenView.setScaleType(ImageView.ScaleType.FIT_CENTER); 117 addView(mSplashScreenView, 118 new FrameLayout.LayoutParams(iconSize, iconSize, Gravity.CENTER)); 119 mSplashScreenView.setAlpha(0f); 120 121 mMarginView = new MarginView(context); 122 addView(mMarginView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 123 ViewGroup.LayoutParams.MATCH_PARENT)); 124 } 125 onThemeChange()126 public void onThemeChange() { 127 mCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(getContext()); 128 mMarginColor = getResources().getColor(R.color.taskbar_background); 129 mHighlightColor = getResources().getColor(android.R.color.system_accent1_500); 130 131 if (mMarginPercent > 0) { 132 mMarginView.invalidate(); 133 } 134 } 135 136 /** Sets the desired margins around the drop zone container when fully showing. */ setContainerMargin(float left, float top, float right, float bottom)137 public void setContainerMargin(float left, float top, float right, float bottom) { 138 mContainerMargin[0] = left; 139 mContainerMargin[1] = top; 140 mContainerMargin[2] = right; 141 mContainerMargin[3] = bottom; 142 if (mMarginPercent > 0) { 143 mMarginView.invalidate(); 144 } 145 } 146 147 /** Ignores the bottom margin provided by the insets. */ setForceIgnoreBottomMargin(boolean ignoreBottomMargin)148 public void setForceIgnoreBottomMargin(boolean ignoreBottomMargin) { 149 mIgnoreBottomMargin = ignoreBottomMargin; 150 if (mMarginPercent > 0) { 151 mMarginView.invalidate(); 152 } 153 } 154 155 /** Sets the bottom inset so the drop zones are above bottom navigation. */ setBottomInset(float bottom)156 public void setBottomInset(float bottom) { 157 mBottomInset = bottom; 158 ((LayoutParams) mSplashScreenView.getLayoutParams()).bottomMargin = (int) bottom; 159 if (mMarginPercent > 0) { 160 mMarginView.invalidate(); 161 } 162 } 163 164 /** Sets the color and icon to use for the splashscreen when shown. */ setAppInfo(int color, Drawable appIcon)165 public void setAppInfo(int color, Drawable appIcon) { 166 Color c = Color.valueOf(color); 167 mSplashScreenColor = Color.argb(SPLASHSCREEN_ALPHA, c.red(), c.green(), c.blue()); 168 mSplashScreenView.setImageDrawable(appIcon); 169 } 170 171 /** @return an active animator for this view if one exists. */ 172 @Nullable getAnimator()173 public Animator getAnimator() { 174 if (mMarginAnimator != null && mMarginAnimator.isRunning()) { 175 return mMarginAnimator; 176 } else if (mBackgroundAnimator != null && mBackgroundAnimator.isRunning()) { 177 return mBackgroundAnimator; 178 } 179 return null; 180 } 181 182 /** Animates between highlight and splashscreen depending on current state. */ animateSwitch()183 public void animateSwitch() { 184 mShowingHighlight = !mShowingHighlight; 185 mShowingSplash = !mShowingHighlight; 186 final int newColor = mShowingHighlight ? mHighlightColor : mSplashScreenColor; 187 animateBackground(mColorDrawable.getColor(), newColor); 188 animateSplashScreenIcon(); 189 } 190 191 /** Animates the highlight indicating the zone is hovered on or not. */ setShowingHighlight(boolean showingHighlight)192 public void setShowingHighlight(boolean showingHighlight) { 193 mShowingHighlight = showingHighlight; 194 mShowingSplash = !mShowingHighlight; 195 final int newColor = mShowingHighlight ? mHighlightColor : mSplashScreenColor; 196 animateBackground(Color.TRANSPARENT, newColor); 197 animateSplashScreenIcon(); 198 } 199 200 /** Animates the margins around the drop zone to show or hide. */ setShowingMargin(boolean visible)201 public void setShowingMargin(boolean visible) { 202 if (mShowingMargin != visible) { 203 mShowingMargin = visible; 204 animateMarginToState(); 205 } 206 if (!mShowingMargin) { 207 mShowingHighlight = false; 208 mShowingSplash = false; 209 animateBackground(mColorDrawable.getColor(), Color.TRANSPARENT); 210 animateSplashScreenIcon(); 211 } 212 } 213 animateBackground(int startColor, int endColor)214 private void animateBackground(int startColor, int endColor) { 215 if (mBackgroundAnimator != null) { 216 mBackgroundAnimator.cancel(); 217 } 218 mBackgroundAnimator = ObjectAnimator.ofArgb(mColorDrawable, 219 "color", 220 startColor, 221 endColor); 222 if (!mShowingSplash && !mShowingHighlight) { 223 mBackgroundAnimator.setInterpolator(FAST_OUT_SLOW_IN); 224 } 225 mBackgroundAnimator.start(); 226 } 227 animateSplashScreenIcon()228 private void animateSplashScreenIcon() { 229 mSplashScreenView.animate().alpha(mShowingSplash ? 1f : 0f).start(); 230 } 231 animateMarginToState()232 private void animateMarginToState() { 233 if (mMarginAnimator != null) { 234 mMarginAnimator.cancel(); 235 } 236 mMarginAnimator = ObjectAnimator.ofFloat(this, INSETS, 237 mMarginPercent, 238 mShowingMargin ? 1f : 0f); 239 mMarginAnimator.setInterpolator(FAST_OUT_SLOW_IN); 240 mMarginAnimator.setDuration(mShowingMargin 241 ? MARGIN_ANIMATION_ENTER_DURATION 242 : MARGIN_ANIMATION_EXIT_DURATION); 243 mMarginAnimator.start(); 244 } 245 setMarginPercent(float percent)246 private void setMarginPercent(float percent) { 247 if (percent != mMarginPercent) { 248 mMarginPercent = percent; 249 mMarginView.invalidate(); 250 } 251 } 252 getMarginPercent()253 private float getMarginPercent() { 254 return mMarginPercent; 255 } 256 257 /** Simple view that draws a rounded rect margin around its contents. **/ 258 private class MarginView extends View { 259 MarginView(Context context)260 MarginView(Context context) { 261 super(context); 262 } 263 264 @Override onDraw(Canvas canvas)265 protected void onDraw(Canvas canvas) { 266 super.onDraw(canvas); 267 mPath.reset(); 268 mPath.addRoundRect(mContainerMargin[0] * mMarginPercent, 269 mContainerMargin[1] * mMarginPercent, 270 getWidth() - (mContainerMargin[2] * mMarginPercent), 271 getHeight() - (mContainerMargin[3] * mMarginPercent) 272 - (mIgnoreBottomMargin ? 0 : mBottomInset), 273 mCornerRadius * mMarginPercent, 274 mCornerRadius * mMarginPercent, 275 Path.Direction.CW); 276 mPath.setFillType(Path.FillType.INVERSE_EVEN_ODD); 277 canvas.clipPath(mPath); 278 canvas.drawColor(mMarginColor); 279 } 280 } 281 } 282