1 /*
2  * Copyright (C) 2021 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.util
18 
19 import android.content.Context
20 import android.content.res.Configuration
21 import android.util.AttributeSet
22 import android.util.DisplayMetrics
23 import android.util.TypedValue
24 import android.widget.LinearLayout
25 import android.widget.TextView
26 import com.android.systemui.R
27 
28 /**
29  * Horizontal [LinearLayout] to contain some text.
30  *
31  * The height of this container can alternate between two different heights, depending on whether
32  * the text takes one line or more.
33  *
34  * When the text takes multiple lines, it will use the values in the regular attributes (`padding`,
35  * `layout_height`). The single line behavior must be set in XML.
36  *
37  * XML attributes for single line behavior:
38  * * `systemui:textViewId`: set the id for the [TextView] that determines the height of the
39  *   container
40  * * `systemui:singleLineHeight`: sets the height of the view when the text takes up only one line.
41  *   By default, it will use [getMinimumHeight].
42  * * `systemui:singleLineVerticalPadding`: sets the padding (top and bottom) when then text takes up
43  * only one line. By default, it is 0.
44  *
45  * All dimensions are updated when configuration changes.
46  */
47 class DualHeightHorizontalLinearLayout @JvmOverloads constructor(
48     context: Context,
49     attrs: AttributeSet? = null,
50     defStyleAttrs: Int = 0,
51     defStyleRes: Int = 0
52 ) : LinearLayout(context, attrs, defStyleAttrs, defStyleRes) {
53 
54     private val singleLineHeightValue: TypedValue?
55     private var singleLineHeightPx = 0
56 
57     private val singleLineVerticalPaddingValue: TypedValue?
58     private var singleLineVerticalPaddingPx = 0
59 
60     private val textViewId: Int
61     private var textView: TextView? = null
62 
63     private val displayMetrics: DisplayMetrics
64         get() = context.resources.displayMetrics
65 
66     private var initialPadding = mPaddingTop // All vertical padding is the same
67 
68     init {
69         if (orientation != HORIZONTAL) {
70             throw IllegalStateException("This view should always have horizontal orientation")
71         }
72 
73         val ta = context.obtainStyledAttributes(
74                 attrs,
75                 R.styleable.DualHeightHorizontalLinearLayout, defStyleAttrs, defStyleRes
76         )
77 
78         val tempHeight = TypedValue()
79         singleLineHeightValue = if (
80                 ta.hasValue(R.styleable.DualHeightHorizontalLinearLayout_singleLineHeight)
81         ) {
82             ta.getValue(R.styleable.DualHeightHorizontalLinearLayout_singleLineHeight, tempHeight)
83             tempHeight
84         } else {
85             null
86         }
87 
88         val tempPadding = TypedValue()
89         singleLineVerticalPaddingValue = if (
90                 ta.hasValue(R.styleable.DualHeightHorizontalLinearLayout_singleLineVerticalPadding)
91         ) {
92             ta.getValue(
93                     R.styleable.DualHeightHorizontalLinearLayout_singleLineVerticalPadding,
94                     tempPadding
95             )
96             tempPadding
97         } else {
98             null
99         }
100 
101         textViewId = ta.getResourceId(R.styleable.DualHeightHorizontalLinearLayout_textViewId, 0)
102 
103         ta.recycle()
104     }
105 
106     init {
107         updateResources()
108     }
109 
110     override fun setPadding(left: Int, top: Int, right: Int, bottom: Int) {
111         super.setPadding(left, top, right, bottom)
112         initialPadding = top
113     }
114 
115     override fun setPaddingRelative(start: Int, top: Int, end: Int, bottom: Int) {
116         super.setPaddingRelative(start, top, end, bottom)
117         initialPadding = top
118     }
119 
120     override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
121         super.onMeasure(widthMeasureSpec, heightMeasureSpec)
122         textView?.let { tv ->
123             if (tv.lineCount < 2) {
124                 setMeasuredDimension(measuredWidth, singleLineHeightPx)
125                 mPaddingBottom = 0
126                 mPaddingTop = 0
127             } else {
128                 mPaddingBottom = initialPadding
129                 mPaddingTop = initialPadding
130             }
131         }
132     }
133 
134     override fun onFinishInflate() {
135         super.onFinishInflate()
136         textView = findViewById(textViewId)
137     }
138 
139     override fun onConfigurationChanged(newConfig: Configuration?) {
140         super.onConfigurationChanged(newConfig)
141         updateResources()
142     }
143 
144     override fun setOrientation(orientation: Int) {
145         if (orientation == VERTICAL) {
146             throw IllegalStateException("This view should always have horizontal orientation")
147         }
148         super.setOrientation(orientation)
149     }
150 
151     private fun updateResources() {
152         updateDimensionValue(singleLineHeightValue, minimumHeight, ::singleLineHeightPx::set)
153         updateDimensionValue(singleLineVerticalPaddingValue, 0, ::singleLineVerticalPaddingPx::set)
154     }
155 
156     private inline fun updateDimensionValue(
157         tv: TypedValue?,
158         defaultValue: Int,
159         propertySetter: (Int) -> Unit
160     ) {
161         val value = tv?.let {
162             if (it.resourceId != 0) {
163                 context.resources.getDimensionPixelSize(it.resourceId)
164             } else {
165                 it.getDimension(displayMetrics).toInt()
166             }
167         } ?: defaultValue
168         propertySetter(value)
169     }
170 }