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 }