文字列がレンダリングされる前にTextView
で占める行数を取得するにはどうすればよいですか。
ViewTreeObserver
はレンダリングされた後にのみ発生するため、機能しません。
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);
Wordの破損を避けるためにWord全体が次の行に配置されている場合、受け入れられた回答は機能しません。
|hello |
|world! |
行数を100%確実にする唯一の方法は、TextViewが使用するのと同じテキストフローエンジンを使用することです。 TextViewはそのリフローロジックを共有しないため、テキストを複数の行に分割するカスタム文字列プロセッサがあります。各行は、指定された幅に適合します。また、Word全体が収まらない限り、単語を壊さないように最善を尽くします。
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));
これで、レンダリングする必要なく、TextViewの行数を100%確実にすることができます。
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()
TextViewの親の幅を知っているか、または決定できる場合は、行数が計算される結果となるビュー測定を呼び出すことができます。
val parentWidth = PARENT_WIDTH // assumes this is known/can be found
myTextView.measure(
MeasureSpec.makeMeasureSpec(parentWidth, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED))
TextViewのlayout
はnullではなくなり、計算された行数をmyTextView.lineCount
で確認できます。
@ 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の行を取得します。
これは上のリンクの私のコードベースです。それは私のために働いています。
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));