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