关于Android 动态获取列表中TextView的内容文本高度问题

3,442 阅读4分钟

这是我参与11月更文挑战的第13天,活动详情查看:2021最后一次更文挑战

前言

最近项目中遇到一个需求,需要动态的计算出列表中内容显示的高度,然后动态显示列表需要显示内容的元素,开始一想,简单的很,就是异步在加载列表之前把数据解析出来,然后算下高度,重新再把内容赋值给数据源就OK了,你以为呢,这样就结束了?那你错了,问题大着呢,这样异步请求回来的数据,可能显示不全,可能显示错乱。。。那么怎么解决呢,尝试了很多方法。。。

一、异步获取TextView的高度

异步获取TextView文本的高度有多种,网上都有,简单介绍下:

1)通过onPreDrawListener

tempTextView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
    @Override
    public boolean onPreDraw() {
        //这个回调会调用多次,获取完行数记得注销监听
        tempTextView.getViewTreeObserver().removeOnPreDrawListener(this);
        int contentHeight = (int) (tempTextView.getLineCount() * textSize);
        totalHeight = totalHeight - contentHeight;
        .....
        return false;
    }
});

2)通过View 自带的post方法,异步获取

tempTextView.post(new Runnable() {
    @Override
    public void run() {
        tempTextView.getViewTreeObserver().removeOnPreDrawListener(this);
        int contentHeight = (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行数一样的值了。

总结

只能说自己这块涉及的少,首先想到的还是通过一般的实现逻辑去解决问题,没有仔细去研究原理,可能多走了弯路,但是也是增长了知识,希望对遇到类似的问题的同学有所帮助。