1 /*
2  * Copyright (C) 2017 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.systemui.recents.views;
18 
19 import static android.app.ActivityTaskManager.SPLIT_SCREEN_CREATE_MODE_BOTTOM_OR_RIGHT;
20 import static android.app.ActivityTaskManager.SPLIT_SCREEN_CREATE_MODE_TOP_OR_LEFT;
21 import static android.view.WindowManager.DOCKED_BOTTOM;
22 import static android.view.WindowManager.DOCKED_INVALID;
23 import static android.view.WindowManager.DOCKED_LEFT;
24 import static android.view.WindowManager.DOCKED_RIGHT;
25 import static android.view.WindowManager.DOCKED_TOP;
26 
27 import android.animation.Animator;
28 import android.animation.AnimatorSet;
29 import android.animation.ObjectAnimator;
30 import android.animation.PropertyValuesHolder;
31 import android.annotation.IntDef;
32 import android.content.Context;
33 import android.content.res.Configuration;
34 import android.content.res.Resources;
35 import android.graphics.Canvas;
36 import android.graphics.Color;
37 import android.graphics.Paint;
38 import android.graphics.Point;
39 import android.graphics.Rect;
40 import android.graphics.RectF;
41 import android.graphics.drawable.ColorDrawable;
42 import android.util.IntProperty;
43 import android.view.animation.Interpolator;
44 
45 import com.android.internal.policy.DockedDividerUtils;
46 import com.android.systemui.Interpolators;
47 import com.android.systemui.R;
48 import com.android.systemui.recents.LegacyRecentsImpl;
49 import com.android.systemui.recents.utilities.Utilities;
50 
51 import java.lang.annotation.Retention;
52 import java.lang.annotation.RetentionPolicy;
53 import java.util.ArrayList;
54 
55 /**
56  * The various possible dock states when dragging and dropping a task.
57  */
58 public class DockState implements DropTarget {
59 
60     public static final int DOCK_AREA_BG_COLOR = 0xFFffffff;
61     public static final int DOCK_AREA_GRID_BG_COLOR = 0xFF000000;
62 
63     // The rotation to apply to the hint text
64     @Retention(RetentionPolicy.SOURCE)
65     @IntDef({HORIZONTAL, VERTICAL})
66     public @interface TextOrientation {}
67     private static final int HORIZONTAL = 0;
68     private static final int VERTICAL = 1;
69 
70     private static final int DOCK_AREA_ALPHA = 80;
71     public static final DockState NONE = new DockState(DOCKED_INVALID, -1, 80, 255, HORIZONTAL,
72             null, null, null);
73     public static final DockState LEFT = new DockState(DOCKED_LEFT,
74             SPLIT_SCREEN_CREATE_MODE_TOP_OR_LEFT, DOCK_AREA_ALPHA, 0, VERTICAL,
75             new RectF(0, 0, 0.125f, 1), new RectF(0, 0, 0.125f, 1),
76             new RectF(0, 0, 0.5f, 1));
77     public static final DockState TOP = new DockState(DOCKED_TOP,
78             SPLIT_SCREEN_CREATE_MODE_TOP_OR_LEFT, DOCK_AREA_ALPHA, 0, HORIZONTAL,
79             new RectF(0, 0, 1, 0.125f), new RectF(0, 0, 1, 0.125f),
80             new RectF(0, 0, 1, 0.5f));
81     public static final DockState RIGHT = new DockState(DOCKED_RIGHT,
82             SPLIT_SCREEN_CREATE_MODE_BOTTOM_OR_RIGHT, DOCK_AREA_ALPHA, 0, VERTICAL,
83             new RectF(0.875f, 0, 1, 1), new RectF(0.875f, 0, 1, 1),
84             new RectF(0.5f, 0, 1, 1));
85     public static final DockState BOTTOM = new DockState(DOCKED_BOTTOM,
86             SPLIT_SCREEN_CREATE_MODE_BOTTOM_OR_RIGHT, DOCK_AREA_ALPHA, 0, HORIZONTAL,
87             new RectF(0, 0.875f, 1, 1), new RectF(0, 0.875f, 1, 1),
88             new RectF(0, 0.5f, 1, 1));
89 
90     @Override
acceptsDrop(int x, int y, int width, int height, Rect insets, boolean isCurrentTarget)91     public boolean acceptsDrop(int x, int y, int width, int height, Rect insets,
92             boolean isCurrentTarget) {
93         if (isCurrentTarget) {
94             getMappedRect(expandedTouchDockArea, width, height, mTmpRect);
95             return mTmpRect.contains(x, y);
96         } else {
97             getMappedRect(touchArea, width, height, mTmpRect);
98             updateBoundsWithSystemInsets(mTmpRect, insets);
99             return mTmpRect.contains(x, y);
100         }
101     }
102 
103     // Represents the view state of this dock state
104     public static class ViewState {
105         private static final IntProperty<ViewState> HINT_ALPHA =
106                 new IntProperty<ViewState>("drawableAlpha") {
107                     @Override
108                     public void setValue(ViewState object, int alpha) {
109                         object.mHintTextAlpha = alpha;
110                         object.dockAreaOverlay.invalidateSelf();
111                     }
112 
113                     @Override
114                     public Integer get(ViewState object) {
115                         return object.mHintTextAlpha;
116                     }
117                 };
118 
119         public final int dockAreaAlpha;
120         public final ColorDrawable dockAreaOverlay;
121         public final int hintTextAlpha;
122         public final int hintTextOrientation;
123 
124         private final int mHintTextResId;
125         private String mHintText;
126         private Paint mHintTextPaint;
127         private Point mHintTextBounds = new Point();
128         private int mHintTextAlpha = 255;
129         private AnimatorSet mDockAreaOverlayAnimator;
130         private Rect mTmpRect = new Rect();
131 
ViewState(int areaAlpha, int hintAlpha, @TextOrientation int hintOrientation, int hintTextResId)132         private ViewState(int areaAlpha, int hintAlpha, @TextOrientation int hintOrientation,
133                 int hintTextResId) {
134             dockAreaAlpha = areaAlpha;
135             dockAreaOverlay = new ColorDrawable(LegacyRecentsImpl.getConfiguration().isGridEnabled
136                     ? DOCK_AREA_GRID_BG_COLOR : DOCK_AREA_BG_COLOR);
137             dockAreaOverlay.setAlpha(0);
138             hintTextAlpha = hintAlpha;
139             hintTextOrientation = hintOrientation;
140             mHintTextResId = hintTextResId;
141             mHintTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
142             mHintTextPaint.setColor(Color.WHITE);
143         }
144 
145         /**
146          * Updates the view state with the given context.
147          */
update(Context context)148         public void update(Context context) {
149             Resources res = context.getResources();
150             mHintText = context.getString(mHintTextResId);
151             mHintTextPaint.setTextSize(res.getDimensionPixelSize(
152                     R.dimen.recents_drag_hint_text_size));
153             mHintTextPaint.getTextBounds(mHintText, 0, mHintText.length(), mTmpRect);
154             mHintTextBounds.set((int) mHintTextPaint.measureText(mHintText), mTmpRect.height());
155         }
156 
157         /**
158          * Draws the current view state.
159          */
draw(Canvas canvas)160         public void draw(Canvas canvas) {
161             // Draw the overlay background
162             if (dockAreaOverlay.getAlpha() > 0) {
163                 dockAreaOverlay.draw(canvas);
164             }
165 
166             // Draw the hint text
167             if (mHintTextAlpha > 0) {
168                 Rect bounds = dockAreaOverlay.getBounds();
169                 int x = bounds.left + (bounds.width() - mHintTextBounds.x) / 2;
170                 int y = bounds.top + (bounds.height() + mHintTextBounds.y) / 2;
171                 mHintTextPaint.setAlpha(mHintTextAlpha);
172                 if (hintTextOrientation == VERTICAL) {
173                     canvas.save();
174                     canvas.rotate(-90f, bounds.centerX(), bounds.centerY());
175                 }
176                 canvas.drawText(mHintText, x, y, mHintTextPaint);
177                 if (hintTextOrientation == VERTICAL) {
178                     canvas.restore();
179                 }
180             }
181         }
182 
183         /**
184          * Creates a new bounds and alpha animation.
185          */
startAnimation(Rect bounds, int areaAlpha, int hintAlpha, int duration, Interpolator interpolator, boolean animateAlpha, boolean animateBounds)186         public void startAnimation(Rect bounds, int areaAlpha, int hintAlpha, int duration,
187                 Interpolator interpolator, boolean animateAlpha, boolean animateBounds) {
188             if (mDockAreaOverlayAnimator != null) {
189                 mDockAreaOverlayAnimator.cancel();
190             }
191 
192             ObjectAnimator anim;
193             ArrayList<Animator> animators = new ArrayList<>();
194             if (dockAreaOverlay.getAlpha() != areaAlpha) {
195                 if (animateAlpha) {
196                     anim = ObjectAnimator.ofInt(dockAreaOverlay,
197                             Utilities.DRAWABLE_ALPHA, dockAreaOverlay.getAlpha(), areaAlpha);
198                     anim.setDuration(duration);
199                     anim.setInterpolator(interpolator);
200                     animators.add(anim);
201                 } else {
202                     dockAreaOverlay.setAlpha(areaAlpha);
203                 }
204             }
205             if (mHintTextAlpha != hintAlpha) {
206                 if (animateAlpha) {
207                     anim = ObjectAnimator.ofInt(this, HINT_ALPHA, mHintTextAlpha,
208                             hintAlpha);
209                     anim.setDuration(150);
210                     anim.setInterpolator(hintAlpha > mHintTextAlpha
211                             ? Interpolators.ALPHA_IN
212                             : Interpolators.ALPHA_OUT);
213                     animators.add(anim);
214                 } else {
215                     mHintTextAlpha = hintAlpha;
216                     dockAreaOverlay.invalidateSelf();
217                 }
218             }
219             if (bounds != null && !dockAreaOverlay.getBounds().equals(bounds)) {
220                 if (animateBounds) {
221                     PropertyValuesHolder prop = PropertyValuesHolder.ofObject(
222                             Utilities.DRAWABLE_RECT, Utilities.RECT_EVALUATOR,
223                             new Rect(dockAreaOverlay.getBounds()), bounds);
224                     anim = ObjectAnimator.ofPropertyValuesHolder(dockAreaOverlay, prop);
225                     anim.setDuration(duration);
226                     anim.setInterpolator(interpolator);
227                     animators.add(anim);
228                 } else {
229                     dockAreaOverlay.setBounds(bounds);
230                 }
231             }
232             if (!animators.isEmpty()) {
233                 mDockAreaOverlayAnimator = new AnimatorSet();
234                 mDockAreaOverlayAnimator.playTogether(animators);
235                 mDockAreaOverlayAnimator.start();
236             }
237         }
238     }
239 
240     public final int dockSide;
241     public final int createMode;
242     public final ViewState viewState;
243     private final RectF touchArea;
244     private final RectF dockArea;
245     private final RectF expandedTouchDockArea;
246     private static final Rect mTmpRect = new Rect();
247 
248     /**
249      * @param createMode used to pass to ActivityManager to dock the task
250      * @param touchArea the area in which touch will initiate this dock state
251      * @param dockArea the visible dock area
252      * @param expandedTouchDockArea the area in which touch will continue to dock after entering
253      *                              the initial touch area.  This is also the new dock area to
254      *                              draw.
255      */
DockState(int dockSide, int createMode, int dockAreaAlpha, int hintTextAlpha, @TextOrientation int hintTextOrientation, RectF touchArea, RectF dockArea, RectF expandedTouchDockArea)256     DockState(int dockSide, int createMode, int dockAreaAlpha, int hintTextAlpha,
257             @TextOrientation int hintTextOrientation, RectF touchArea, RectF dockArea,
258             RectF expandedTouchDockArea) {
259         this.dockSide = dockSide;
260         this.createMode = createMode;
261         this.viewState = new ViewState(dockAreaAlpha, hintTextAlpha, hintTextOrientation,
262                 R.string.recents_drag_hint_message);
263         this.dockArea = dockArea;
264         this.touchArea = touchArea;
265         this.expandedTouchDockArea = expandedTouchDockArea;
266     }
267 
268     /**
269      * Updates the dock state with the given context.
270      */
update(Context context)271     public void update(Context context) {
272         viewState.update(context);
273     }
274 
275     /**
276      * Returns the docked task bounds with the given {@param width} and {@param height}.
277      */
getPreDockedBounds(int width, int height, Rect insets)278     public Rect getPreDockedBounds(int width, int height, Rect insets) {
279         getMappedRect(dockArea, width, height, mTmpRect);
280         return updateBoundsWithSystemInsets(mTmpRect, insets);
281     }
282 
283     /**
284      * Returns the expanded docked task bounds with the given {@param width} and
285      * {@param height}.
286      */
getDockedBounds(int width, int height, int dividerSize, Rect insets, Resources res)287     public Rect getDockedBounds(int width, int height, int dividerSize, Rect insets,
288             Resources res) {
289         // Calculate the docked task bounds
290         boolean isHorizontalDivision =
291                 res.getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT;
292         int position = DockedDividerUtils.calculateMiddlePosition(isHorizontalDivision,
293                 insets, width, height, dividerSize);
294         Rect newWindowBounds = new Rect();
295         DockedDividerUtils.calculateBoundsForPosition(position, dockSide, newWindowBounds,
296                 width, height, dividerSize);
297         return newWindowBounds;
298     }
299 
300     /**
301      * Returns the task stack bounds with the given {@param width} and
302      * {@param height}.
303      */
getDockedTaskStackBounds(Rect displayRect, int width, int height, int dividerSize, Rect insets, TaskStackLayoutAlgorithm layoutAlgorithm, Resources res, Rect windowRectOut)304     public Rect getDockedTaskStackBounds(Rect displayRect, int width, int height,
305             int dividerSize, Rect insets, TaskStackLayoutAlgorithm layoutAlgorithm,
306             Resources res, Rect windowRectOut) {
307         // Calculate the inverse docked task bounds
308         boolean isHorizontalDivision =
309                 res.getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT;
310         int position = DockedDividerUtils.calculateMiddlePosition(isHorizontalDivision,
311                 insets, width, height, dividerSize);
312         DockedDividerUtils.calculateBoundsForPosition(position,
313                 DockedDividerUtils.invertDockSide(dockSide), windowRectOut, width, height,
314                 dividerSize);
315 
316         // Calculate the task stack bounds from the new window bounds
317         Rect taskStackBounds = new Rect();
318         // If the task stack bounds is specifically under the dock area, then ignore the top
319         // inset
320         int top = dockArea.bottom < 1f
321                 ? 0
322                 : insets.top;
323         // For now, ignore the left insets since we always dock on the left and show Recents
324         // on the right
325         layoutAlgorithm.getTaskStackBounds(displayRect, windowRectOut, top, 0, insets.right,
326                 taskStackBounds);
327         return taskStackBounds;
328     }
329 
330     /**
331      * Returns the expanded bounds in certain dock sides such that the bounds account for the
332      * system insets (namely the vertical nav bar).  This call modifies and returns the given
333      * {@param bounds}.
334      */
335     private Rect updateBoundsWithSystemInsets(Rect bounds, Rect insets) {
336         if (dockSide == DOCKED_LEFT) {
337             bounds.right += insets.left;
338         } else if (dockSide == DOCKED_RIGHT) {
339             bounds.left -= insets.right;
340         }
341         return bounds;
342     }
343 
344     /**
345      * Returns the mapped rect to the given dimensions.
346      */
347     private void getMappedRect(RectF bounds, int width, int height, Rect out) {
348         out.set((int) (bounds.left * width), (int) (bounds.top * height),
349                 (int) (bounds.right * width), (int) (bounds.bottom * height));
350     }
351 }