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; 41 42 /** Resolved layout direction */ 43 private int mResolvedDirection = LAYOUT_DIRECTION_UNDEFINED; 44 private int mMaxLinesForHeight = -1; 45 private int mLayoutMaxLines = -1; 46 private int mImageEndMargin; 47 48 public ImageFloatingTextView(Context context) { 49 this(context, null); 50 } 51 52 public ImageFloatingTextView(Context context, @Nullable AttributeSet attrs) { 53 this(context, attrs, 0); 54 } 55 56 public ImageFloatingTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 57 this(context, attrs, defStyleAttr, 0); 58 } 59 60 public ImageFloatingTextView(Context context, AttributeSet attrs, int defStyleAttr, 61 int defStyleRes) { 62 super(context, attrs, defStyleAttr, defStyleRes); 63 } 64 65 @Override 66 protected Layout makeSingleLayout(int wantWidth, BoringLayout.Metrics boring, int ellipsisWidth, 67 Layout.Alignment alignment, boolean shouldEllipsize, 68 TextUtils.TruncateAt effectiveEllipsize, boolean useSaved) { 69 TransformationMethod transformationMethod = getTransformationMethod(); 70 CharSequence text = getText(); 71 if (transformationMethod != null) { 72 text = transformationMethod.getTransformation(text, this); 73 } 74 text = text == null ? "" : text; 75 StaticLayout.Builder builder = StaticLayout.Builder.obtain(text, 0, text.length(), 76 getPaint(), wantWidth) 77 .setAlignment(alignment) 78 .setTextDirection(getTextDirectionHeuristic()) 79 .setLineSpacing(getLineSpacingExtra(), getLineSpacingMultiplier()) 80 .setIncludePad(getIncludeFontPadding()) 81 .setUseLineSpacingFromFallbacks(true) 82 .setBreakStrategy(Layout.BREAK_STRATEGY_HIGH_QUALITY) 83 .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_FULL); 84 int maxLines; 85 if (mMaxLinesForHeight > 0) { 86 maxLines = mMaxLinesForHeight; 87 } else { 88 maxLines = getMaxLines() >= 0 ? getMaxLines() : Integer.MAX_VALUE; 89 } 90 builder.setMaxLines(maxLines); 91 mLayoutMaxLines = maxLines; 92 if (shouldEllipsize) { 93 builder.setEllipsize(effectiveEllipsize) 94 .setEllipsizedWidth(ellipsisWidth); 95 } 96 97 // we set the endmargin on the requested number of lines. 98 int[] margins = null; 99 if (mIndentLines > 0) { 100 margins = new int[mIndentLines + 1]; 101 for (int i = 0; i < mIndentLines; i++) { 102 margins[i] = mImageEndMargin; 103 } 104 } 105 if (mResolvedDirection == LAYOUT_DIRECTION_RTL) { 106 builder.setIndents(margins, null); 107 } else { 108 builder.setIndents(null, margins); 109 } 110 111 return builder.build(); 112 } 113 114 @RemotableViewMethod 115 public void setImageEndMargin(int imageEndMargin) { 116 mImageEndMargin = imageEndMargin; 117 } 118 119 @Override 120 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 121 int availableHeight = MeasureSpec.getSize(heightMeasureSpec) - mPaddingTop - mPaddingBottom; 122 if (getLayout() != null && getLayout().getHeight() != availableHeight) { 123 // We've been measured before and the new size is different than before, lets make sure 124 // we reset the maximum lines, otherwise we may be cut short 125 mMaxLinesForHeight = -1; 126 nullLayouts(); 127 } 128 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 129 Layout layout = getLayout(); 130 if (layout.getHeight() > availableHeight) { 131 // With the existing layout, not all of our lines fit on the screen, let's find the 132 // first one that fits and ellipsize at that one. 133 int maxLines = layout.getLineCount() - 1; 134 while (maxLines > 1 && layout.getLineBottom(maxLines - 1) > availableHeight) { 135 maxLines--; 136 } 137 if (getMaxLines() > 0) { 138 maxLines = Math.min(getMaxLines(), maxLines); 139 } 140 // Only if the number of lines is different from the current layout, we recreate it. 141 if (maxLines != mLayoutMaxLines) { 142 mMaxLinesForHeight = maxLines; 143 nullLayouts(); 144 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 145 } 146 } 147 } 148 149 @Override 150 public void onRtlPropertiesChanged(int layoutDirection) { 151 super.onRtlPropertiesChanged(layoutDirection); 152 153 if (layoutDirection != mResolvedDirection && isLayoutDirectionResolved()) { 154 mResolvedDirection = layoutDirection; 155 if (mIndentLines > 0) { 156 // Invalidate layout. 157 nullLayouts(); 158 requestLayout(); 159 } 160 } 161 } 162 163 @RemotableViewMethod 164 public void setHasImage(boolean hasImage) { 165 setNumIndentLines(hasImage ? 2 : 0); 166 } 167 168 /** 169 * @param lines the number of lines at the top that should be indented by indentEnd 170 * @return whether a change was made 171 */ 172 public boolean setNumIndentLines(int lines) { 173 if (mIndentLines != lines) { 174 mIndentLines = lines; 175 // Invalidate layout. 176 nullLayouts(); 177 requestLayout(); 178 return true; 179 } 180 return false; 181 } 182 } 183