1 /* <lambda>null2 * 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.settings.connecteddevice.display 18 19 import android.graphics.RectF 20 import kotlin.math.hypot 21 22 // Unfortunately, in the world of IEEE 32-bit floats, A + X - X is not always == A 23 // For example: A = 1075.4271f 24 // C = 1249.2203f 25 // For example: - A - 173.79326f = - C 26 // However: - C + A = - 173.79321f 27 // So we need to keep track of how the movingDisplay block is attaching to otherDisplays throughout 28 // the calculations below. We cannot use the rect.left with its width as a proxy for rect.right. We 29 // have to save the "inner" or attached side and use the width or height to calculate the "external" 30 // side. 31 32 /** A potential X position for the display to clamp at. */ 33 private class XCoor( 34 val left : Float, val right : Float, 35 36 /** 37 * If present, the position of the display being attached to. If absent, indicates the X 38 * position is derived from the exact drag position. 39 */ 40 val attaching : RectF?, 41 ) 42 43 /** A potential Y position for the display to clamp at. */ 44 private class YCoor( 45 val top : Float, val bottom : Float, 46 47 /** 48 * If present, the position of the display being attached to. If absent, indicates the Y 49 * position is derived from the exact drag position. 50 */ 51 val attaching : RectF?, 52 ) 53 54 /** 55 * Finds the optimal clamp position assuming the user has dragged the block to `movingDisplay`. 56 * 57 * @param otherDisplays positions of the stationary displays (every one not being dragged) 58 * @param movingDisplay the position the user is current holding the block during a drag 59 * 60 * @return the clamp position as a RectF, whose dimensions will match that of `movingDisplay` 61 */ 62 fun clampPosition(otherDisplays : Iterable<RectF>, movingDisplay : RectF) : RectF { 63 val xCoors = otherDisplays.flatMap { 64 listOf( 65 // Attaching to left edge of `it` 66 XCoor(it.left - movingDisplay.width(), it.left, it), 67 // Attaching to right edge of `it` 68 XCoor(it.right, it.right + movingDisplay.width(), it), 69 ) 70 }.plusElement(XCoor(movingDisplay.left, movingDisplay.right, null)) 71 72 val yCoors = otherDisplays.flatMap { 73 listOf( 74 // Attaching to the top edge of `it` 75 YCoor(it.top - movingDisplay.height(), it.top, it), 76 // Attaching to the bottom edge of `it` 77 YCoor(it.bottom, it.bottom + movingDisplay.height(), it), 78 ) 79 }.plusElement(YCoor(movingDisplay.top, movingDisplay.bottom, null)) 80 81 class Cand(val x : XCoor, val y : YCoor) 82 83 val candidateGrid = xCoors.flatMap { x -> yCoors.map { y -> Cand(x, y) }} 84 val hasAttachInRange = candidateGrid.filter { 85 if (it.x.attaching != null) { 86 // Attaching to a vertical (left or right) edge. The y range of dragging and 87 // stationary blocks must overlap. 88 it.y.top <= it.x.attaching.bottom && it.y.bottom >= it.x.attaching.top 89 } else if (it.y.attaching != null) { 90 // Attaching to a horizontal (top or bottom) edge. The x range of dragging and 91 // stationary blocks must overlap. 92 it.x.left <= it.y.attaching.right && it.x.right >= it.y.attaching.left 93 } else { 94 // Not attaching to another display's edge at all, so not a valid clamp position. 95 false 96 } 97 } 98 // Clamp positions closest to the user's drag position are best. Sort by increasing distance 99 // from it, so the best will be first. 100 val prioritized = hasAttachInRange.sortedBy { 101 hypot(it.x.left - movingDisplay.left, it.y.top - movingDisplay.top) 102 } 103 val notIntersectingAny = prioritized.asSequence() 104 .map { RectF(it.x.left, it.y.top, it.x.right, it.y.bottom) } 105 .filter { p -> otherDisplays.all { !RectF.intersects(p, it) } } 106 107 // Note we return a copy of `movingDisplay` if there is no valid clamp position, which will only 108 // happen if `otherDisplays` is empty or has no valid rectangles. It may not be wise to rely on 109 // this behavior. 110 return notIntersectingAny.firstOrNull() ?: RectF(movingDisplay) 111 } 112