如何获取字符串在TextView
呈现之前将占用的行数。
AViewTreeObserver
将不起作用,因为它们仅在渲染后才被触发。
如何获取字符串在TextView
呈现之前将占用的行数。
AViewTreeObserver
将不起作用,因为它们仅在渲染后才被触发。
当整个单词放在下一行时,接受的答案不起作用,以避免破坏单词:
|hello |
|world! |
100% 确定行数的唯一方法是使用与 TextView 相同的文本流引擎。由于 TextView 不共享其重排逻辑,这里有一个自定义字符串处理器,它将文本分成多行,每行都适合给定的宽度。除非整个单词不适合,否则它也会尽力不破坏单词:
public List<String> splitWordsIntoStringsThatFit(String source, float maxWidthPx, Paint paint) {
ArrayList<String> result = new ArrayList<>();
ArrayList<String> currentLine = new ArrayList<>();
String[] sources = source.split("\\s");
for(String chunk : sources) {
if(paint.measureText(chunk) < maxWidthPx) {
processFitChunk(maxWidthPx, paint, result, currentLine, chunk);
} else {
//the chunk is too big, split it.
List<String> splitChunk = splitIntoStringsThatFit(chunk, maxWidthPx, paint);
for(String chunkChunk : splitChunk) {
processFitChunk(maxWidthPx, paint, result, currentLine, chunkChunk);
}
}
}
if(! currentLine.isEmpty()) {
result.add(TextUtils.join(" ", currentLine));
}
return result;
}
/**
* Splits a string to multiple strings each of which does not exceed the width
* of maxWidthPx.
*/
private List<String> splitIntoStringsThatFit(String source, float maxWidthPx, Paint paint) {
if(TextUtils.isEmpty(source) || paint.measureText(source) <= maxWidthPx) {
return Arrays.asList(source);
}
ArrayList<String> result = new ArrayList<>();
int start = 0;
for(int i = 1; i <= source.length(); i++) {
String substr = source.substring(start, i);
if(paint.measureText(substr) >= maxWidthPx) {
//this one doesn't fit, take the previous one which fits
String fits = source.substring(start, i - 1);
result.add(fits);
start = i - 1;
}
if (i == source.length()) {
String fits = source.substring(start, i);
result.add(fits);
}
}
return result;
}
/**
* Processes the chunk which does not exceed maxWidth.
*/
private void processFitChunk(float maxWidth, Paint paint, ArrayList<String> result, ArrayList<String> currentLine, String chunk) {
currentLine.add(chunk);
String currentLineStr = TextUtils.join(" ", currentLine);
if (paint.measureText(currentLineStr) >= maxWidth) {
//remove chunk
currentLine.remove(currentLine.size() - 1);
result.add(TextUtils.join(" ", currentLine));
currentLine.clear();
//ok because chunk fits
currentLine.add(chunk);
}
}
这是单元测试的一部分:
String text = "Hello this is a very long and meanless chunk: abcdefghijkonetuhosnahrc.pgraoneuhnotehurc.pgansohtunsaohtu. Hope you like it!";
Paint paint = new Paint();
paint.setTextSize(30);
paint.setTypeface(Typeface.DEFAULT_BOLD);
List<String> strings = splitWordsIntoStringsThatFit(text, 50, paint);
assertEquals(3, strings.size());
assertEquals("Hello this is a very long and meanless chunk:", strings.get(0));
assertEquals("abcdefghijkonetuhosnahrc.pgraoneuhnotehurc.pganso", strings.get(1));
assertEquals("htunsaohtu. Hope you like it!", strings.get(2));
现在可以 100% 确定 TextView 中的行数,而无需渲染它:
TextView textView = ... //text view must be of fixed width
Paint paint = new Paint();
paint.setTextSize(yourTextViewTextSizePx);
paint.setTypeface(yourTextViewTypeface);
float textViewWidthPx = ...;
List<String> strings = splitWordsIntoStringsThatFit(yourText, textViewWidthPx, paint);
textView.setText(TextUtils.join("\n", strings);
int lineCount = strings.size(); //will be the same as textView.getLineCount()
final Rect bounds = new Rect();
final Paint paint = new Paint();
paint.setTextSize(currentTextSize);
paint.getTextBounds(testString, 0, testString.length(), bounds);
现在将文本的宽度除以 TextView 的宽度以获得总行数。
final int numLines = (int) Math.ceil((float) bounds.width() / currentSize);
currentSize:将呈现文本的视图的预期大小。大小不应超过屏幕宽度。
@denis-kniazhev 的答案非常好。但是,它使用自定义逻辑将文本分成几行。可以使用标准TextView
布局组件来测量文本。
这就是它的样子:
TextView myTextView = findViewById(R.id.text);
TextMeasurementUtils.TextMeasurementParams params = TextMeasurementUtils.TextMeasurementParams.Builder
.from(myTextView).build();
List<CharSequence> lines = TextMeasurementUtils.getTextLines(text, params);
TextMeasurementUtils.java
import android.os.Build;
import android.text.Layout;
import android.text.StaticLayout;
import android.text.TextDirectionHeuristic;
import android.text.TextPaint;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.List;
public class TextMeasurementUtils {
/**
* Split text into lines using specified parameters and the same algorithm
* as used by the {@link TextView} component
*
* @param text the text to split
* @param params the measurement parameters
* @return
*/
public static List<CharSequence> getTextLines(CharSequence text, TextMeasurementParams params) {
StaticLayout layout;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
StaticLayout.Builder builder = StaticLayout.Builder
.obtain(text, 0, text.length(), params.textPaint, params.width)
.setAlignment(params.alignment)
.setLineSpacing(params.lineSpacingExtra, params.lineSpacingMultiplier)
.setIncludePad(params.includeFontPadding)
.setBreakStrategy(params.breakStrategy)
.setHyphenationFrequency(params.hyphenationFrequency);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
builder.setJustificationMode(params.justificationMode);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
builder.setUseLineSpacingFromFallbacks(params.useFallbackLineSpacing);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
builder.setTextDirection((TextDirectionHeuristic) params.textDirectionHeuristic);
}
layout = builder.build();
} else {
layout = new StaticLayout(
text,
params.textPaint,
params.width,
params.alignment,
params.lineSpacingMultiplier,
params.lineSpacingExtra,
params.includeFontPadding);
}
List<CharSequence> result = new ArrayList<>();
for (int i = 0; i < layout.getLineCount(); i++) {
result.add(layout.getText().subSequence(layout.getLineStart(i), layout.getLineEnd(i)));
}
return result;
}
/**
* The text measurement parameters
*/
public static class TextMeasurementParams {
public final TextPaint textPaint;
public final Layout.Alignment alignment;
public final float lineSpacingExtra;
public final float lineSpacingMultiplier;
public final boolean includeFontPadding;
public final int breakStrategy;
public final int hyphenationFrequency;
public final int justificationMode;
public final boolean useFallbackLineSpacing;
public final Object textDirectionHeuristic;
public final int width;
private TextMeasurementParams(Builder builder) {
textPaint = requireNonNull(builder.textPaint);
alignment = requireNonNull(builder.alignment);
lineSpacingExtra = builder.lineSpacingExtra;
lineSpacingMultiplier = builder.lineSpacingMultiplier;
includeFontPadding = builder.includeFontPadding;
breakStrategy = builder.breakStrategy;
hyphenationFrequency = builder.hyphenationFrequency;
justificationMode = builder.justificationMode;
useFallbackLineSpacing = builder.useFallbackLineSpacing;
textDirectionHeuristic = builder.textDirectionHeuristic;
width = builder.width;
}
public static final class Builder {
private TextPaint textPaint;
private Layout.Alignment alignment;
private float lineSpacingExtra;
private float lineSpacingMultiplier = 1.0f;
private boolean includeFontPadding = true;
private int breakStrategy;
private int hyphenationFrequency;
private int justificationMode;
private boolean useFallbackLineSpacing;
private Object textDirectionHeuristic;
private int width;
public Builder() {
}
public Builder(TextMeasurementParams copy) {
this.textPaint = copy.textPaint;
this.alignment = copy.alignment;
this.lineSpacingExtra = copy.lineSpacingExtra;
this.lineSpacingMultiplier = copy.lineSpacingMultiplier;
this.includeFontPadding = copy.includeFontPadding;
this.breakStrategy = copy.breakStrategy;
this.hyphenationFrequency = copy.hyphenationFrequency;
this.justificationMode = copy.justificationMode;
this.useFallbackLineSpacing = copy.useFallbackLineSpacing;
this.textDirectionHeuristic = copy.textDirectionHeuristic;
this.width = copy.width;
}
public static Builder from(TextView view) {
Layout layout = view.getLayout();
Builder result = new Builder()
.textPaint(layout.getPaint())
.alignment(layout.getAlignment())
.width(view.getWidth() -
view.getCompoundPaddingLeft() - view.getCompoundPaddingRight());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
result.lineSpacingExtra(view.getLineSpacingExtra())
.lineSpacingMultiplier(view.getLineSpacingMultiplier())
.includeFontPadding(view.getIncludeFontPadding());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
result.breakStrategy(view.getBreakStrategy())
.hyphenationFrequency(view.getHyphenationFrequency());
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
result.justificationMode(view.getJustificationMode());
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
result.useFallbackLineSpacing(view.isFallbackLineSpacing());
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
result.textDirectionHeuristic(view.getTextDirectionHeuristic());
}
}
return result;
}
public Builder textPaint(TextPaint val) {
textPaint = val;
return this;
}
public Builder alignment(Layout.Alignment val) {
alignment = val;
return this;
}
public Builder lineSpacingExtra(float val) {
lineSpacingExtra = val;
return this;
}
public Builder lineSpacingMultiplier(float val) {
lineSpacingMultiplier = val;
return this;
}
public Builder includeFontPadding(boolean val) {
includeFontPadding = val;
return this;
}
public Builder breakStrategy(int val) {
breakStrategy = val;
return this;
}
public Builder hyphenationFrequency(int val) {
hyphenationFrequency = val;
return this;
}
public Builder justificationMode(int val) {
justificationMode = val;
return this;
}
public Builder useFallbackLineSpacing(boolean val) {
useFallbackLineSpacing = val;
return this;
}
public Builder textDirectionHeuristic(Object val) {
textDirectionHeuristic = val;
return this;
}
public Builder width(int val) {
width = val;
return this;
}
public TextMeasurementParams build() {
return new TextMeasurementParams(this);
}
}
}
public static <T> T requireNonNull(T obj) {
if (obj == null)
throw new NullPointerException();
return obj;
}
}
如果您知道或可以确定 TextView 父级的宽度,则可以调用导致计算行数的视图测量。
val parentWidth = PARENT_WIDTH // assumes this is known/can be found
myTextView.measure(
MeasureSpec.makeMeasureSpec(parentWidth, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED))
TextViewlayout
不再为空,您可以使用 . 检查计算的行数myTextView.lineCount
。
使用 kotlin 扩展函数doOnPreDraw
在视图渲染之前知道行数。例子:
my_text_view.text = "text with multiple lines \n\n\n"
my_text_view.doOnPreDraw {
// before it is drawn, use lineCount...
print(my_text_view.lineCount)
}
https://developer.android.com/reference/kotlin/androidx/core/view/package-summary#doonpredraw
感谢尤金·波波维奇,我得到了:
import android.os.Build
import android.text.Layout
import android.text.StaticLayout
import android.text.TextDirectionHeuristic
import android.text.TextPaint
import android.widget.TextView
object TextMeasurementUtil {
/**
* Split text into lines using specified parameters and the same algorithm
* as used by the [TextView] component
*
* @param text the text to split
* @param params the measurement parameters
* @return
*/
fun getTextLines(text: CharSequence, params: TextViewParams): List<CharSequence> {
val layout = getStaticLayout(text, params)
return (0 until layout.lineCount).map {
layout.text.subSequence(layout.getLineStart(it), layout.getLineEnd(it))
}
}
fun getTextLineCount(text: CharSequence, params: TextViewParams): Int {
val layout = getStaticLayout(text, params)
return layout.lineCount
}
fun getTextLines(textView: TextView): List<CharSequence> {
val layout = getStaticLayout(textView)
return (0 until layout.lineCount).map {
layout.text.subSequence(layout.getLineStart(it), layout.getLineEnd(it))
}
}
fun getTextLineCount(textView: TextView): Int {
val layout = getStaticLayout(textView)
return layout.lineCount
}
/**
* The text measurement parameters
*/
fun getTextViewParams(textView: TextView): TextViewParams {
val layout = textView.layout
val width = textView.width - textView.compoundPaddingLeft - textView.compoundPaddingRight
var lineSpacingExtra = 0f
var lineSpacingMultiplier = 1.0f
var includeFontPadding = true
var breakStrategy = 0
var hyphenationFrequency = 0
var justificationMode = 0
var useFallbackLineSpacing = false
var textDirectionHeuristic: TextDirectionHeuristic? = null
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
lineSpacingExtra = textView.lineSpacingExtra
lineSpacingMultiplier = textView.lineSpacingMultiplier
includeFontPadding = textView.includeFontPadding
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
breakStrategy = textView.breakStrategy
hyphenationFrequency = textView.hyphenationFrequency
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
justificationMode = textView.justificationMode
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
useFallbackLineSpacing = textView.isFallbackLineSpacing
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
textDirectionHeuristic = textView.textDirectionHeuristic
}
}
return TextViewParams(
textPaint = layout.paint,
alignment = layout.alignment,
lineSpacingExtra = lineSpacingExtra,
lineSpacingMultiplier = lineSpacingMultiplier,
includeFontPadding = includeFontPadding,
breakStrategy = breakStrategy,
hyphenationFrequency = hyphenationFrequency,
justificationMode = justificationMode,
useFallbackLineSpacing = useFallbackLineSpacing,
textDirectionHeuristic = textDirectionHeuristic,
width = width
)
}
private fun getStaticLayout(text: CharSequence,
params: TextViewParams): StaticLayout =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val builder = StaticLayout.Builder
.obtain(text, 0, text.length, params.textPaint, params.width)
.setAlignment(params.alignment)
.setLineSpacing(params.lineSpacingExtra, params.lineSpacingMultiplier)
.setIncludePad(params.includeFontPadding)
.setBreakStrategy(params.breakStrategy)
.setHyphenationFrequency(params.hyphenationFrequency)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
builder.setJustificationMode(params.justificationMode)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
builder.setUseLineSpacingFromFallbacks(params.useFallbackLineSpacing)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
builder.setTextDirection(params.textDirectionHeuristic!!)
}
builder.build()
} else {
@Suppress("DEPRECATION")
StaticLayout(
text,
params.textPaint,
params.width,
params.alignment,
params.lineSpacingMultiplier,
params.lineSpacingExtra,
params.includeFontPadding)
}
private fun getStaticLayout(textView: TextView): StaticLayout =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val builder = StaticLayout.Builder
.obtain(textView.text, 0, textView.text.length, textView.layout.paint,
textView.width)
.setAlignment(textView.layout.alignment)
.setLineSpacing(textView.lineSpacingExtra, textView.lineSpacingMultiplier)
.setIncludePad(textView.includeFontPadding)
.setBreakStrategy(textView.breakStrategy)
.setHyphenationFrequency(textView.hyphenationFrequency)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
builder.setJustificationMode(textView.justificationMode)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
builder.setUseLineSpacingFromFallbacks(textView.isFallbackLineSpacing)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
builder.setTextDirection(textView.textDirectionHeuristic)
}
builder.build()
} else {
@Suppress("DEPRECATION")
StaticLayout(
textView.text,
textView.layout.paint,
textView.width,
textView.layout.alignment,
textView.lineSpacingMultiplier,
textView.lineSpacingExtra,
textView.includeFontPadding)
}
data class TextViewParams(
val textPaint: TextPaint,
val alignment: Layout.Alignment,
val lineSpacingExtra: Float,
val lineSpacingMultiplier: Float,
val includeFontPadding: Boolean,
val breakStrategy: Int,
val hyphenationFrequency: Int,
val justificationMode: Int,
val useFallbackLineSpacing: Boolean,
val textDirectionHeuristic: TextDirectionHeuristic?,
val width: Int
)
}
用法:
如果要在 equal TextView
s 中打印不同的文本(例如,在RecyclerView
一个或类似ViewHolder
的 s 中):
val params = TextMeasurementUtil.getTextViewParams(textView)
val lines = TextMeasurementUtil.getTextLines(textView.text, params)
val count = TextMeasurementUtil.getTextLineCount(textView.text, params)
在任何其他情况下:
val lines = TextMeasurementUtil.getTextLines(textView)
val count = TextMeasurementUtil.getTextLineCount(textView)
在你调用或doOnPreDrawRecyclerView
方法之前你不会知道参数,所以使用:TextView
post
textView.doOnPreDraw {
val lines = TextMeasurementUtil.getTextLines(textView)
val count = TextMeasurementUtil.getTextLineCount(textView)
}
在渲染之前获取 TextView 的行。
这是我上面链接的代码库。它对我有用。
private int widthMeasureSpec;
private int heightMeasureSpec;
private int heightOfEachLine;
private int paddingFirstLine;
private void calculateHeightOfEachLine() {
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
Display display = wm.getDefaultDisplay();
Point size = new Point();
display.getSize(size);
int deviceWidth = size.x;
widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(deviceWidth, View.MeasureSpec.AT_MOST);
heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
//1 line = 76; 2 lines = 76 + 66; 3 lines = 76 + 66 + 66
//=> height of first line = 76 pixel; height of second line = third line =... n line = 66 pixel
int heightOfFirstLine = getHeightOfTextView("A");
int heightOfSecondLine = getHeightOfTextView("A\nA") - heightOfFirstLine;
paddingFirstLine = heightOfFirstLine - heightOfSecondLine;
heightOfEachLine = heightOfSecondLine;
}
private int getHeightOfTextView(String text) {
// Getting height of text view before rendering to layout
TextView textView = new TextView(context);
textView.setPadding(10, 0, 10, 0);
//textView.setTypeface(typeface);
textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, context.getResources().getDimension(R.dimen.tv_size_14sp));
textView.setText(text, TextView.BufferType.SPANNABLE);
textView.measure(widthMeasureSpec, heightMeasureSpec);
return textView.getMeasuredHeight();
}
private int getLineCountOfTextViewBeforeRendering(String text) {
return (getHeightOfTextView(text) - paddingFirstLine) / heightOfEachLine;
}
注意:此代码也必须设置为屏幕上的真实文本视图
textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, context.getResources().getDimension(R.dimen.tv_size_14sp));