前言
最近项目中遇到一个需求,需要动态的计算出列表中内容显示的高度,然后动态显示列表需要显示内容的元素,开始一想,简单的很,就是异步在加载列表之前把数据解析出来,然后算下高度,重新再把内容赋值给数据源就OK了,你以为呢,这样就结束了?那你错了,问题大着呢,这样异步请求回来的数据,可能显示不全,可能显示错乱。。。那么怎么解决呢,尝试了很多方法。。。
一、异步获取TextView的高度
异步获取TextView文本的高度有多种,网上都有,简单介绍下:
1)通过onPreDrawListener
tempTextView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
//这个回调会调用多次,获取完行数记得注销监听
tempTextView.getViewTreeObserver().removeOnPreDrawListener(this);
int noteContentHeight = (int) (tempTextView.getLineCount() * textSize);
noteTotalHeight = noteTotalHeight - noteContentHeight;
.....
return false;
}
});
2)通过View 自带的post方法,异步获取
tempTextView.post(new Runnable() {
@Override
public void run() {
tempTextView.getViewTreeObserver().removeOnPreDrawListener(this);
int noteContentHeight = (int) (tempTextView.getLineCount() * textSize);
}
});
以上是比较常用的方法,需要注意的是,文本的textSize = textSize+ LineSpacingExtra;
二、遇到问题,解决问题
但是这样还不行,因为TextView需要绘制才能拿到内容的行高,要不然返回的全是0,所有,可以在Activity 的布局里写一个invisible的textView,背景设置成透明的,不影响界面显示,去加载内容,之后就能拿到高度了。然后根据需求需要显示的高度,做对比,不同类型的内容高度都算出来,然后计算高度,超过就不显示这个类型的内容了,没有超过就加到内容显示的数组。
if (index < contentList.size()) {
ContentModel model = contentList.get(index);
if (model.getType() == 0) {
tempTextView.setText(model.getContent());
tempTextView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
//这个回调会调用多次,获取完行数记得注销监听
tempTextView.getViewTreeObserver().removeOnPreDrawListener(this);
int contentHeight = (int) (tempTextView.getLineCount() * 69);
contentTotalHeight = contentTotalHeight - contentHeight;
tempContentList.add(model);
if (contentTotalHeight > 0) {
tempContentList.add(model);
}
return false;
}
});
} else if (model.getType() == 1) {
contentTotalHeight = contentTotalHeight - 89;
if (contentTotalHeight > 0) {
tempContentList.add(model);
}
} else if (model.getType() == 2) {
contentTotalHeight = contentTotalHeight - 83;
if (contentTotalHeight > 0) {
tempContentList.add(model);
}
}
}
类似这样的,89或者83是不同类型的高度,这里我写固定值,根据需求来计算,当然可能不止3种类型,理论上这样,是不是就可以把要显示的内容放到内容显示的列表里,返回再把数据重新赋值给源数据,显示就可以了。但是实际结果是,文字有不显示的,有显示错乱的,原因很简单,tempTextView.getViewTreeObserver().addOnPreDrawListener()这样,或者tempTextView.post(new Runnable() {})获取高度都是异步的操作,列表数据其他模块都加载完了,这个结果有可能都没有返回,所有就有上面所说的结果了。
后来想了下,那能不能等文本的高度算出来之后,再执行下一个数据的处理呢,当然可以,递归嘛,优化之后:
private void getContentList(ContentCallBack contentCallBack) {
if (index < contentList.size()) {
ContentModel model = contentList.get(index);
if (model.getType() == 0) {
tempTextView.setText(model.getContent());
tempTextView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
//这个回调会调用多次,获取完行数记得注销监听
tempTextView.getViewTreeObserver().removeOnPreDrawListener(this);
int contentHeight = (int) (tempTextView.getLineCount() * 69);
contentTotalHeight = contentTotalHeight - noteContentHeight;
tempContentList.add(model);
if (contentTotalHeight > 0) {
index++;
getContentList(contentCallBack);
} else {
if (noteContentCallBack != null) {
contentCallBack.success(tempContentList);
}
}
return false;
}
});
} else if (model.getType() == 1) {
contentTotalHeight = contentTotalHeight - 89;
if (contentTotalHeight > 0) {
tempContentList.add(model);
index++;
getContentList(contentCallBack);
} else {
if (contentCallBack != null) {
contentCallBack.success(tempContentList);
}
}
} else if (model.getType() == 2) {
contentTotalHeight = contentTotalHeight - 83;
if (contentTotalHeight > 0) {
tempContentList.add(model);
index++;
getContentList(contentCallBack);
} else {
if (contentCallBack != null) {
contentCallBack.success(tempContentList);
}
}
}
} else {
if (contentCallBack != null) {
contentCallBack.success(tempContentList);
}
}
}
private interface ContentCallBack {
void success(List<ContentModel> contentModels);
}
调用:
getNoteContentList(new ContentCallBack() {
@Override
public void success(List<ContentModel> contentModels) {
....
处理数据
}
});
外层调用,也需要用递归,不能用for循环去计算每个item内容的高度,for循环不会等你执行完异步再走下一个的index。
具体实现这里就不贴了,原理知道了就很简单了,这样使用了2次递归之后,返回的数据是正确的,而且不会错乱,不会不显示,长叹一口气,就一个内容显示搞这么复杂,然后连上我心爱的小米10测试机,run起来,看下结果,咳咳咳,完美,不管数据怎么刷新,都不会有错乱和显示异常的问题。但是总感觉好像太复杂了...2次递归
三、优化
下班之后,路上还是在想这个问题,有没有简单的方法呢,一步到位,而且没有异步也没有递归,TextView的行数怎么算出来的,回家后就网上看了看,稍微看了下源码,找到了以下的方法:
/**
* 提前获取textview行数
*/
public static int getTextViewLines(TextView textView, int textViewWidth) {
int width = textViewWidth - textView.getCompoundPaddingLeft() - textView.getCompoundPaddingRight();
StaticLayout staticLayout;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
staticLayout = getStaticLayout23(textView, width);
} else {
staticLayout = getStaticLayout(textView, width);
}
int lines = staticLayout.getLineCount();
int maxLines = textView.getMaxLines();
if (maxLines > lines) {
return lines;
}
return maxLines;
}
/**
* sdk>=23
*/
@RequiresApi(api = Build.VERSION_CODES.M)
private static StaticLayout getStaticLayout23(TextView textView, int width) {
StaticLayout.Builder builder = StaticLayout.Builder.obtain(textView.getText(),
0, textView.getText().length(), textView.getPaint(), width)
.setAlignment(Layout.Alignment.ALIGN_NORMAL)
.setTextDirection(TextDirectionHeuristics.FIRSTSTRONG_LTR)
.setLineSpacing(textView.getLineSpacingExtra(), textView.getLineSpacingMultiplier())
.setIncludePad(textView.getIncludeFontPadding())
.setBreakStrategy(textView.getBreakStrategy())
.setHyphenationFrequency(textView.getHyphenationFrequency())
.setMaxLines(textView.getMaxLines() == -1 ? Integer.MAX_VALUE : textView.getMaxLines());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
builder.setJustificationMode(textView.getJustificationMode());
}
if (textView.getEllipsize() != null && textView.getKeyListener() == null) {
builder.setEllipsize(textView.getEllipsize())
.setEllipsizedWidth(width);
}
return builder.build();
}
/**
* sdk<23
*/
private static StaticLayout getStaticLayout(TextView textView, int width) {
return new StaticLayout(textView.getText(),
0, textView.getText().length(),
textView.getPaint(), width, Layout.Alignment.ALIGN_NORMAL,
textView.getLineSpacingMultiplier(),
textView.getLineSpacingExtra(), textView.getIncludeFontPadding(), textView.getEllipsize(),
width);
}
这个方法有个前提,就是要已知TextView的宽度,TextView内部的换行是通过一个StaticLayout的类来处理的,而且我们调用的getLineCount()方法最后也是调用的StaticLayout类中的getLineCount()方法,所以我们只需要创建一个和TextView内部一样的StaticLayout就可以了,然后调用staticLayout.getLineCount()
方法就可以获取到和当前TextView行数一样的值了。
总结
只能说自己这块涉及的少,首先想到的还是通过一般的实现逻辑去解决问题,没有仔细去研究原理,可能多走了弯路,但是也是增长了知识,希望对遇到类似的问题的同学有所帮助。