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 }