【安卓开发】TextView显示带图片HTML踩坑记录
需求是显示一个网页,并且内容要可控,可以跟原生UI交互。
我用的是Compose,原生的Text是没有这种功能的,从0开始造轮子也不是不行,但是View体系已经有两个完整的解决方案了,没那个必要:
WebView
可以,但没必要,我只需要显示一个网页,并不需要太多交互,也不用跳转,WebView过于臃肿;其次就是交互很麻烦,得通过js hook,因此驳回。
TextView
可以通过Html.fromHtml()得到一个可以直接设置给TextView的对象,而且交互比较方便。
选择第二种方案以后,就开启了踩坑之路:
TextView无法滚动
解决方案:

2.无法显示图片
这里坑就比较多了,我按时间顺序整理一下:
a.异步加载
Html.fromHtml()的第三个参数是一个ImageGetter接口,要实现一个getDrawable方法,给你一个url,让你返回Drawable。
那么问题来了,这个方法是在主线程调用的,如果你在主线程加载图片,则卡死,如果 你开IO线程,那就没办法返回结果。
仔细想一下就会发现,这个问题无解,这一次加载一定会阻塞主线程,因此需要设计一套缓存机制,在第一次加载的时候记录下url,加载完成以后再次调用getDrawable,返回已经缓存到本地的图片。
所以我就写了一个帮助类:
首先是为什么不写成单例类,因为可能有多个页面都要使用,而且这个类的生命周期是跟页面绑定的,所以不能写成单例。
其次是图片数量限制,因为没办法控制HTML的内容,为了防止OOM把最大图片数量限制在了10张。
然后这里虽然涉及到了多线程访问,但是不同线程并不会互相干扰,所以没用ConcurrentHashMap,也没加锁。
scope是协程上下文,基本就是ViewModel的scope,前面说过这个类的生命周期跟页面绑定,所以页面销毁的时候,viewModel销毁,所有协程也跟着销毁。
以及页面没有销毁,但是需要加载新的HTML,也得调用reset()刷新缓存。
我预想中的调用流程是这样的:
getDrawable内部查询缓存,不为空直接使用,否则调用load,load会立即返回一个Drawable作为占位图片,然后去加载网络图片,加载完成以后缓存,然后回调onFinished,回调函数内部再次给TextView设置HTML,于是上述流程被重复。
因为每一次设置HTML都会加载所有图片,所以为了防止某个图片在加载过程中被重复加载,直接把占位符缓存进去,所以开头加了判断,直接返回占位符。
剩下没什么好说的,非常质朴的缓存。
b.ImageGetter()的缺陷
ImageGetter会把img的src属性传到getDrawable里,但我的src属性长这样:

真正的url在src里。
一开始我想修改ImageGetter类,或者别的什么,但是明显官方没给这个接口,反射也不是很好操作;然后想着要不提前把图片加载好,但是这样getDrawable内部依旧拿不到作为key的url,只能根据顺序决定使用哪一张图片,不太稳定;最后索性直接修改了HTML文档,把src替换成了src,原来的src换成别的不影响显示的属性。
c.Drawable
一切都弄好以后,图片还是不显示,然后发现Drawable还需要设置Rect。这个值直接给图片原大小的话,会显示不全,所以简单的缩放了两倍:

后续可以判断一下,超出屏幕就铺满宽度,然后根据宽度决定高度的缩放比例,就完美了。
中间还有一些小问题,省略。
以上。
过了很多天,发现一个新的问题:
页面销毁,不意味着HTML会改变,同一段HTML的图片不应该被多次加载,浪费流量。
不过好在coil是自带缓存的,替我兜了个底,不然还是得写成单例,然后根据当前url来决定是否清理缓存。