• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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