1 /* 2 * Copyright (C) 2015 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.internal.widget; 18 19 import android.annotation.Nullable; 20 import android.content.Context; 21 import android.os.Build; 22 import android.os.Trace; 23 import android.text.BoringLayout; 24 import android.text.Layout; 25 import android.text.PrecomputedText; 26 import android.text.StaticLayout; 27 import android.text.TextUtils; 28 import android.text.method.TransformationMethod; 29 import android.util.AttributeSet; 30 import android.view.RemotableViewMethod; 31 import android.widget.RemoteViews; 32 import android.widget.TextView; 33 34 import com.android.internal.R; 35 36 /** 37 * A TextView that can float around an image on the end. 38 * 39 * @hide 40 */ 41 @RemoteViews.RemoteView 42 public class ImageFloatingTextView extends TextView { 43 44 /** Number of lines from the top to indent. */ 45 private int mIndentLines = 0; 46 /** Whether or not there is an image to indent for. */ 47 private boolean mHasImage = false; 48 49 /** Resolved layout direction */ 50 private int mResolvedDirection = LAYOUT_DIRECTION_UNDEFINED; 51 private int mMaxLinesForHeight = -1; 52 private int mLayoutMaxLines = -1; 53 private int mImageEndMargin; 54 private final int mMaxLineUpperLimit; 55 56 private int mStaticLayoutCreationCountInOnMeasure = 0; 57 58 private static final boolean TRACE_ONMEASURE = Build.isDebuggable(); 59 ImageFloatingTextView(Context context)60 public ImageFloatingTextView(Context context) { 61 this(context, null); 62 } 63 ImageFloatingTextView(Context context, @Nullable AttributeSet attrs)64 public ImageFloatingTextView(Context context, @Nullable AttributeSet attrs) { 65 this(context, attrs, 0); 66 } 67 ImageFloatingTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr)68 public ImageFloatingTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 69 this(context, attrs, defStyleAttr, 0); 70 } 71 ImageFloatingTextView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)72 public ImageFloatingTextView(Context context, AttributeSet attrs, int defStyleAttr, 73 int defStyleRes) { 74 super(context, attrs, defStyleAttr, defStyleRes); 75 setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_FULL_FAST); 76 setBreakStrategy(Layout.BREAK_STRATEGY_HIGH_QUALITY); 77 mMaxLineUpperLimit = 78 getResources().getInteger(R.integer.config_notificationLongTextMaxLineCount); 79 } 80 81 @Override makeSingleLayout(int wantWidth, BoringLayout.Metrics boring, int ellipsisWidth, Layout.Alignment alignment, boolean shouldEllipsize, TextUtils.TruncateAt effectiveEllipsize, boolean useSaved)82 protected Layout makeSingleLayout(int wantWidth, BoringLayout.Metrics boring, int ellipsisWidth, 83 Layout.Alignment alignment, boolean shouldEllipsize, 84 TextUtils.TruncateAt effectiveEllipsize, boolean useSaved) { 85 if (TRACE_ONMEASURE) { 86 Trace.beginSection("ImageFloatingTextView#makeSingleLayout"); 87 mStaticLayoutCreationCountInOnMeasure++; 88 } 89 TransformationMethod transformationMethod = getTransformationMethod(); 90 CharSequence text = getText(); 91 if (transformationMethod != null) { 92 text = transformationMethod.getTransformation(text, this); 93 } 94 text = text == null ? "" : text; 95 StaticLayout.Builder builder = StaticLayout.Builder.obtain(text, 0, text.length(), 96 getPaint(), wantWidth) 97 .setAlignment(alignment) 98 .setTextDirection(getTextDirectionHeuristic()) 99 .setLineSpacing(getLineSpacingExtra(), getLineSpacingMultiplier()) 100 .setIncludePad(getIncludeFontPadding()) 101 .setUseLineSpacingFromFallbacks(true) 102 .setBreakStrategy(getBreakStrategy()) 103 .setHyphenationFrequency(getHyphenationFrequency()); 104 int maxLines; 105 if (mMaxLinesForHeight > 0) { 106 maxLines = mMaxLinesForHeight; 107 } else { 108 maxLines = getMaxLines() >= 0 ? getMaxLines() : Integer.MAX_VALUE; 109 } 110 111 if (mMaxLineUpperLimit > 0) { 112 maxLines = Math.min(maxLines, mMaxLineUpperLimit); 113 } 114 115 builder.setMaxLines(maxLines); 116 mLayoutMaxLines = maxLines; 117 if (shouldEllipsize) { 118 builder.setEllipsize(effectiveEllipsize) 119 .setEllipsizedWidth(ellipsisWidth); 120 } 121 122 // we set the endmargin on the requested number of lines. 123 int[] margins = null; 124 if (mHasImage && mIndentLines > 0) { 125 margins = new int[mIndentLines + 1]; 126 for (int i = 0; i < mIndentLines; i++) { 127 margins[i] = mImageEndMargin; 128 } 129 } 130 if (mResolvedDirection == LAYOUT_DIRECTION_RTL) { 131 builder.setIndents(margins, null); 132 } else { 133 builder.setIndents(null, margins); 134 } 135 136 final StaticLayout result = builder.build(); 137 if (TRACE_ONMEASURE) { 138 trackMaxLines(); 139 Trace.endSection(); 140 } 141 return result; 142 } 143 144 /** 145 * @param imageEndMargin the end margin (in pixels) to indent the first few lines of the text 146 */ 147 @RemotableViewMethod setImageEndMargin(int imageEndMargin)148 public void setImageEndMargin(int imageEndMargin) { 149 if (mImageEndMargin != imageEndMargin) { 150 mImageEndMargin = imageEndMargin; 151 invalidateTextIfIndenting(); 152 } 153 } 154 155 /** 156 * @param imageEndMarginDp the end margin (in dp) to indent the first few lines of the text 157 */ 158 @RemotableViewMethod setImageEndMarginDp(float imageEndMarginDp)159 public void setImageEndMarginDp(float imageEndMarginDp) { 160 setImageEndMargin( 161 (int) (imageEndMarginDp * getResources().getDisplayMetrics().density)); 162 } 163 164 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)165 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 166 if (TRACE_ONMEASURE) { 167 Trace.beginSection("ImageFloatingTextView#onMeasure"); 168 } 169 mStaticLayoutCreationCountInOnMeasure = 0; 170 int availableHeight = MeasureSpec.getSize(heightMeasureSpec) - mPaddingTop - mPaddingBottom; 171 if (getLayout() != null && getLayout().getHeight() != availableHeight) { 172 // We've been measured before and the new size is different than before, lets make sure 173 // we reset the maximum lines, otherwise the last line of text may be partially cut off 174 mMaxLinesForHeight = -1; 175 nullLayouts(); 176 } 177 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 178 Layout layout = getLayout(); 179 if (layout.getHeight() > availableHeight) { 180 // With the existing layout, not all of our lines fit on the screen, let's find the 181 // first one that fits and ellipsize at that one. 182 int maxLines = layout.getLineCount(); 183 while (maxLines > 1 && layout.getLineBottom(maxLines - 1) > availableHeight) { 184 maxLines--; 185 } 186 if (getMaxLines() > 0) { 187 maxLines = Math.min(getMaxLines(), maxLines); 188 } 189 // Only if the number of lines is different from the current layout, we recreate it. 190 if (maxLines != mLayoutMaxLines) { 191 mMaxLinesForHeight = maxLines; 192 nullLayouts(); 193 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 194 } 195 } 196 197 198 if (TRACE_ONMEASURE) { 199 trackParameters(); 200 Trace.endSection(); 201 } 202 } 203 204 @Override onRtlPropertiesChanged(int layoutDirection)205 public void onRtlPropertiesChanged(int layoutDirection) { 206 super.onRtlPropertiesChanged(layoutDirection); 207 208 if (layoutDirection != mResolvedDirection && isLayoutDirectionResolved()) { 209 mResolvedDirection = layoutDirection; 210 invalidateTextIfIndenting(); 211 } 212 } 213 invalidateTextIfIndenting()214 private void invalidateTextIfIndenting() { 215 if (mHasImage && mIndentLines > 0) { 216 // Invalidate layout. 217 nullLayouts(); 218 requestLayout(); 219 } 220 } 221 222 /** 223 * @param hasImage whether there is an image to wrap text around. 224 */ 225 @RemotableViewMethod setHasImage(boolean hasImage)226 public void setHasImage(boolean hasImage) { 227 setHasImageAndNumIndentLines(hasImage, mIndentLines); 228 } 229 230 /** 231 * @param lines the number of lines at the top that should be indented by indentEnd 232 */ 233 @RemotableViewMethod setNumIndentLines(int lines)234 public void setNumIndentLines(int lines) { 235 setHasImageAndNumIndentLines(mHasImage, lines); 236 } 237 setHasImageAndNumIndentLines(boolean hasImage, int lines)238 private void setHasImageAndNumIndentLines(boolean hasImage, int lines) { 239 int oldEffectiveLines = mHasImage ? mIndentLines : 0; 240 int newEffectiveLines = hasImage ? lines : 0; 241 mIndentLines = lines; 242 mHasImage = hasImage; 243 if (oldEffectiveLines != newEffectiveLines) { 244 // always invalidate layout. 245 nullLayouts(); 246 requestLayout(); 247 } 248 } 249 trackParameters()250 private void trackParameters() { 251 if (!TRACE_ONMEASURE) { 252 return; 253 } 254 Trace.setCounter("ImageFloatingView#staticLayoutCreationCount", 255 mStaticLayoutCreationCountInOnMeasure); 256 Trace.setCounter("ImageFloatingView#isPrecomputedText", 257 isTextAPrecomputedText()); 258 } 259 /** 260 * @return 1 if {@link TextView#getText()} is PrecomputedText, else 0 261 */ isTextAPrecomputedText()262 private int isTextAPrecomputedText() { 263 final CharSequence text = getText(); 264 if (text == null) { 265 return 0; 266 } 267 268 if (text instanceof PrecomputedText) { 269 return 1; 270 } 271 272 return 0; 273 } 274 trackMaxLines()275 private void trackMaxLines() { 276 if (!TRACE_ONMEASURE) { 277 return; 278 } 279 280 Trace.setCounter("ImageFloatingView#layoutMaxLines", mLayoutMaxLines); 281 } 282 } 283