1 /*
2  * Copyright (C) 2022 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.decor
18 
19 import android.content.Context
20 import android.content.res.ColorStateList
21 import android.view.DisplayCutout
22 import android.view.Gravity
23 import android.view.Surface
24 import android.view.View
25 import android.view.ViewGroup
26 import android.widget.FrameLayout
27 import android.widget.ImageView
28 import com.android.systemui.R
29 
30 class RoundedCornerDecorProviderImpl(
31     override val viewId: Int,
32     @DisplayCutout.BoundsPosition override val alignedBound1: Int,
33     @DisplayCutout.BoundsPosition override val alignedBound2: Int,
34     private val roundedCornerResDelegate: RoundedCornerResDelegate
35 ) : CornerDecorProvider() {
36 
37     private val isTop = alignedBounds.contains(DisplayCutout.BOUNDS_POSITION_TOP)
38 
39     override fun inflateView(
40         context: Context,
41         parent: ViewGroup,
42         @Surface.Rotation rotation: Int,
43         tintColor: Int
44     ): View {
45         return ImageView(context).also { view ->
46             // View
47             view.id = viewId
48             initView(view, rotation, tintColor)
49 
50             // LayoutParams
51             val layoutSize = if (isTop) {
52                 roundedCornerResDelegate.topRoundedSize
53             } else {
54                 roundedCornerResDelegate.bottomRoundedSize
55             }
56             val params = FrameLayout.LayoutParams(
57                 layoutSize.width,
58                 layoutSize.height,
59                 alignedBound1.toLayoutGravity(rotation) or alignedBound2.toLayoutGravity(rotation)
60             )
61 
62             // AddView
63             parent.addView(view, params)
64         }
65     }
66 
67     private fun initView(
68         view: ImageView,
69         @Surface.Rotation rotation: Int,
70         tintColor: Int
71     ) {
72         view.setRoundedCornerImage(roundedCornerResDelegate, isTop)
73         view.adjustRotation(alignedBounds, rotation)
74         view.imageTintList = ColorStateList.valueOf(tintColor)
75     }
76 
77     override fun onReloadResAndMeasure(
78         view: View,
79         reloadToken: Int,
80         @Surface.Rotation rotation: Int,
81         tintColor: Int,
82         displayUniqueId: String?
83     ) {
84         roundedCornerResDelegate.updateDisplayUniqueId(displayUniqueId, reloadToken)
85 
86         initView((view as ImageView), rotation, tintColor)
87 
88         val layoutSize = if (isTop) {
89             roundedCornerResDelegate.topRoundedSize
90         } else {
91             roundedCornerResDelegate.bottomRoundedSize
92         }
93         (view.layoutParams as FrameLayout.LayoutParams).let {
94             it.width = layoutSize.width
95             it.height = layoutSize.height
96             it.gravity = alignedBound1.toLayoutGravity(rotation) or
97                 alignedBound2.toLayoutGravity(rotation)
98             view.setLayoutParams(it)
99         }
100     }
101 }
102 
103 @DisplayCutout.BoundsPosition
104 private fun Int.toLayoutGravity(@Surface.Rotation rotation: Int): Int = when (rotation) {
105     Surface.ROTATION_0 -> when (this) {
106         DisplayCutout.BOUNDS_POSITION_LEFT -> Gravity.LEFT
107         DisplayCutout.BOUNDS_POSITION_TOP -> Gravity.TOP
108         DisplayCutout.BOUNDS_POSITION_RIGHT -> Gravity.RIGHT
109         else /* DisplayCutout.BOUNDS_POSITION_BOTTOM */ -> Gravity.BOTTOM
110     }
111     Surface.ROTATION_90 -> when (this) {
112         DisplayCutout.BOUNDS_POSITION_LEFT -> Gravity.BOTTOM
113         DisplayCutout.BOUNDS_POSITION_TOP -> Gravity.LEFT
114         DisplayCutout.BOUNDS_POSITION_RIGHT -> Gravity.TOP
115         else /* DisplayCutout.BOUNDS_POSITION_BOTTOM */ -> Gravity.RIGHT
116     }
117     Surface.ROTATION_270 -> when (this) {
118         DisplayCutout.BOUNDS_POSITION_LEFT -> Gravity.TOP
119         DisplayCutout.BOUNDS_POSITION_TOP -> Gravity.RIGHT
120         DisplayCutout.BOUNDS_POSITION_RIGHT -> Gravity.BOTTOM
121         else /* DisplayCutout.BOUNDS_POSITION_BOTTOM */ -> Gravity.LEFT
122     }
123     else /* Surface.ROTATION_180 */ -> when (this) {
124         DisplayCutout.BOUNDS_POSITION_LEFT -> Gravity.RIGHT
125         DisplayCutout.BOUNDS_POSITION_TOP -> Gravity.BOTTOM
126         DisplayCutout.BOUNDS_POSITION_RIGHT -> Gravity.LEFT
127         else /* DisplayCutout.BOUNDS_POSITION_BOTTOM */ -> Gravity.TOP
128     }
129 }
130 
131 private fun ImageView.setRoundedCornerImage(
132     resDelegate: RoundedCornerResDelegate,
133     isTop: Boolean
134 ) {
135     val drawable = if (isTop)
136         resDelegate.topRoundedDrawable
137     else
138         resDelegate.bottomRoundedDrawable
139 
140     if (drawable != null) {
141         setImageDrawable(drawable)
142     } else {
143         setImageResource(
144             if (isTop)
145                 R.drawable.rounded_corner_top
146             else
147                 R.drawable.rounded_corner_bottom
148         )
149     }
150 }
151 
152 /**
153  * Configures the rounded corner drawable's view matrix based on the gravity.
154  *
155  * The gravity describes which corner to configure for, and the drawable we are rotating is assumed
156  * to be oriented for the top-left corner of the device regardless of the target corner.
157  * Therefore we need to rotate 180 degrees to get a bottom-left corner, and mirror in the x- or
158  * y-axis for the top-right and bottom-left corners.
159  */
160 private fun ImageView.adjustRotation(alignedBounds: List<Int>, @Surface.Rotation rotation: Int) {
161     var newRotation = 0F
162     var newScaleX = 1F
163     var newScaleY = 1F
164 
165     val isTop = alignedBounds.contains(DisplayCutout.BOUNDS_POSITION_TOP)
166     val isLeft = alignedBounds.contains(DisplayCutout.BOUNDS_POSITION_LEFT)
167     when (rotation) {
168         Surface.ROTATION_0 -> when {
169             isTop && isLeft -> {}
170             isTop && !isLeft -> { newScaleX = -1F }
171             !isTop && isLeft -> { newScaleY = -1F }
172             else /* !isTop && !isLeft */ -> { newRotation = 180F }
173         }
174         Surface.ROTATION_90 -> when {
175             isTop && isLeft -> { newScaleY = -1F }
176             isTop && !isLeft -> {}
177             !isTop && isLeft -> { newRotation = 180F }
178             else /* !isTop && !isLeft */ -> { newScaleX = -1F }
179         }
180         Surface.ROTATION_270 -> when {
181             isTop && isLeft -> { newScaleX = -1F }
182             isTop && !isLeft -> { newRotation = 180F }
183             !isTop && isLeft -> {}
184             else /* !isTop && !isLeft */ -> { newScaleY = -1F }
185         }
186         else /* Surface.ROTATION_180 */ -> when {
187             isTop && isLeft -> { newRotation = 180F }
188             isTop && !isLeft -> { newScaleY = -1F }
189             !isTop && isLeft -> { newScaleX = -1F }
190             else /* !isTop && !isLeft */ -> {}
191         }
192     }
193 
194     this.rotation = newRotation
195     this.scaleX = newScaleX
196     this.scaleY = newScaleY
197 }
198