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.wm.shell.windowdecor.tiling 18 19 import android.animation.Animator 20 import android.animation.AnimatorListenerAdapter 21 import android.animation.ValueAnimator 22 import android.content.Context 23 import android.content.res.Configuration 24 import android.graphics.Path 25 import android.graphics.PixelFormat 26 import android.graphics.Rect 27 import android.graphics.Region 28 import android.os.Binder 29 import android.util.Size 30 import android.view.LayoutInflater 31 import android.view.MotionEvent 32 import android.view.RoundedCorner 33 import android.view.SurfaceControl 34 import android.view.SurfaceControlViewHost 35 import android.view.View 36 import android.view.WindowManager 37 import android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE 38 import android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL 39 import android.view.WindowManager.LayoutParams.FLAG_SLIPPERY 40 import android.view.WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH 41 import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION 42 import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY 43 import android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER 44 import android.view.WindowlessWindowManager 45 import com.android.wm.shell.R 46 import com.android.wm.shell.common.SyncTransactionQueue 47 import java.util.function.Supplier 48 49 /** 50 * a [WindowlessWindowManaer] responsible for hosting the [TilingDividerView] on the display root 51 * when two tasks are tiled on left and right to resize them simultaneously. 52 */ 53 class DesktopTilingDividerWindowManager( 54 config: Configuration, 55 private val windowName: String, 56 private val context: Context, 57 private val leash: SurfaceControl, 58 private val syncQueue: SyncTransactionQueue, 59 private val transitionHandler: DesktopTilingWindowDecoration, 60 private val transactionSupplier: Supplier<SurfaceControl.Transaction>, 61 private var dividerBounds: Rect, 62 private val displayContext: Context, 63 private val isDarkMode: Boolean, 64 ) : WindowlessWindowManager(config, leash, null), DividerMoveCallback, View.OnLayoutChangeListener { 65 private lateinit var viewHost: SurfaceControlViewHost 66 private var tilingDividerView: TilingDividerView? = null 67 private var dividerShown = false 68 private var handleRegionSize: Size = 69 Size( 70 context.resources.getDimensionPixelSize(R.dimen.split_divider_handle_region_width), 71 context.resources.getDimensionPixelSize(R.dimen.split_divider_handle_region_height), 72 ) 73 private var setTouchRegion = true 74 private val maxRoundedCornerRadius = getMaxRoundedCornerRadius() 75 76 /** 77 * Gets bounds of divider window with screen based coordinate on the param Rect. 78 * 79 * @param rect bounds for the [TilingDividerView] 80 */ 81 fun getDividerBounds(rect: Rect) { 82 rect.set(dividerBounds) 83 } 84 85 /** 86 * Sets the touch region for the SurfaceControlViewHost. 87 * 88 * The region includes the area around the handle (for accessibility), the divider itself and 89 * the rounded corners (to prevent click reaching windows behind). 90 */ 91 fun setTouchRegion(handle: Rect, divider: Rect, cornerRadius: Float) { 92 val path = Path() 93 path.fillType = Path.FillType.WINDING 94 // The UI starts on the top-left corner, the region will be: 95 // 96 // cornerLeft cornerRight 97 // c1Top +--------+ 98 // |corners | 99 // c1Bottom +--+ +--+ 100 // | | 101 // handleLeft| | handleRight 102 // handleTop +----+ +----+ 103 // | handle | 104 // handleBot +----+ +----+ 105 // | | 106 // | | 107 // c2Top +--+ +--+ 108 // |corners | 109 // c2Bottom +--------+ 110 val cornerLeft = 0f 111 val centerX = cornerRadius + divider.width() / 2f 112 val centerY = divider.height() 113 val cornerRight = divider.width() + 2 * cornerRadius 114 val handleLeft = centerX - handle.width() / 2f 115 val handleRight = handleLeft + handle.width() 116 val dividerLeft = centerY - divider.width() / 2f 117 val dividerRight = dividerLeft + divider.width() 118 119 val c1Top = 0f 120 val c1Bottom = cornerRadius 121 val handleTop = centerY - handle.height() / 2f 122 val handleBottom = handleTop + handle.height() 123 val c2Top = divider.height() - cornerRadius 124 val c2Bottom = divider.height().toFloat() 125 126 // Top corners 127 path.addRect(cornerLeft, c1Top, cornerRight, c1Bottom, Path.Direction.CCW) 128 // Bottom corners 129 path.addRect(cornerLeft, c1Top, cornerRight, c2Bottom, Path.Direction.CCW) 130 // Handle 131 path.addRect(handleLeft, handleTop, handleRight, handleBottom, Path.Direction.CCW) 132 // Divider 133 path.addRect(dividerLeft, c2Top, dividerRight, c2Bottom, Path.Direction.CCW) 134 135 val clip = Rect(handleLeft.toInt(), c1Top.toInt(), handleRight.toInt(), c2Bottom.toInt()) 136 137 val region = Region() 138 region.setPath(path, Region(clip)) 139 140 setTouchRegion(viewHost.windowToken.asBinder(), region) 141 } 142 143 /** 144 * Builds a view host upon tiling two tasks left and right, and shows the divider view in the 145 * middle of the screen between both tasks. 146 * 147 * @param relativeLeash the task leash that the TilingDividerView should be shown on top of. 148 */ 149 fun generateViewHost(relativeLeash: SurfaceControl) { 150 val surfaceControlViewHost = 151 SurfaceControlViewHost(context, context.display, this, "DesktopTilingManager") 152 val dividerView = 153 LayoutInflater.from(context).inflate(R.layout.tiling_split_divider, /* root= */ null) 154 as TilingDividerView 155 val lp = getWindowManagerParams() 156 surfaceControlViewHost.setView(dividerView, lp) 157 val tmpDividerBounds = Rect() 158 getDividerBounds(tmpDividerBounds) 159 dividerView.setup(this, tmpDividerBounds, handleRegionSize, isDarkMode) 160 val dividerAnimatorT = transactionSupplier.get() 161 val dividerAnimator = 162 ValueAnimator.ofFloat(0f, 1f).apply { 163 duration = DIVIDER_FADE_IN_ALPHA_DURATION 164 addUpdateListener { 165 dividerAnimatorT.setAlpha(leash, animatedValue as Float).apply() 166 } 167 addListener( 168 object : AnimatorListenerAdapter() { 169 override fun onAnimationStart(animation: Animator) { 170 dividerAnimatorT 171 .setRelativeLayer(leash, relativeLeash, 1) 172 .setPosition( 173 leash, 174 dividerBounds.left.toFloat() - maxRoundedCornerRadius, 175 dividerBounds.top.toFloat(), 176 ) 177 .setAlpha(leash, 0f) 178 .show(leash) 179 .apply() 180 } 181 182 override fun onAnimationEnd(animation: Animator) { 183 dividerAnimatorT.setAlpha(leash, 1f).apply() 184 dividerShown = true 185 } 186 } 187 ) 188 } 189 dividerAnimator.start() 190 viewHost = surfaceControlViewHost 191 tilingDividerView = dividerView 192 updateTouchRegion() 193 dividerView.addOnLayoutChangeListener(this) 194 } 195 196 /** Changes divider colour if dark/light mode is toggled. */ 197 fun onUiModeChange(isDarkMode: Boolean) { 198 tilingDividerView?.onUiModeChange(isDarkMode) 199 } 200 201 /** Notifies the divider view of task info change and possible color change. */ 202 fun onTaskInfoChange() { 203 tilingDividerView?.onTaskInfoChange() 204 } 205 206 /** Hides the divider bar. */ 207 fun hideDividerBar() { 208 if (!dividerShown) { 209 return 210 } 211 val t = transactionSupplier.get() 212 t.hide(leash) 213 t.apply() 214 dividerShown = false 215 } 216 217 /** Shows the divider bar. */ 218 fun showDividerBar() { 219 if (dividerShown) return 220 val t = transactionSupplier.get() 221 t.show(leash) 222 t.apply() 223 dividerShown = true 224 } 225 226 /** 227 * When the tiled task on top changes, the divider bar's Z access should change to be on top of 228 * the latest focused task. 229 */ 230 fun onRelativeLeashChanged(relativeLeash: SurfaceControl, t: SurfaceControl.Transaction) { 231 t.setRelativeLayer(leash, relativeLeash, 1) 232 } 233 234 override fun onDividerMoveStart(pos: Int, motionEvent: MotionEvent) { 235 setSlippery(false) 236 transitionHandler.onDividerHandleDragStart(motionEvent) 237 } 238 239 /** 240 * Moves the divider view to a new position after touch, gets called from the 241 * [TilingDividerView] onTouch function. 242 */ 243 override fun onDividerMove(pos: Int): Boolean { 244 val t = transactionSupplier.get() 245 t.setPosition(leash, pos.toFloat() - maxRoundedCornerRadius, dividerBounds.top.toFloat()) 246 val dividerWidth = dividerBounds.width() 247 dividerBounds.set(pos, dividerBounds.top, pos + dividerWidth, dividerBounds.bottom) 248 return transitionHandler.onDividerHandleMoved(dividerBounds, t) 249 } 250 251 /** 252 * Notifies the transition handler of tiling operations ending, which might result in resizing 253 * WindowContainerTransactions if the sizes of the tiled tasks changed. 254 */ 255 override fun onDividerMovedEnd(pos: Int, motionEvent: MotionEvent) { 256 setSlippery(true) 257 val t = transactionSupplier.get() 258 t.setPosition(leash, pos.toFloat() - maxRoundedCornerRadius, dividerBounds.top.toFloat()) 259 val dividerWidth = dividerBounds.width() 260 dividerBounds.set(pos, dividerBounds.top, pos + dividerWidth, dividerBounds.bottom) 261 transitionHandler.onDividerHandleDragEnd(dividerBounds, t, motionEvent) 262 } 263 264 private fun getWindowManagerParams(): WindowManager.LayoutParams { 265 val lp = 266 WindowManager.LayoutParams( 267 /* w= */ dividerBounds.width() + 2 * maxRoundedCornerRadius, 268 /* h= */ dividerBounds.height(), 269 TYPE_DOCK_DIVIDER, 270 FLAG_NOT_FOCUSABLE or 271 FLAG_NOT_TOUCH_MODAL or 272 FLAG_WATCH_OUTSIDE_TOUCH or 273 FLAG_SLIPPERY, 274 PixelFormat.TRANSLUCENT, 275 ) 276 lp.token = Binder() 277 lp.title = windowName 278 lp.privateFlags = 279 lp.privateFlags or (PRIVATE_FLAG_NO_MOVE_ANIMATION or PRIVATE_FLAG_TRUSTED_OVERLAY) 280 return lp 281 } 282 283 /** 284 * Releases the surface control of the current [TilingDividerView] and tear down the view 285 * hierarchy.y. 286 */ 287 fun release() { 288 tilingDividerView = null 289 viewHost.release() 290 transactionSupplier.get().hide(leash).remove(leash).apply() 291 } 292 293 override fun onLayoutChange( 294 v: View?, 295 left: Int, 296 top: Int, 297 right: Int, 298 bottom: Int, 299 oldLeft: Int, 300 oldTop: Int, 301 oldRight: Int, 302 oldBottom: Int, 303 ) { 304 if (!setTouchRegion) return 305 306 updateTouchRegion() 307 setTouchRegion = false 308 } 309 310 private fun updateTouchRegion() { 311 val startX = -handleRegionSize.width / 2 312 val handle = Rect(startX, 0, startX + handleRegionSize.width, dividerBounds.height()) 313 setTouchRegion(handle, dividerBounds, maxRoundedCornerRadius.toFloat()) 314 } 315 316 private fun setSlippery(slippery: Boolean) { 317 val lp = tilingDividerView?.layoutParams as WindowManager.LayoutParams 318 val isSlippery = (lp.flags and FLAG_SLIPPERY) != 0 319 if (isSlippery == slippery) return 320 321 if (slippery) { 322 lp.flags = lp.flags or FLAG_SLIPPERY 323 } else { 324 lp.flags = lp.flags and FLAG_SLIPPERY.inv() 325 } 326 viewHost.relayout(lp) 327 } 328 329 private fun getMaxRoundedCornerRadius(): Int { 330 val display = displayContext.display 331 return listOf( 332 RoundedCorner.POSITION_TOP_LEFT, 333 RoundedCorner.POSITION_TOP_RIGHT, 334 RoundedCorner.POSITION_BOTTOM_RIGHT, 335 RoundedCorner.POSITION_BOTTOM_LEFT, 336 ) 337 .maxOf { position -> display.getRoundedCorner(position)?.getRadius() ?: 0 } 338 } 339 340 companion object { 341 private const val DIVIDER_FADE_IN_ALPHA_DURATION = 300L 342 } 343 } 344