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 package com.android.wm.shell.windowdecor.tiling 17 18 import android.content.Context 19 import android.content.res.Configuration 20 import android.graphics.Canvas 21 import android.graphics.Paint 22 import android.graphics.Rect 23 import android.provider.DeviceConfig 24 import android.util.AttributeSet 25 import android.util.Size 26 import android.view.MotionEvent 27 import android.view.PointerIcon 28 import android.view.RoundedCorner 29 import android.view.View 30 import android.view.ViewConfiguration 31 import android.widget.FrameLayout 32 import androidx.compose.ui.graphics.toArgb 33 import com.android.internal.annotations.VisibleForTesting 34 import com.android.internal.config.sysui.SystemUiDeviceConfigFlags 35 import com.android.wm.shell.R 36 import com.android.wm.shell.common.split.DividerHandleView 37 import com.android.wm.shell.common.split.DividerRoundedCorner 38 import com.android.wm.shell.shared.animation.Interpolators 39 import com.android.wm.shell.windowdecor.DragDetector 40 import com.android.wm.shell.windowdecor.common.DecorThemeUtil 41 42 /** Divider for tiling split screen, currently mostly a copy of [DividerView]. */ 43 class TilingDividerView : FrameLayout, View.OnTouchListener, DragDetector.MotionEventHandler { 44 private val paint = Paint() 45 private val backgroundRect = Rect() 46 47 private lateinit var callback: DividerMoveCallback 48 private lateinit var handle: DividerHandleView 49 private lateinit var corners: DividerRoundedCorner 50 private var cornersRadius: Int = 0 51 private var touchElevation = 0 52 53 private var moving = false 54 private var startPos = 0 55 var handleRegionWidth: Int = 0 56 private var handleRegionHeight = 0 57 private var lastAcceptedPos = 0 58 @VisibleForTesting var handleY: IntRange = 0..0 59 private var canResize = false 60 private var resized = false 61 private var isDarkMode = false 62 private var decorThemeUtil = DecorThemeUtil(context) 63 64 /** 65 * Tracks divider bar visible bounds in screen-based coordination. Used to calculate with 66 * insets. 67 */ 68 private val dividerBounds = Rect() 69 private var dividerBar: FrameLayout? = null 70 private lateinit var dragDetector: DragDetector 71 72 constructor(context: Context) : super(context) 73 74 constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) 75 76 constructor( 77 context: Context, 78 attrs: AttributeSet?, 79 defStyleAttr: Int, 80 ) : super(context, attrs, defStyleAttr) 81 82 constructor( 83 context: Context, 84 attrs: AttributeSet?, 85 defStyleAttr: Int, 86 defStyleRes: Int, 87 ) : super(context, attrs, defStyleAttr, defStyleRes) 88 89 /** Sets up essential dependencies of the divider bar. */ setupnull90 fun setup( 91 dividerMoveCallback: DividerMoveCallback, 92 dividerBounds: Rect, 93 handleRegionSize: Size, 94 isDarkMode: Boolean, 95 ) { 96 callback = dividerMoveCallback 97 this.dividerBounds.set(dividerBounds) 98 this.isDarkMode = isDarkMode 99 paint.color = decorThemeUtil.getColorScheme(isDarkMode).outlineVariant.toArgb() 100 handle.setIsLeftRightSplit(true) 101 handle.setup(/* isSplitScreen= */ false, isDarkMode) 102 corners.setIsLeftRightSplit(true) 103 corners.setup(/* isSplitScreen= */ false, paint.color) 104 handleRegionHeight = handleRegionSize.height 105 handleRegionWidth = handleRegionSize.width 106 cornersRadius = 107 context.display.getRoundedCorner(RoundedCorner.POSITION_TOP_LEFT)?.radius ?: 0 108 initHandleYCoordinates() 109 dragDetector = 110 DragDetector( 111 this, 112 /* holdToDragMinDurationMs= */ 0, 113 ViewConfiguration.get(mContext).scaledTouchSlop, 114 ) 115 } 116 onUiModeChangenull117 fun onUiModeChange(isDarkMode: Boolean) { 118 this.isDarkMode = isDarkMode 119 handle.onUiModeChange(isDarkMode) 120 paint.color = decorThemeUtil.getColorScheme(isDarkMode).outlineVariant.toArgb() 121 corners.onUiModeChange(paint.color) 122 invalidate() 123 } 124 onTaskInfoChangenull125 fun onTaskInfoChange() { 126 decorThemeUtil = DecorThemeUtil(context) 127 if (paint.color != decorThemeUtil.getColorScheme(isDarkMode).outlineVariant.toArgb()) { 128 paint.color = decorThemeUtil.getColorScheme(isDarkMode).outlineVariant.toArgb() 129 corners.onCornerColorChange(paint.color) 130 invalidate() 131 } 132 } 133 onFinishInflatenull134 override fun onFinishInflate() { 135 super.onFinishInflate() 136 dividerBar = requireViewById(R.id.divider_bar) 137 handle = requireViewById(R.id.docked_divider_handle) 138 corners = requireViewById(R.id.docked_divider_rounded_corner) 139 touchElevation = 140 resources.getDimensionPixelSize(R.dimen.docked_stack_divider_lift_elevation) 141 setOnTouchListener(this) 142 setWillNotDraw(false) 143 val isDarkMode = 144 context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == 145 Configuration.UI_MODE_NIGHT_YES 146 paint.color = decorThemeUtil.getColorScheme(isDarkMode).outlineVariant.toArgb() 147 paint.isAntiAlias = true 148 paint.style = Paint.Style.FILL 149 } 150 onLayoutnull151 override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { 152 super.onLayout(changed, left, top, right, bottom) 153 if (changed) { 154 val dividerSize = resources.getDimensionPixelSize(R.dimen.split_divider_bar_width) 155 val backgroundLeft = (width - dividerSize) / 2 156 val backgroundTop = 0 157 val backgroundRight = backgroundLeft + dividerSize 158 val backgroundBottom = height 159 backgroundRect.set(backgroundLeft, backgroundTop, backgroundRight, backgroundBottom) 160 } 161 } 162 onResolvePointerIconnull163 override fun onResolvePointerIcon(event: MotionEvent, pointerIndex: Int): PointerIcon = 164 PointerIcon.getSystemIcon(context, PointerIcon.TYPE_HORIZONTAL_DOUBLE_ARROW) 165 166 override fun onTouch(v: View, event: MotionEvent): Boolean = 167 dragDetector.onMotionEvent(v, event) 168 169 private fun setTouching() { 170 handle.setTouching(true, true) 171 // Lift handle as well so it doesn't get behind the background, even though it doesn't 172 // cast shadow. 173 handle 174 .animate() 175 .setInterpolator(Interpolators.TOUCH_RESPONSE) 176 .setDuration(TOUCH_ANIMATION_DURATION) 177 .translationZ(touchElevation.toFloat()) 178 .start() 179 } 180 releaseTouchingnull181 private fun releaseTouching() { 182 handle.setTouching(false, true) 183 handle 184 .animate() 185 .setInterpolator(Interpolators.FAST_OUT_SLOW_IN) 186 .setDuration(TOUCH_RELEASE_ANIMATION_DURATION) 187 .translationZ(0f) 188 .start() 189 } 190 onHoverEventnull191 override fun onHoverEvent(event: MotionEvent): Boolean { 192 if ( 193 !DeviceConfig.getBoolean( 194 DeviceConfig.NAMESPACE_SYSTEMUI, 195 SystemUiDeviceConfigFlags.CURSOR_HOVER_STATES_ENABLED, 196 /* defaultValue = */ false, 197 ) 198 ) { 199 return false 200 } 201 202 if (event.action == MotionEvent.ACTION_HOVER_ENTER) { 203 setHovering() 204 return true 205 } 206 if (event.action == MotionEvent.ACTION_HOVER_EXIT) { 207 releaseHovering() 208 return true 209 } 210 return false 211 } 212 213 @VisibleForTesting setHoveringnull214 fun setHovering() { 215 handle.setHovering(true, true) 216 handle 217 .animate() 218 .setInterpolator(Interpolators.TOUCH_RESPONSE) 219 .setDuration(TOUCH_ANIMATION_DURATION) 220 .translationZ(touchElevation.toFloat()) 221 .start() 222 } 223 onDrawnull224 override fun onDraw(canvas: Canvas) { 225 canvas.drawRect(backgroundRect, paint) 226 } 227 228 @VisibleForTesting releaseHoveringnull229 fun releaseHovering() { 230 handle.setHovering(false, true) 231 handle 232 .animate() 233 .setInterpolator(Interpolators.FAST_OUT_SLOW_IN) 234 .setDuration(TOUCH_RELEASE_ANIMATION_DURATION) 235 .translationZ(0f) 236 .start() 237 } 238 handleMotionEventnull239 override fun handleMotionEvent(v: View?, event: MotionEvent): Boolean { 240 val touchPos = event.rawX.toInt() 241 val yTouchPosInDivider = event.y.toInt() 242 when (event.actionMasked) { 243 MotionEvent.ACTION_DOWN -> { 244 if (!isWithinHandleRegion(yTouchPosInDivider)) return true 245 callback.onDividerMoveStart(touchPos, event) 246 setTouching() 247 canResize = true 248 } 249 250 MotionEvent.ACTION_MOVE -> { 251 if (!canResize) return true 252 if (!moving) { 253 startPos = touchPos 254 moving = true 255 } 256 257 val pos = dividerBounds.left + touchPos - startPos 258 if (callback.onDividerMove(pos)) { 259 lastAcceptedPos = touchPos 260 resized = true 261 } 262 } 263 264 MotionEvent.ACTION_CANCEL, 265 MotionEvent.ACTION_UP -> { 266 if (!canResize) return true 267 if (moving && resized) { 268 dividerBounds.left = dividerBounds.left + lastAcceptedPos - startPos 269 callback.onDividerMovedEnd(dividerBounds.left, event) 270 } 271 moving = false 272 canResize = false 273 resized = false 274 releaseTouching() 275 } 276 } 277 return true 278 } 279 isWithinHandleRegionnull280 private fun isWithinHandleRegion(touchYPos: Int): Boolean = touchYPos in handleY 281 282 private fun initHandleYCoordinates() { 283 val handleStartY = (dividerBounds.height() - handleRegionHeight) / 2 284 val handleEndY = handleStartY + handleRegionHeight 285 handleY = handleStartY..handleEndY 286 } 287 288 companion object { 289 const val TOUCH_ANIMATION_DURATION: Long = 150 290 const val TOUCH_RELEASE_ANIMATION_DURATION: Long = 200 291 private val TAG = TilingDividerView::class.java.simpleName 292 } 293 } 294