【Android】谷歌为什么不帮我默认实现啊,ImageGetter 和 TagHandler 的作用与区别
The following article is from Android补给站 Author Newki
本文作者
作者:Newki
链接:
https://juejin.cn/post/7413274784484130828
本文由作者授权发布。
因为本文是手机发布的,所以随便找了张自己拍的照片当封面。
前言
fromHtml(String source, ImageGetter imageGetter,TagHandler tagHandler)
是这么用的啊,凭什么 iOS 直接加载一个字符串就能显示,我们 Android 还得用 ImageGetter 和 TagHandler 的参数,这都是啥啊,为什么要整的这么复杂?
<br>,< p>,< div align=>,< strong>, <b>, <em>, <cite>, <dfn>, <i>, <big>, <small>, <font size=>, <font color=>, <blockquote>, <tt>, <a href=>,
<u>, <sup>, <sub>, <h1>,<h2>,<h3>,<h4>,<h5>,<h6>, <img src=>, <strike>
并且在 TagHandler 的钩子中我们可以自行解析实现一些标签甚至是自定义标签,实现特殊的效果,更加的灵活。(类似的解析框架有很多)
private void setHtmlWithImages(TextView textView, String htmlContent) {
Html.ImageGetter imageGetter = source -> {
try {
// 仅当 source 包含 base64 编码时才进行处理
if (source.contains("base64,")) {
byte[] decodedString = Base64.decode(source.substring(source.indexOf(",") + 1), Base64.DEFAULT);
Bitmap bitmap = BitmapFactory.decodeStream(new ByteArrayInputStream(decodedString));
if (bitmap == null) {
Log.e("ImageGetter", "Failed to decode base64 image");
} else {
Log.i("ImageGetter", "Decoded image successfully");
}
return new BitmapDrawable(textView.getResources(), bitmap) {
@Override
public void draw(Canvas canvas) {
setBounds(0, 0, bitmap.getWidth(), bitmap.getHeight());
super.draw(canvas);
}
};
} else {
Log.e("ImageGetter", "Image source does not contain base64");
return null;
}
} catch (Exception e) {
Log.e("ImageGetter", "Error decoding image", e);
return null;
}
};
Spanned spanned = Html.fromHtml(htmlContent, imageGetter, null);
textView.setText(spanned);
}
如果是加载网络 URL 图片呢,那选择更多了,比如我们使用 OkHttp 来获取图片数据,并使用 RxJava 来处理异步操作:
public void setHtmlWithImages(TextView textView, String htmlContent) {
Html.ImageGetter imageGetter = source -> {
try {
// 同步请求图片
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder().url(source).build();
Response response = client.newCall(request).execute();
if (!response.isSuccessful()) {
Log.e("ImageGetter", "Failed to download image: " + response);
return null;
}
byte[] bytes = response.body().bytes();
Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
if (bitmap != null) {
return new BitmapDrawable(textView.getResources(), bitmap);
} else {
Log.e("ImageGetter", "Failed to decode image");
return null;
}
} catch (Exception e) {
Log.e("ImageGetter", "Error loading image", e);
return null;
}
};
Observable.create((Observable.OnSubscribe<Spanned>) subscriber -> {
Spanned spanned = Html.fromHtml(htmlContent, imageGetter, null);
subscriber.onNext(spanned);
subscriber.onCompleted();
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Observer<Spanned>() {
@Override
public void onNext(Spanned spanned) {
textView.setText(spanned);
}
@Override
public void onCompleted() {}
@Override
public void onError(Throwable e) {
Log.e("setHtmlWithImages", "Error setting HTML with images", e);
}
});
}
public void setHtmlWithImages(TextView textView, String htmlContent) {
Html.ImageGetter imageGetter = source -> {
// 创建一个 drawable 占位符
BitmapDrawable placeholder = new BitmapDrawable(textView.getResources());
// 使用 Glide 异步加载图片
Glide.with(textView.getContext())
.asBitmap()
.load(source)
.into(new CustomTarget<Bitmap>() {
@Override
public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) {
BitmapDrawable drawable = new BitmapDrawable(textView.getResources(), resource);
drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
textView.setText(Html.fromHtml(htmlContent, source1 -> drawable, null));
}
@Override
public void onLoadCleared(@Nullable Drawable placeholder) {
// 可选:处理加载取消时的占位符
}
});
return placeholder;
};
// 初始设置文本,以便开始加载图片
textView.setText(Html.fromHtml(htmlContent, imageGetter, null));
}
public void setHtmlWithGif(TextView textView, String htmlContent) {
Html.ImageGetter imageGetter = source -> {
try {
URL url = new URL(source);
HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
connection.connect();
GifDrawable gifDrawable = new GifDrawable(connection.getInputStream());
gifDrawable.setBounds(0, 0, gifDrawable.getIntrinsicWidth(), gifDrawable.getIntrinsicHeight());
return gifDrawable;
} catch (IOException e) {
e.printStackTrace();
return null;
}
};
textView.setText(Html.fromHtml(htmlContent, imageGetter, null));
}
private static class CustomTagHandler implements Html.TagHandler {
private static final String UL_TAG = "ul";
private static final String OL_TAG = "ol";
private static final String LI_TAG = "li";
private static final String CODE_TAG = "code";
private static final String CENTER_TAG = "center";
private static final String STRIKE_TAG = "strike";
private int olIndex = 0;
@Override
public void handleTag(boolean opening, String tag, Spanned output, Html.TagHandler.StartEnd startEnd, boolean isEmpty) {
if (tag.equalsIgnoreCase(UL_TAG) || tag.equalsIgnoreCase(OL_TAG)) {
if (opening) {
olIndex = 0;
}
} else if (tag.equalsIgnoreCase(LI_TAG)) {
if (opening) {
int start = output.length();
if (tag.equalsIgnoreCase(OL_TAG)) {
olIndex++;
output.insert(start, olIndex + ". ");
}
output.setSpan(new BulletSpan(20), start, start, Spanned.SPAN_MARK_MARK);
} else {
int end = output.length();
BulletSpan[] spans = output.getSpans(0, end, BulletSpan.class);
if (spans.length > 0) {
int start = output.getSpanStart(spans[spans.length - 1]);
output.setSpan(spans[spans.length - 1], start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
} else if (tag.equalsIgnoreCase(CODE_TAG)) {
if (opening) {
int start = output.length();
output.setSpan(new TypefaceSpan("monospace"), start, start, Spanned.SPAN_MARK_MARK);
} else {
int end = output.length();
TypefaceSpan[] spans = output.getSpans(0, end, TypefaceSpan.class);
if (spans.length > 0) {
int start = output.getSpanStart(spans[spans.length - 1]);
output.setSpan(spans[spans.length - 1], start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
} else if (tag.equalsIgnoreCase(CENTER_TAG)) {
if (opening) {
int start = output.length();
output.setSpan(new AlignmentSpan.Standard(Layout.Alignment.ALIGN_CENTER), start, start, Spanned.SPAN_MARK_MARK);
} else {
int end = output.length();
AlignmentSpan[] spans = output.getSpans(0, end, AlignmentSpan.class);
if (spans.length > 0) {
int start = output.getSpanStart(spans[spans.length - 1]);
output.setSpan(spans[spans.length - 1], start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
} else if (tag.equalsIgnoreCase(STRIKE_TAG)) {
if (opening) {
int start = output.length();
output.setSpan(new StrikethroughSpan(), start, start, Spanned.SPAN_MARK_MARK);
} else {
int end = output.length();
StrikethroughSpan[] spans = output.getSpans(0, end, StrikethroughSpan.class);
if (spans.length > 0) {
int start = output.getSpanStart(spans[spans.length - 1]);
output.setSpan(spans[spans.length - 1], start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
}
}
}
private static class CustomTagHandler implements Html.TagHandler {
private static final String CUSTOM_TAG = "custom";
@Override
public void handleTag(boolean opening, String tag, Spanned output, Html.TagHandler.StartEnd startEnd, boolean isEmpty) {
if (tag.equalsIgnoreCase(CUSTOM_TAG)) {
if (opening) {
startHandleTag(output);
} else {
endHandleTag(output);
}
}
}
private void startHandleTag(Spanned output) {
int length = output.length();
output.setSpan(new ForegroundColorSpan(0xFFFF0000), length, length, Spanned.SPAN_MARK_MARK);
}
private void endHandleTag(Spanned output) {
int length = output.length();
ForegroundColorSpan[] spans = output.getSpans(0, length, ForegroundColorSpan.class);
if (spans.length > 0) {
int start = output.getSpanStart(spans[spans.length - 1]);
output.setSpan(spans[spans.length - 1], start, length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
}
public class MyTypefaceSpan extends MetricAffectingSpan {
private final Typeface typeface;
public MyTypefaceSpan(final Typeface typeface) {
this.typeface = typeface;
}
@Override
public void updateDrawState(final TextPaint drawState) {
apply(drawState);
}
@Override
public void updateMeasureState(final TextPaint paint) {
apply(paint);
}
private void apply(final Paint paint) {
final Typeface oldTypeface = paint.getTypeface();
final int oldStyle = oldTypeface != null ? oldTypeface.getStyle() : 0;
int fakeStyle = oldStyle & ~typeface.getStyle();
if ((fakeStyle & Typeface.BOLD) != 0) {
paint.setFakeBoldText(true);
}
if ((fakeStyle & Typeface.ITALIC) != 0) {
paint.setTextSkewX(-0.25f);
}
paint.setTypeface(typeface);
}
}
public class TypeFaceLabel implements Html.TagHandler {
private Typeface typeface;
private int startIndex = 0;
private int stopIndex = 0;
public TypeFaceLabel(Typeface typeface) {
this.typeface = typeface;
}
@Override
public void handleTag(boolean opening, String tag, Editable output, XMLReader xmlReader) {
if (tag.toLowerCase().equals("face")) {
if (opening) {
startIndex = output.length();
} else {
stopIndex = output.length();
//使用的是自定义的字体来实现
output.setSpan(new MyTypefaceSpan(typeface), startIndex, stopIndex, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
}
}
我们定义 string 资源:
<string name="hr_view_resume"><![CDATA[<font color="#000000">HR from</font>
<face><font color="#0689FB"> %s </font></face>
<font color="#000000"> has viewed your resume.</font>
]]></string>
String content = String.format(mContext.getString(R.string.hr_view_resume), item.employer_name);
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
tv_resume_log_content.setText(Html.fromHtml(content, Html.FROM_HTML_MODE_LEGACY, null, new TypeFaceLabel(TypefaceUtil.getSFSemobold(mContext))));
} else {
tv_resume_log_content.setText(Html.fromHtml(content, null, new TypeFaceLabel(TypefaceUtil.getSFSemobold(mContext))));
}
其实关于 ImageGetter 与 TagHandler 的使用,以后有大佬开源了很棒的方案,比如 EasyImageGetter 和 HtmlTextView 他们都是很棒的开源库。
https://github.com/easyandroidgroup/EasyAndroid/blob/master/utils/src/main/java/com/haoge/easyandroid/easy/EasyImageGetter.kt
https://github.com/SufficientlySecure/html-textview
本文的代码在文中已经全部给出,如果要要扩展实现其实也不难,相信大家都能轻松的完成啦。
那么今天的分享就到这里了,当然如果你有其他的更多的更好的实现方式,也希望大家能评论区交流一起学习进步。如果我的文章有错别字,不通顺的,或者代码、注释、有错漏的地方,同学们都可以指出修正。
如果感觉本文对你有一点的启发和帮助,还望你能点赞支持一下,你的支持对我真的很重要。
Ok,这一期就此完结。
最后推荐一下我做的网站,玩Android: wanandroid.com ,包含详尽的知识体系、好用的工具,还有本公众号文章合集,欢迎体验和收藏!
扫一扫 关注我的公众号
如果你想要跟大家分享你的文章,欢迎投稿~
┏(^0^)┛明天见!