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.ObjectAnimator; 22 import android.content.Context; 23 import android.graphics.Canvas; 24 import android.graphics.Color; 25 import android.graphics.Path; 26 import android.graphics.drawable.ColorDrawable; 27 import android.graphics.drawable.Drawable; 28 import android.util.AttributeSet; 29 import android.util.FloatProperty; 30 import android.util.IntProperty; 31 import android.view.View; 32 import android.view.ViewGroup; 33 import android.widget.FrameLayout; 34 import android.widget.ImageView; 35 36 import androidx.annotation.Nullable; 37 38 import com.android.internal.policy.ScreenDecorationsUtils; 39 import com.android.wm.shell.R; 40 41 /** 42 * Renders a drop zone area for items being dragged. 43 */ 44 public class DropZoneView extends FrameLayout { 45 46 private static final int SPLASHSCREEN_ALPHA_INT = (int) (255 * 0.90f); 47 private static final int HIGHLIGHT_ALPHA_INT = 255; 48 private static final int MARGIN_ANIMATION_ENTER_DURATION = 400; 49 private static final int MARGIN_ANIMATION_EXIT_DURATION = 250; 50 51 private static final FloatProperty<DropZoneView> INSETS = 52 new FloatProperty<DropZoneView>("insets") { 53 @Override 54 public void setValue(DropZoneView v, float percent) { 55 v.setMarginPercent(percent); 56 } 57 58 @Override 59 public Float get(DropZoneView v) { 60 return v.getMarginPercent(); 61 } 62 }; 63 64 private static final IntProperty<ColorDrawable> SPLASHSCREEN_ALPHA = 65 new IntProperty<ColorDrawable>("splashscreen") { 66 @Override 67 public void setValue(ColorDrawable d, int alpha) { 68 d.setAlpha(alpha); 69 } 70 71 @Override 72 public Integer get(ColorDrawable d) { 73 return d.getAlpha(); 74 } 75 }; 76 77 private static final IntProperty<ColorDrawable> HIGHLIGHT_ALPHA = 78 new IntProperty<ColorDrawable>("highlight") { 79 @Override 80 public void setValue(ColorDrawable d, int alpha) { 81 d.setAlpha(alpha); 82 } 83 84 @Override 85 public Integer get(ColorDrawable d) { 86 return d.getAlpha(); 87 } 88 }; 89 90 private final Path mPath = new Path(); 91 private final float[] mContainerMargin = new float[4]; 92 private float mCornerRadius; 93 private float mBottomInset; 94 private int mMarginColor; // i.e. color used for negative space like the container insets 95 private int mHighlightColor; 96 97 private boolean mShowingHighlight; 98 private boolean mShowingSplash; 99 private boolean mShowingMargin; 100 101 // TODO: might be more seamless to animate between splash/highlight color instead of 2 separate 102 private ObjectAnimator mSplashAnimator; 103 private ObjectAnimator mHighlightAnimator; 104 private ObjectAnimator mMarginAnimator; 105 private float mMarginPercent; 106 107 // Renders a highlight or neutral transparent color 108 private ColorDrawable mDropZoneDrawable; 109 // Renders the translucent splashscreen with the app icon in the middle 110 private ImageView mSplashScreenView; 111 private ColorDrawable mSplashBackgroundDrawable; 112 // Renders the margin / insets around the dropzone container 113 private MarginView mMarginView; 114 DropZoneView(Context context)115 public DropZoneView(Context context) { 116 this(context, null); 117 } 118 DropZoneView(Context context, AttributeSet attrs)119 public DropZoneView(Context context, AttributeSet attrs) { 120 this(context, attrs, 0); 121 } 122 DropZoneView(Context context, AttributeSet attrs, int defStyleAttr)123 public DropZoneView(Context context, AttributeSet attrs, int defStyleAttr) { 124 this(context, attrs, defStyleAttr, 0); 125 } 126 DropZoneView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)127 public DropZoneView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 128 super(context, attrs, defStyleAttr, defStyleRes); 129 setContainerMargin(0, 0, 0, 0); // make sure it's populated 130 131 mCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context); 132 mMarginColor = getResources().getColor(R.color.taskbar_background); 133 mHighlightColor = getResources().getColor(android.R.color.system_accent1_500); 134 135 mDropZoneDrawable = new ColorDrawable(); 136 mDropZoneDrawable.setColor(mHighlightColor); 137 mDropZoneDrawable.setAlpha(0); 138 setBackgroundDrawable(mDropZoneDrawable); 139 140 mSplashScreenView = new ImageView(context); 141 mSplashScreenView.setScaleType(ImageView.ScaleType.CENTER); 142 mSplashBackgroundDrawable = new ColorDrawable(); 143 mSplashBackgroundDrawable.setColor(Color.WHITE); 144 mSplashBackgroundDrawable.setAlpha(SPLASHSCREEN_ALPHA_INT); 145 mSplashScreenView.setBackgroundDrawable(mSplashBackgroundDrawable); 146 addView(mSplashScreenView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 147 ViewGroup.LayoutParams.MATCH_PARENT)); 148 mSplashScreenView.setAlpha(0f); 149 150 mMarginView = new MarginView(context); 151 addView(mMarginView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 152 ViewGroup.LayoutParams.MATCH_PARENT)); 153 } 154 onThemeChange()155 public void onThemeChange() { 156 mCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(getContext()); 157 mMarginColor = getResources().getColor(R.color.taskbar_background); 158 mHighlightColor = getResources().getColor(android.R.color.system_accent1_500); 159 160 final int alpha = mDropZoneDrawable.getAlpha(); 161 mDropZoneDrawable.setColor(mHighlightColor); 162 mDropZoneDrawable.setAlpha(alpha); 163 164 if (mMarginPercent > 0) { 165 mMarginView.invalidate(); 166 } 167 } 168 169 /** Sets the desired margins around the drop zone container when fully showing. */ setContainerMargin(float left, float top, float right, float bottom)170 public void setContainerMargin(float left, float top, float right, float bottom) { 171 mContainerMargin[0] = left; 172 mContainerMargin[1] = top; 173 mContainerMargin[2] = right; 174 mContainerMargin[3] = bottom; 175 if (mMarginPercent > 0) { 176 mMarginView.invalidate(); 177 } 178 } 179 180 /** Sets the bottom inset so the drop zones are above bottom navigation. */ setBottomInset(float bottom)181 public void setBottomInset(float bottom) { 182 mBottomInset = bottom; 183 ((LayoutParams) mSplashScreenView.getLayoutParams()).bottomMargin = (int) bottom; 184 if (mMarginPercent > 0) { 185 mMarginView.invalidate(); 186 } 187 } 188 189 /** Sets the color and icon to use for the splashscreen when shown. */ setAppInfo(int splashScreenColor, Drawable appIcon)190 public void setAppInfo(int splashScreenColor, Drawable appIcon) { 191 mSplashBackgroundDrawable.setColor(splashScreenColor); 192 mSplashScreenView.setImageDrawable(appIcon); 193 } 194 195 /** @return an active animator for this view if one exists. */ 196 @Nullable getAnimator()197 public ObjectAnimator getAnimator() { 198 if (mMarginAnimator != null && mMarginAnimator.isRunning()) { 199 return mMarginAnimator; 200 } else if (mHighlightAnimator != null && mHighlightAnimator.isRunning()) { 201 return mHighlightAnimator; 202 } else if (mSplashAnimator != null && mSplashAnimator.isRunning()) { 203 return mSplashAnimator; 204 } 205 return null; 206 } 207 208 /** Animates the splashscreen to show or hide. */ setShowingSplash(boolean showingSplash)209 public void setShowingSplash(boolean showingSplash) { 210 if (mShowingSplash != showingSplash) { 211 mShowingSplash = showingSplash; 212 animateSplashToState(); 213 } 214 } 215 216 /** Animates the highlight indicating the zone is hovered on or not. */ setShowingHighlight(boolean showingHighlight)217 public void setShowingHighlight(boolean showingHighlight) { 218 if (mShowingHighlight != showingHighlight) { 219 mShowingHighlight = showingHighlight; 220 animateHighlightToState(); 221 } 222 } 223 224 /** Animates the margins around the drop zone to show or hide. */ setShowingMargin(boolean visible)225 public void setShowingMargin(boolean visible) { 226 if (mShowingMargin != visible) { 227 mShowingMargin = visible; 228 animateMarginToState(); 229 } 230 if (!mShowingMargin) { 231 setShowingHighlight(false); 232 setShowingSplash(false); 233 } 234 } 235 animateSplashToState()236 private void animateSplashToState() { 237 if (mSplashAnimator != null) { 238 mSplashAnimator.cancel(); 239 } 240 mSplashAnimator = ObjectAnimator.ofInt(mSplashBackgroundDrawable, 241 SPLASHSCREEN_ALPHA, 242 mSplashBackgroundDrawable.getAlpha(), 243 mShowingSplash ? SPLASHSCREEN_ALPHA_INT : 0); 244 if (!mShowingSplash) { 245 mSplashAnimator.setInterpolator(FAST_OUT_SLOW_IN); 246 } 247 mSplashAnimator.start(); 248 mSplashScreenView.animate().alpha(mShowingSplash ? 1f : 0f).start(); 249 } 250 animateHighlightToState()251 private void animateHighlightToState() { 252 if (mHighlightAnimator != null) { 253 mHighlightAnimator.cancel(); 254 } 255 mHighlightAnimator = ObjectAnimator.ofInt(mDropZoneDrawable, 256 HIGHLIGHT_ALPHA, 257 mDropZoneDrawable.getAlpha(), 258 mShowingHighlight ? HIGHLIGHT_ALPHA_INT : 0); 259 if (!mShowingHighlight) { 260 mHighlightAnimator.setInterpolator(FAST_OUT_SLOW_IN); 261 } 262 mHighlightAnimator.start(); 263 } 264 animateMarginToState()265 private void animateMarginToState() { 266 if (mMarginAnimator != null) { 267 mMarginAnimator.cancel(); 268 } 269 mMarginAnimator = ObjectAnimator.ofFloat(this, INSETS, 270 mMarginPercent, 271 mShowingMargin ? 1f : 0f); 272 mMarginAnimator.setInterpolator(FAST_OUT_SLOW_IN); 273 mMarginAnimator.setDuration(mShowingMargin 274 ? MARGIN_ANIMATION_ENTER_DURATION 275 : MARGIN_ANIMATION_EXIT_DURATION); 276 mMarginAnimator.start(); 277 } 278 setMarginPercent(float percent)279 private void setMarginPercent(float percent) { 280 if (percent != mMarginPercent) { 281 mMarginPercent = percent; 282 mMarginView.invalidate(); 283 } 284 } 285 getMarginPercent()286 private float getMarginPercent() { 287 return mMarginPercent; 288 } 289 290 /** Simple view that draws a rounded rect margin around its contents. **/ 291 private class MarginView extends View { 292 MarginView(Context context)293 MarginView(Context context) { 294 super(context); 295 } 296 297 @Override onDraw(Canvas canvas)298 protected void onDraw(Canvas canvas) { 299 super.onDraw(canvas); 300 mPath.reset(); 301 mPath.addRoundRect(mContainerMargin[0] * mMarginPercent, 302 mContainerMargin[1] * mMarginPercent, 303 getWidth() - (mContainerMargin[2] * mMarginPercent), 304 getHeight() - (mContainerMargin[3] * mMarginPercent) - mBottomInset, 305 mCornerRadius * mMarginPercent, 306 mCornerRadius * mMarginPercent, 307 Path.Direction.CW); 308 mPath.setFillType(Path.FillType.INVERSE_EVEN_ODD); 309 canvas.clipPath(mPath); 310 canvas.drawColor(mMarginColor); 311 } 312 } 313 } 314