1 /* 2 * Copyright (C) 2024 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.windowdecor; 18 19 import static android.view.InputDevice.SOURCE_MOUSE; 20 import static android.view.InputDevice.SOURCE_TOUCHSCREEN; 21 import static android.window.DesktopModeFlags.ENABLE_WINDOWING_EDGE_DRAG_RESIZE; 22 23 import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_BOTTOM; 24 import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_LEFT; 25 import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_RIGHT; 26 import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_TOP; 27 import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_UNDEFINED; 28 29 import android.annotation.NonNull; 30 import android.content.Context; 31 import android.content.res.Resources; 32 import android.graphics.Point; 33 import android.graphics.Rect; 34 import android.graphics.Region; 35 import android.util.Size; 36 import android.view.MotionEvent; 37 38 import com.android.wm.shell.R; 39 40 import java.util.Objects; 41 42 /** 43 * Geometry for a drag resize region for a particular window. 44 */ 45 public final class DragResizeWindowGeometry { 46 private final int mTaskCornerRadius; 47 private final Size mTaskSize; 48 // The size of the handle outside the task window applied to the edges of the window, for the 49 // user to drag resize. 50 private final int mResizeHandleEdgeOutset; 51 // The size of the handle inside the task window applied to the edges of the window, for the 52 // user to drag resize. 53 private final int mResizeHandleEdgeInset; 54 // The task corners to permit drag resizing with a course input, such as touch. 55 private final @NonNull TaskCorners mLargeTaskCorners; 56 // The task corners to permit drag resizing with a fine input, such as stylus or cursor. 57 private final @NonNull TaskCorners mFineTaskCorners; 58 // The bounds for each edge drag region, which can resize the task in one direction. 59 final @NonNull TaskEdges mTaskEdges; 60 61 private final DisabledEdge mDisabledEdge; 62 DragResizeWindowGeometry(int taskCornerRadius, @NonNull Size taskSize, int resizeHandleEdgeOutset, int resizeHandleEdgeInset, int fineCornerSize, int largeCornerSize, DisabledEdge disabledEdge)63 DragResizeWindowGeometry(int taskCornerRadius, @NonNull Size taskSize, 64 int resizeHandleEdgeOutset, int resizeHandleEdgeInset, int fineCornerSize, 65 int largeCornerSize, DisabledEdge disabledEdge) { 66 mTaskCornerRadius = taskCornerRadius; 67 mTaskSize = taskSize; 68 mResizeHandleEdgeOutset = resizeHandleEdgeOutset; 69 mResizeHandleEdgeInset = resizeHandleEdgeInset; 70 71 mDisabledEdge = disabledEdge; 72 73 mLargeTaskCorners = new TaskCorners(mTaskSize, largeCornerSize, disabledEdge); 74 mFineTaskCorners = new TaskCorners(mTaskSize, fineCornerSize, disabledEdge); 75 76 // Save touch areas for each edge. 77 mTaskEdges = new TaskEdges(mTaskSize, mResizeHandleEdgeOutset, mDisabledEdge); 78 } 79 80 /** 81 * Returns the resource value to use for the resize handle on the edge of the window. 82 */ getResizeEdgeHandleSize(@onNull Resources res)83 static int getResizeEdgeHandleSize(@NonNull Resources res) { 84 return ENABLE_WINDOWING_EDGE_DRAG_RESIZE.isTrue() 85 ? res.getDimensionPixelSize(R.dimen.freeform_edge_handle_outset) 86 : res.getDimensionPixelSize(R.dimen.freeform_resize_handle); 87 } 88 89 /** 90 * Returns the resource value to use for the edge resize handle inside the task bounds. 91 */ getResizeHandleEdgeInset(@onNull Resources res)92 static int getResizeHandleEdgeInset(@NonNull Resources res) { 93 return res.getDimensionPixelSize(R.dimen.freeform_edge_handle_inset); 94 } 95 96 /** 97 * Returns the resource value to use for course input, such as touch, that benefits from a large 98 * square on each of the window's corners. 99 */ getLargeResizeCornerSize(@onNull Resources res)100 static int getLargeResizeCornerSize(@NonNull Resources res) { 101 return res.getDimensionPixelSize(R.dimen.desktop_mode_corner_resize_large); 102 } 103 104 /** 105 * Returns the resource value to use for fine input, such as stylus, that can use a smaller 106 * square on each of the window's corners. 107 */ getFineResizeCornerSize(@onNull Resources res)108 static int getFineResizeCornerSize(@NonNull Resources res) { 109 return res.getDimensionPixelSize(R.dimen.freeform_resize_corner); 110 } 111 112 /** 113 * Returns the size of the task this geometry is calculated for. 114 */ 115 @NonNull getTaskSize()116 Size getTaskSize() { 117 // Safe to return directly since size is immutable. 118 return mTaskSize; 119 } 120 121 /** 122 * Returns the union of all regions that can be touched for drag resizing; the corners window 123 * and window edges. 124 */ union(@onNull Region region)125 void union(@NonNull Region region) { 126 // Apply the edge resize regions. 127 mTaskEdges.union(region); 128 129 if (ENABLE_WINDOWING_EDGE_DRAG_RESIZE.isTrue()) { 130 // Apply the corners as well for the larger corners, to ensure we capture all possible 131 // touches. 132 mLargeTaskCorners.union(region); 133 } else { 134 // Only apply fine corners for the legacy approach. 135 mFineTaskCorners.union(region); 136 } 137 } 138 139 /** 140 * Returns if this MotionEvent should be handled, based on its source and position. 141 */ shouldHandleEvent(@onNull Context context, @NonNull MotionEvent e, @NonNull Point offset)142 boolean shouldHandleEvent(@NonNull Context context, @NonNull MotionEvent e, 143 @NonNull Point offset) { 144 final float x = e.getX(0) + offset.x; 145 final float y = e.getY(0) + offset.y; 146 147 if (ENABLE_WINDOWING_EDGE_DRAG_RESIZE.isTrue()) { 148 // First check if touch falls within a corner. 149 // Large corner bounds are used for course input like touch, otherwise fine bounds. 150 boolean result = isEventFromTouchscreen(e) 151 ? isInCornerBounds(mLargeTaskCorners, x, y) 152 : isInCornerBounds(mFineTaskCorners, x, y); 153 // Check if touch falls within the edge resize handle. Limit edge resizing to stylus and 154 // mouse input. 155 if (!result && isEdgeResizePermitted(e)) { 156 result = isInEdgeResizeBounds(x, y); 157 } 158 return result; 159 } else { 160 // Legacy uses only fine corners for touch, and edges only for non-touch input. 161 return isEventFromTouchscreen(e) 162 ? isInCornerBounds(mFineTaskCorners, x, y) 163 : isInEdgeResizeBounds(x, y); 164 } 165 } 166 isEventFromTouchscreen(@onNull MotionEvent e)167 static boolean isEventFromTouchscreen(@NonNull MotionEvent e) { 168 return (e.getSource() & SOURCE_TOUCHSCREEN) == SOURCE_TOUCHSCREEN; 169 } 170 171 /** 172 * Whether resizing a window from the edge is permitted based on the motion event. 173 */ isEdgeResizePermitted(@onNull MotionEvent e)174 public static boolean isEdgeResizePermitted(@NonNull MotionEvent e) { 175 if (ENABLE_WINDOWING_EDGE_DRAG_RESIZE.isTrue()) { 176 return e.getToolType(0) == MotionEvent.TOOL_TYPE_STYLUS 177 || e.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE 178 // Touchpad input 179 || (e.isFromSource(SOURCE_MOUSE) 180 && e.getToolType(0) == MotionEvent.TOOL_TYPE_FINGER); 181 } else { 182 return e.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE; 183 } 184 } 185 isInCornerBounds(TaskCorners corners, float xf, float yf)186 private boolean isInCornerBounds(TaskCorners corners, float xf, float yf) { 187 return corners.calculateCornersCtrlType(xf, yf) != 0; 188 } 189 isInEdgeResizeBounds(float x, float y)190 private boolean isInEdgeResizeBounds(float x, float y) { 191 return calculateEdgeResizeCtrlType(x, y) != CTRL_TYPE_UNDEFINED; 192 } 193 194 /** 195 * Returns the control type for the drag-resize, based on the touch regions and this 196 * MotionEvent's coordinates. 197 * 198 * @param isTouchscreen Controls the size of the corner resize regions; touchscreen events 199 * (finger & stylus) are eligible for a larger area than cursor events. 200 * @param isEdgeResizePermitted Indicates if the event is eligible for falling into an edge 201 * resize region. 202 */ 203 @DragPositioningCallback.CtrlType calculateCtrlType(boolean isTouchscreen, boolean isEdgeResizePermitted, float x, float y)204 int calculateCtrlType(boolean isTouchscreen, boolean isEdgeResizePermitted, float x, float y) { 205 if (ENABLE_WINDOWING_EDGE_DRAG_RESIZE.isTrue()) { 206 // First check if touch falls within a corner. 207 // Large corner bounds are used for course input like touch, otherwise fine bounds. 208 int ctrlType = isTouchscreen 209 ? mLargeTaskCorners.calculateCornersCtrlType(x, y) 210 : mFineTaskCorners.calculateCornersCtrlType(x, y); 211 212 // Check if touch falls within the edge resize handle, since edge resizing can apply 213 // for any input source. 214 if (ctrlType == CTRL_TYPE_UNDEFINED && isEdgeResizePermitted) { 215 ctrlType = calculateEdgeResizeCtrlType(x, y); 216 } 217 return ctrlType; 218 } else { 219 // Legacy uses only fine corners for touch, and edges only for non-touch input. 220 return isTouchscreen 221 ? mFineTaskCorners.calculateCornersCtrlType(x, y) 222 : calculateEdgeResizeCtrlType(x, y); 223 } 224 } 225 226 @DragPositioningCallback.CtrlType calculateEdgeResizeCtrlType(float x, float y)227 private int calculateEdgeResizeCtrlType(float x, float y) { 228 int ctrlType = CTRL_TYPE_UNDEFINED; 229 // mTaskCornerRadius is only used in comparing with corner regions. Comparisons with 230 // sides will use the bounds specified in setGeometry and not go into task bounds. 231 if (x < mTaskCornerRadius) { 232 ctrlType |= CTRL_TYPE_LEFT; 233 } 234 if (x > mTaskSize.getWidth() - mTaskCornerRadius) { 235 ctrlType |= CTRL_TYPE_RIGHT; 236 } 237 if (y < mTaskCornerRadius) { 238 ctrlType |= CTRL_TYPE_TOP; 239 } 240 if (y > mTaskSize.getHeight() - mTaskCornerRadius) { 241 ctrlType |= CTRL_TYPE_BOTTOM; 242 } 243 // If the touch is within one of the four corners, check if it is within the bounds of the 244 // handle. 245 if ((ctrlType & (CTRL_TYPE_LEFT | CTRL_TYPE_RIGHT)) != 0 246 && (ctrlType & (CTRL_TYPE_TOP | CTRL_TYPE_BOTTOM)) != 0) { 247 return checkDistanceFromCenter(ctrlType, x, y); 248 } 249 // Allow a small resize handle inside the task bounds defined by the edge inset. 250 return (x <= mResizeHandleEdgeInset || y <= mResizeHandleEdgeInset 251 || x >= mTaskSize.getWidth() - mResizeHandleEdgeInset 252 || y >= mTaskSize.getHeight() - mResizeHandleEdgeInset) 253 ? ctrlType : CTRL_TYPE_UNDEFINED; 254 } 255 256 /** 257 * Return {@code ctrlType} if the corner input is outside the (potentially rounded) corner of 258 * the task, and within the thickness of the resize handle. Otherwise, return 0. 259 */ 260 @DragPositioningCallback.CtrlType checkDistanceFromCenter(@ragPositioningCallback.CtrlType int ctrlType, float x, float y)261 private int checkDistanceFromCenter(@DragPositioningCallback.CtrlType int ctrlType, float x, 262 float y) { 263 if ((mDisabledEdge == DisabledEdge.RIGHT && (ctrlType & CTRL_TYPE_RIGHT) != 0) 264 || mDisabledEdge == DisabledEdge.LEFT && ((ctrlType & CTRL_TYPE_LEFT) != 0)) { 265 return CTRL_TYPE_UNDEFINED; 266 } 267 final Point cornerRadiusCenter = calculateCenterForCornerRadius(ctrlType); 268 double distanceFromCenter = Math.hypot(x - cornerRadiusCenter.x, y - cornerRadiusCenter.y); 269 270 if (distanceFromCenter < mTaskCornerRadius + mResizeHandleEdgeOutset 271 && distanceFromCenter >= mTaskCornerRadius) { 272 return ctrlType; 273 } 274 return CTRL_TYPE_UNDEFINED; 275 } 276 277 /** 278 * Returns center of rounded corner circle; this is simply the corner if radius is 0. 279 */ calculateCenterForCornerRadius(@ragPositioningCallback.CtrlType int ctrlType)280 private Point calculateCenterForCornerRadius(@DragPositioningCallback.CtrlType int ctrlType) { 281 int centerX; 282 int centerY; 283 284 switch (ctrlType) { 285 case CTRL_TYPE_LEFT | CTRL_TYPE_TOP: { 286 centerX = mTaskCornerRadius; 287 centerY = mTaskCornerRadius; 288 break; 289 } 290 case CTRL_TYPE_LEFT | CTRL_TYPE_BOTTOM: { 291 centerX = mTaskCornerRadius; 292 centerY = mTaskSize.getHeight() - mTaskCornerRadius; 293 break; 294 } 295 case CTRL_TYPE_RIGHT | CTRL_TYPE_TOP: { 296 centerX = mTaskSize.getWidth() - mTaskCornerRadius; 297 centerY = mTaskCornerRadius; 298 break; 299 } 300 case CTRL_TYPE_RIGHT | CTRL_TYPE_BOTTOM: { 301 centerX = mTaskSize.getWidth() - mTaskCornerRadius; 302 centerY = mTaskSize.getHeight() - mTaskCornerRadius; 303 break; 304 } 305 default: { 306 throw new IllegalArgumentException( 307 "ctrlType should be complex, but it's 0x" + Integer.toHexString(ctrlType)); 308 } 309 } 310 return new Point(centerX, centerY); 311 } 312 313 @Override equals(Object obj)314 public boolean equals(Object obj) { 315 if (obj == null) return false; 316 if (this == obj) return true; 317 if (!(obj instanceof DragResizeWindowGeometry other)) return false; 318 319 return this.mTaskCornerRadius == other.mTaskCornerRadius 320 && this.mTaskSize.equals(other.mTaskSize) 321 && this.mResizeHandleEdgeOutset == other.mResizeHandleEdgeOutset 322 && this.mResizeHandleEdgeInset == other.mResizeHandleEdgeInset 323 && this.mFineTaskCorners.equals(other.mFineTaskCorners) 324 && this.mLargeTaskCorners.equals(other.mLargeTaskCorners) 325 && this.mTaskEdges.equals(other.mTaskEdges); 326 } 327 328 @Override hashCode()329 public int hashCode() { 330 return Objects.hash( 331 mTaskCornerRadius, 332 mTaskSize, 333 mResizeHandleEdgeOutset, 334 mResizeHandleEdgeInset, 335 mFineTaskCorners, 336 mLargeTaskCorners, 337 mTaskEdges); 338 } 339 340 /** 341 * Representation of the drag resize regions at the corner of the window. 342 */ 343 private static class TaskCorners { 344 // The size of the square applied to the corners of the window, for the user to drag 345 // resize. 346 private final int mCornerSize; 347 // The square for each corner. 348 private final @NonNull Rect mLeftTopCornerBounds; 349 private final @NonNull Rect mRightTopCornerBounds; 350 private final @NonNull Rect mLeftBottomCornerBounds; 351 private final @NonNull Rect mRightBottomCornerBounds; 352 private final @NonNull DisabledEdge mDisabledEdge; 353 TaskCorners(@onNull Size taskSize, int cornerSize, DisabledEdge disabledEdge)354 TaskCorners(@NonNull Size taskSize, int cornerSize, DisabledEdge disabledEdge) { 355 mCornerSize = cornerSize; 356 mDisabledEdge = disabledEdge; 357 final int cornerRadius = cornerSize / 2; 358 mLeftTopCornerBounds = (disabledEdge == DisabledEdge.LEFT) ? new Rect() : new Rect( 359 -cornerRadius, 360 -cornerRadius, 361 cornerRadius, 362 cornerRadius); 363 364 mRightTopCornerBounds = (disabledEdge == DisabledEdge.RIGHT) ? new Rect() : new Rect( 365 taskSize.getWidth() - cornerRadius, 366 -cornerRadius, 367 taskSize.getWidth() + cornerRadius, 368 cornerRadius); 369 370 mLeftBottomCornerBounds = (disabledEdge == DisabledEdge.LEFT) ? new Rect() : new Rect( 371 -cornerRadius, 372 taskSize.getHeight() - cornerRadius, 373 cornerRadius, 374 taskSize.getHeight() + cornerRadius); 375 376 mRightBottomCornerBounds = (disabledEdge == DisabledEdge.RIGHT) ? new Rect() : new Rect( 377 taskSize.getWidth() - cornerRadius, 378 taskSize.getHeight() - cornerRadius, 379 taskSize.getWidth() + cornerRadius, 380 taskSize.getHeight() + cornerRadius); 381 } 382 383 /** 384 * Updates the region to include all four corners. 385 */ union(Region region)386 void union(Region region) { 387 if (mDisabledEdge != DisabledEdge.RIGHT) { 388 region.union(mRightTopCornerBounds); 389 region.union(mRightBottomCornerBounds); 390 } 391 if (mDisabledEdge != DisabledEdge.LEFT) { 392 region.union(mLeftTopCornerBounds); 393 region.union(mLeftBottomCornerBounds); 394 } 395 } 396 397 /** 398 * Returns the control type based on the position of the {@code MotionEvent}'s coordinates. 399 */ 400 @DragPositioningCallback.CtrlType calculateCornersCtrlType(float x, float y)401 int calculateCornersCtrlType(float x, float y) { 402 int xi = (int) x; 403 int yi = (int) y; 404 if (mLeftTopCornerBounds.contains(xi, yi)) { 405 return CTRL_TYPE_LEFT | CTRL_TYPE_TOP; 406 } 407 if (mLeftBottomCornerBounds.contains(xi, yi)) { 408 return CTRL_TYPE_LEFT | CTRL_TYPE_BOTTOM; 409 } 410 if (mRightTopCornerBounds.contains(xi, yi)) { 411 return CTRL_TYPE_RIGHT | CTRL_TYPE_TOP; 412 } 413 if (mRightBottomCornerBounds.contains(xi, yi)) { 414 return CTRL_TYPE_RIGHT | CTRL_TYPE_BOTTOM; 415 } 416 return 0; 417 } 418 419 @Override toString()420 public String toString() { 421 return "TaskCorners of size " + mCornerSize + " for the" 422 + " top left " + mLeftTopCornerBounds 423 + " top right " + mRightTopCornerBounds 424 + " bottom left " + mLeftBottomCornerBounds 425 + " bottom right " + mRightBottomCornerBounds; 426 } 427 428 @Override equals(Object obj)429 public boolean equals(Object obj) { 430 if (obj == null) return false; 431 if (this == obj) return true; 432 if (!(obj instanceof TaskCorners other)) return false; 433 434 return this.mCornerSize == other.mCornerSize 435 && this.mLeftTopCornerBounds.equals(other.mLeftTopCornerBounds) 436 && this.mRightTopCornerBounds.equals(other.mRightTopCornerBounds) 437 && this.mLeftBottomCornerBounds.equals(other.mLeftBottomCornerBounds) 438 && this.mRightBottomCornerBounds.equals(other.mRightBottomCornerBounds); 439 } 440 441 @Override hashCode()442 public int hashCode() { 443 return Objects.hash( 444 mCornerSize, 445 mLeftTopCornerBounds, 446 mRightTopCornerBounds, 447 mLeftBottomCornerBounds, 448 mRightBottomCornerBounds); 449 } 450 } 451 452 /** 453 * Representation of the drag resize regions at the edges of the window. 454 */ 455 private static class TaskEdges { 456 private final @NonNull Rect mTopEdgeBounds; 457 private final @NonNull Rect mLeftEdgeBounds; 458 private final @NonNull Rect mRightEdgeBounds; 459 private final @NonNull Rect mBottomEdgeBounds; 460 private final @NonNull Region mRegion; 461 private final @NonNull DisabledEdge mDisabledEdge; 462 TaskEdges(@onNull Size taskSize, int resizeHandleThickness, DisabledEdge disabledEdge)463 private TaskEdges(@NonNull Size taskSize, int resizeHandleThickness, 464 DisabledEdge disabledEdge) { 465 // Save touch areas for each edge. 466 mDisabledEdge = disabledEdge; 467 // Save touch areas for each edge. 468 mTopEdgeBounds = new Rect( 469 -resizeHandleThickness, 470 -resizeHandleThickness, 471 taskSize.getWidth() + resizeHandleThickness, 472 resizeHandleThickness); 473 mLeftEdgeBounds = new Rect( 474 -resizeHandleThickness, 475 0, 476 resizeHandleThickness, 477 taskSize.getHeight()); 478 mRightEdgeBounds = new Rect( 479 taskSize.getWidth() - resizeHandleThickness, 480 0, 481 taskSize.getWidth() + resizeHandleThickness, 482 taskSize.getHeight()); 483 mBottomEdgeBounds = new Rect( 484 -resizeHandleThickness, 485 taskSize.getHeight() - resizeHandleThickness, 486 taskSize.getWidth() + resizeHandleThickness, 487 taskSize.getHeight() + resizeHandleThickness); 488 489 mRegion = new Region(); 490 union(mRegion); 491 } 492 493 /** 494 * Returns {@code true} if the edges contain the given point. 495 */ contains(int x, int y)496 private boolean contains(int x, int y) { 497 return mRegion.contains(x, y); 498 } 499 500 /** 501 * Updates the region to include all four corners. 502 */ union(Region region)503 private void union(Region region) { 504 if (mDisabledEdge != DisabledEdge.RIGHT) { 505 region.union(mRightEdgeBounds); 506 } 507 if (mDisabledEdge != DisabledEdge.LEFT) { 508 region.union(mLeftEdgeBounds); 509 } 510 region.union(mTopEdgeBounds); 511 region.union(mBottomEdgeBounds); 512 } 513 514 @Override toString()515 public String toString() { 516 return "TaskEdges for the" 517 + " top " + mTopEdgeBounds 518 + " left " + mLeftEdgeBounds 519 + " right " + mRightEdgeBounds 520 + " bottom " + mBottomEdgeBounds; 521 } 522 523 @Override equals(Object obj)524 public boolean equals(Object obj) { 525 if (obj == null) return false; 526 if (this == obj) return true; 527 if (!(obj instanceof TaskEdges other)) return false; 528 529 return this.mTopEdgeBounds.equals(other.mTopEdgeBounds) 530 && this.mLeftEdgeBounds.equals(other.mLeftEdgeBounds) 531 && this.mRightEdgeBounds.equals(other.mRightEdgeBounds) 532 && this.mBottomEdgeBounds.equals(other.mBottomEdgeBounds); 533 } 534 535 @Override hashCode()536 public int hashCode() { 537 return Objects.hash( 538 mTopEdgeBounds, 539 mLeftEdgeBounds, 540 mRightEdgeBounds, 541 mBottomEdgeBounds); 542 } 543 } 544 545 public enum DisabledEdge { 546 LEFT, 547 RIGHT, 548 NONE 549 } 550 } 551