• 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 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