1 /*
2  * Copyright (C) 2020 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.deskclock.widget
18 
19 import android.content.Context
20 import android.util.AttributeSet
21 import android.view.View
22 import android.widget.LinearLayout
23 import android.widget.TextView
24 
25 /**
26  * When this layout is in the Horizontal orientation and one and only one child is a TextView with a
27  * non-null android:ellipsize, this layout will reduce android:maxWidth of that TextView to ensure
28  * the siblings are not truncated. This class is useful when that ellipsize-text-view "starts"
29  * before other children of this view group. This layout has no effect if:
30  * <ul>
31  *     <li>the orientation is not horizontal</li>
32  *     <li>any child has weights.</li>
33  *     <li>more than one child has a non-null android:ellipsize.</li>
34  * </ul>
35  *
36  * The purpose of this horizontal-linear-layout is to ensure that when the sum of widths of the
37  * children are greater than this parent, the maximum width of the ellipsize-text-view, is reduced
38  * so that no siblings are truncated.
39  *
40  *
41  * For example: Given Text1 has android:ellipsize="end" and Text2 has android:ellipsize="none",
42  * as Text1 and/or Text2 grow in width, both will consume more width until Text2 hits the end
43  * margin, then Text1 will cease to grow and instead shrink to accommodate any further growth in
44  * Text2.
45  * <ul>
46  * <li>|[text1]|[text2]              |</li>
47  * <li>|[text1 text1]|[text2 text2]  |</li>
48  * <li>|[text... ]|[text2 text2 text2]|</li>
49  * </ul>
50  */
51 class EllipsizeLayout @JvmOverloads constructor(
52     context: Context?,
53     attrs: AttributeSet? = null
54 ) : LinearLayout(context, attrs) {
55     /**
56      * This override only acts when the LinearLayout is in the Horizontal orientation and is in it's
57      * final measurement pass(MeasureSpec.EXACTLY). In this case only, this class
58      *
59      *  * Identifies the one TextView child with the non-null android:ellipsize.
60      *  * Re-measures the needed width of all children (by calling measureChildWithMargins with
61      * the width measure specification to MeasureSpec.UNSPECIFIED.)
62      *  * Sums the children's widths.
63      *  * Whenever the sum of the children's widths is greater than this parent was allocated,
64      * the maximum width of the one TextView child with the non-null android:ellipsize is
65      * reduced.
66      *
67      *
68      * @param widthMeasureSpec horizontal space requirements as imposed by the parent
69      * @param heightMeasureSpec vertical space requirements as imposed by the parent
70      */
onMeasurenull71     override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
72         if (orientation == HORIZONTAL &&
73                 MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY) {
74             var totalLength = 0
75             // If any of the constraints of this class are exceeded, outOfSpec becomes true
76             // and the no alterations are made to the ellipsize-text-view.
77             var outOfSpec = false
78             var ellipsizeView: TextView? = null
79             val count = childCount
80             val parentWidth = MeasureSpec.getSize(widthMeasureSpec)
81             val queryWidthMeasureSpec =
82                     MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(widthMeasureSpec),
83                             MeasureSpec.UNSPECIFIED)
84 
85             var ii = 0
86             while (ii < count && !outOfSpec) {
87                 val child = getChildAt(ii)
88                 if (child != null && child.visibility != View.GONE) {
89                     // Identify the ellipsize view
90                     if (child is TextView) {
91                         val tv = child
92                         if (tv.ellipsize != null) {
93                             if (ellipsizeView == null) {
94                                 ellipsizeView = tv
95                                 // Clear the maximum width on ellipsizeView before measurement
96                                 ellipsizeView.maxWidth = Int.MAX_VALUE
97                             } else {
98                                 // TODO: support multiple android:ellipsize
99                                 outOfSpec = true
100                             }
101                         }
102                     }
103                     // Ask the child to measure itself
104                     measureChildWithMargins(child, queryWidthMeasureSpec, 0, heightMeasureSpec, 0)
105 
106                     // Get the layout parameters to check for a weighted width and to add the
107                     // child's margins to the total length.
108                     val layoutParams = child.layoutParams as LayoutParams?
109                     if (layoutParams != null) {
110                         outOfSpec = outOfSpec or (layoutParams.weight > 0f)
111                         totalLength += (child.measuredWidth +
112                                 layoutParams.leftMargin + layoutParams.rightMargin)
113                     } else {
114                         outOfSpec = true
115                     }
116                 }
117                 ++ii
118             }
119             // Last constraint test
120             outOfSpec = outOfSpec or (ellipsizeView == null || totalLength == 0)
121 
122             if (!outOfSpec && totalLength > parentWidth) {
123                 var maxWidth = ellipsizeView!!.measuredWidth - (totalLength - parentWidth)
124                 // TODO: Respect android:minWidth (easy with @TargetApi(16))
125                 val minWidth = 0
126                 if (maxWidth < minWidth) {
127                     maxWidth = minWidth
128                 }
129                 ellipsizeView.maxWidth = maxWidth
130             }
131         }
132 
133         super.onMeasure(widthMeasureSpec, heightMeasureSpec)
134     }
135 }