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