1 /* 2 * Copyright (C) 2025 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.scene.ui.viewmodel 18 19 import androidx.compose.foundation.gestures.Orientation 20 import androidx.compose.ui.unit.Density 21 import androidx.compose.ui.unit.Dp 22 import androidx.compose.ui.unit.IntOffset 23 import androidx.compose.ui.unit.IntSize 24 import androidx.compose.ui.unit.LayoutDirection 25 import com.android.compose.animation.scene.Edge 26 import com.android.compose.animation.scene.FixedSizeEdgeDetector 27 import com.android.compose.animation.scene.SwipeSource 28 import com.android.compose.animation.scene.SwipeSourceDetector 29 30 /** Identifies an area of the [SceneContainer] to detect swipe gestures on. */ 31 sealed class SceneContainerArea(private val resolveArea: (LayoutDirection) -> Resolved) : 32 SwipeSource { 33 data object StartEdge : 34 SceneContainerArea( <lambda>null35 resolveArea = { 36 if (it == LayoutDirection.Ltr) Resolved.LeftEdge else Resolved.RightEdge 37 } 38 ) 39 40 data object StartHalf : 41 SceneContainerArea( <lambda>null42 resolveArea = { 43 if (it == LayoutDirection.Ltr) Resolved.LeftHalf else Resolved.RightHalf 44 } 45 ) 46 47 data object EndEdge : 48 SceneContainerArea( <lambda>null49 resolveArea = { 50 if (it == LayoutDirection.Ltr) Resolved.RightEdge else Resolved.LeftEdge 51 } 52 ) 53 54 data object EndHalf : 55 SceneContainerArea( <lambda>null56 resolveArea = { 57 if (it == LayoutDirection.Ltr) Resolved.RightHalf else Resolved.LeftHalf 58 } 59 ) 60 61 data object TopEdgeStartHalf : 62 SceneContainerArea( <lambda>null63 resolveArea = { 64 if (it == LayoutDirection.Ltr) Resolved.TopEdgeLeftHalf 65 else Resolved.TopEdgeRightHalf 66 } 67 ) 68 69 data object TopEdgeEndHalf : 70 SceneContainerArea( <lambda>null71 resolveArea = { 72 if (it == LayoutDirection.Ltr) Resolved.TopEdgeRightHalf 73 else Resolved.TopEdgeLeftHalf 74 } 75 ) 76 resolvenull77 override fun resolve(layoutDirection: LayoutDirection): Resolved { 78 return resolveArea(layoutDirection) 79 } 80 81 sealed interface Resolved : SwipeSource.Resolved { 82 data object LeftEdge : Resolved 83 84 data object LeftHalf : Resolved 85 86 data object BottomEdge : Resolved 87 88 data object RightEdge : Resolved 89 90 data object RightHalf : Resolved 91 92 /** The left half of the top edge of the display. */ 93 data object TopEdgeLeftHalf : Resolved 94 95 /** The right half of the top edge of the display. */ 96 data object TopEdgeRightHalf : Resolved 97 } 98 } 99 100 /** 101 * A [SwipeSourceDetector] that detects edges similarly to [FixedSizeEdgeDetector], but additionally 102 * detects the left and right halves of the screen (besides the edges). 103 * 104 * Corner cases (literally): A vertical swipe on the top-left corner of the screen will be resolved 105 * to [SceneContainerArea.Resolved.LeftHalf], whereas a horizontal swipe in the same position will 106 * be resolved to [SceneContainerArea.Resolved.LeftEdge]. The behavior is similar on the top-right 107 * corner of the screen. 108 * 109 * Callers who need to detect the start and end edges based on the layout direction (LTR vs RTL) 110 * should subscribe to [SceneContainerArea.StartEdge] and [SceneContainerArea.EndEdge] instead. 111 * These will be resolved at runtime to [SceneContainerArea.Resolved.LeftEdge] and 112 * [SceneContainerArea.Resolved.RightEdge] appropriately. Similarly, [SceneContainerArea.StartHalf] 113 * and [SceneContainerArea.EndHalf] will be resolved appropriately to 114 * [SceneContainerArea.Resolved.LeftHalf] and [SceneContainerArea.Resolved.RightHalf]. 115 * 116 * @param edgeSize The fixed size of each edge. 117 */ 118 class SceneContainerSwipeDetector(val edgeSize: Dp) : SwipeSourceDetector { 119 120 private val fixedEdgeDetector = FixedSizeEdgeDetector(edgeSize) 121 sourcenull122 override fun source( 123 layoutSize: IntSize, 124 position: IntOffset, 125 density: Density, 126 orientation: Orientation, 127 ): SceneContainerArea.Resolved { 128 val fixedEdge = fixedEdgeDetector.source(layoutSize, position, density, orientation) 129 return when (fixedEdge) { 130 Edge.Resolved.Left -> SceneContainerArea.Resolved.LeftEdge 131 Edge.Resolved.Bottom -> SceneContainerArea.Resolved.BottomEdge 132 Edge.Resolved.Right -> SceneContainerArea.Resolved.RightEdge 133 Edge.Resolved.Top -> { 134 if (position.x < layoutSize.width * 0.5f) { 135 SceneContainerArea.Resolved.TopEdgeLeftHalf 136 } else { 137 SceneContainerArea.Resolved.TopEdgeRightHalf 138 } 139 } 140 null -> { 141 if (position.x < layoutSize.width * 0.5f) { 142 SceneContainerArea.Resolved.LeftHalf 143 } else { 144 SceneContainerArea.Resolved.RightHalf 145 } 146 } 147 } 148 } 149 } 150