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