想必我们在开发的会遇到图文混排的需求,如下图:
注意这里的头像是网络图片,并且头像和昵称、直播间都是可点击跳转的。这种效果的实现其实离不开SpannableString,对于这个类android开发者都不会陌生,使用它很容易实现这个效果!真的吗???一眼看去确实离不开这个类,但是它能够填充网络图片吗? 这是我们需要考虑的问题。那我们先从简单的开始
SpannableString sb = new SpannableString(" 我是测试图文混排");
Drawable drawable = Objects.requireNonNull(ContextCompat.getDrawable(requireActivity(),
R.mipmap.ic_launcher));
drawable.setBounds(0, 0, drawable.getIntrinsicWidth()/3, drawable.getIntrinsicHeight()/3);
//imageSpan使用drawable
ImageSpan imageSpan =new ImageSpan(drawable);
sb.setSpan(imageSpan,0,1, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
tv.setText(sb);
简简单单使用SpannableString ,要注意的是一定要调用setBounds 方法,该方法前两个参数与图片显示的位置相关,后两个参数代表图片的宽高。最后调用setSpan 方法,第二个和第三个参数,代表图片显示在字符的第几个位置。细心的可能发现,我的文本前面是有一个空格的,所以它就占据了空格的位置。
此时,如果我想在文本的尾部再添加一张图片,该怎么办呢?简单,如下:
同样的套路呀,代码如下:
SpannableString sb = new SpannableString(" 我是测试图文混排11 ");
Drawable drawable = Objects.requireNonNull(ContextCompat.getDrawable(requireActivity(),
R.mipmap.ic_launcher));
drawable.setBounds(0, 0, drawable.getIntrinsicWidth()/3, drawable.getIntrinsicHeight()/3);
//imageSpan使用drawable
ImageSpan imageSpan =new ImageSpan(drawable);
ImageSpan imageSpan2 =new ImageSpan(drawable);
sb.setSpan(imageSpan,0,1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
sb.setSpan(imageSpan2,11,12,Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
tv.setText(sb);
文本后面再加一个空格。
此时,我不忍来了句卧槽!!!!如果后面继续跟文本,跟图片,跟文本…,那不是要搞死我。当然,官方也考虑到这点,所以提供了另外一个类来解决这种复杂的图文混排:SpannableStringBuilder
效果如下:
代码如下:
SpannableString sb = new SpannableString(" 我是测试图文混排11 ");
Drawable drawable = Objects.requireNonNull(ContextCompat.getDrawable(requireActivity(),
R.mipmap.ic_launcher));
drawable.setBounds(0, 0, drawable.getIntrinsicWidth()/3, drawable.getIntrinsicHeight()/3);
//imageSpan使用drawable
ImageSpan imageSpan =new ImageSpan(drawable);
ImageSpan imageSpan2 =new ImageSpan(drawable);
sb.setSpan(imageSpan,0,1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
sb.setSpan(imageSpan2,11,12,Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
//使用SpannableStringBuilder
SpannableString sb2 = new SpannableString(" 我是测试图文混排22 ");
ImageSpan imageSpan3 =new ImageSpan(drawable);
sb2.setSpan(imageSpan3,0,1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
SpannableStringBuilder ssb = new SpannableStringBuilder();
ssb.append(sb);
ssb.append(sb2);
tv.setText(ssb);
使用SpannableStringBuilder直接可以在尾部追加SpannableString 。使用起来真香!!!
那么,了解到这里,我们接下来要去处理文章开头抛出的问题,“网络图片该如何处理”???
首先,再来思考一个问题:如何动态改变之前设置好的图片?很可惜,官方并没有提供这方面的api,我们只能事先使用ImageSpan设置好图片才能显示,那不是大坑吗,还玩个锤子!!!其实,遇到这种情况,我们唯一想到的就是反射了。大致流程就是:异步处理完图片以后,通过反射修改ImageSpan设置的drawable对象。
反射的核心代码如下:
final Field mDrawable = ImageSpan.class.getDeclaredField("mDrawable");
mDrawable.setAccessible(true);
mDrawable.set(FriendHorseRaceViewImageSpan.this, bmp);
final Field mDrawableRef = DynamicDrawableSpan.class.getDeclaredField(
"mDrawableRef");
mDrawableRef.setAccessible(true);
mDrawableRef.set(FriendHorseRaceViewImageSpan.this, null);
最后是将目标bitmap通过反射替换掉之前的bitmap。这个方法的调用时机实在哪里?
我们需要自定义ImageSpan,然后重写它的getDrawable()方法,如下:
@Override
public Drawable getDrawable() {
if (!picShowed) {
Glide.with(context).load(url).asBitmap().into(new SimpleTarget<Bitmap>() {
@Override
public void onResourceReady(Bitmap resource,
GlideAnimation<? super Bitmap> glideAnimation) {
Resources resources = context.getResources();
Bitmap zoom = getBitmap(resource, 21);
BitmapDrawable b = new BitmapDrawable(resources, zoom);
b.setBounds(0, 0, b.getIntrinsicWidth(), b.getIntrinsicHeight());
Field mDrawable;
Field mDrawableRef;
try {
mDrawable = ImageSpan.class.getDeclaredField("mDrawable");
mDrawable.setAccessible(true);
mDrawable.set(FriendHorseRaceViewImageSpan.this, b);
mDrawableRef =
DynamicDrawableSpan.class.getDeclaredField( "mDrawableRef");
mDrawableRef.setAccessible(true);
mDrawableRef.set(FriendHorseRaceViewImageSpan.this, null);
picShowed = true;
callback.onSuccess();
} catch (IllegalAccessException | NoSuchFieldException e) {
e.printStackTrace();
}
}
});
}
return super.getDrawable();
}
我这里使用glide加载图片以后,通过callback回调给UI端,为什么需要回调,设置完就能显示吗? 其实,这边还有一个坑,虽然图片被我们设置了,但是UI更新还需要手动去更新,否则还是没有效果。 来看一下,更新的代码:
tv.setText(ssb);
重写设置一下SpannableStringBuilder就行了。下载的过程肯定是一个异步的过程,如何在下载的过程中显示一张默认的图片呢?需要使用它的构造方法了:
super(context, bitmap);
注意,这里我是使用了bitmap,因为反射的时候修改的也是bitmap,如果使用drawable,会出现空白的情况。另外使用bitmap我们还可以设置它的大小,我怀疑空白是因为图片大小的问题
完成以上配置,基本就可以加载网络图片了,效果如下:
代码如下:
SpannableString sb = new SpannableString(" 我是测试图文混排11 ");
Drawable drawable = Objects.requireNonNull(ContextCompat.getDrawable(requireActivity(),
R.mipmap.ic_launcher));
drawable.setBounds(0, 0, drawable.getIntrinsicWidth()/3, drawable.getIntrinsicHeight()/3);
//imageSpan使用drawable
ImageSpan imageSpan =new ImageSpan(drawable);
ImageSpan imageSpan2 =new ImageSpan(drawable);
sb.setSpan(imageSpan,0,1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
sb.setSpan(imageSpan2,11,12,Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
//使用SpannableStringBuilder
SpannableString sb2 = new SpannableString(" 我是测试图文混排22 ");
ImageSpan imageSpan3 =new ImageSpan(drawable);
sb2.setSpan(imageSpan3,0,1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
final SpannableStringBuilder ssb = new SpannableStringBuilder();
SpannableString sb3 = new SpannableString(" ");
Bitmap placeHolder = BitmapFactory.decodeResource(requireContext().getResources(), R.drawable.ic_launcher);
MyImageSpan networkImageSpan = new MyImageSpan(requireContext(), "https://avatar01.jiaoliuqu.com/avatar/6b8b90a1a4d39a475cf05890620b5c54.jpg_style.240",
placeHolder, new FriendHorseRaceViewSpannableString.NetImageSpanCallback() {
@Override
public void onSuccess() {
tv.setText(ssb);
}
});
sb3.setSpan(networkImageSpan,0,1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
ssb.append(sb);
ssb.append(sb2);
ssb.append(sb3);
tv.setText(ssb);
在回调方法onSuccess中,重新调用了 tv.setText(ssb)。由于是项目中的代码,我这边只把核心类的代码拷贝出来,供大家参考,细节问题还需要各自处理。
public class MyImageSpan extends ImageSpan {
private Context context;
private boolean picShowed;
private String url;
private FriendHorseRaceViewSpannableString.NetImageSpanCallback callback;
public MyImageSpan(@NonNull Context context,
String url,
Bitmap placeHolder,
FriendHorseRaceViewSpannableString.NetImageSpanCallback callback) {
super(context, placeHolder);
this.context = context;
this.callback = callback;
this.url = url;
}
@SuppressWarnings("JavaReflectionMemberAccess")
@Override
public Drawable getDrawable() {
if (!picShowed) {
Glide.with(context).load(url).asBitmap().into(new SimpleTarget<Bitmap>() {
@Override
public void onResourceReady(Bitmap resource,
GlideAnimation<? super Bitmap> glideAnimation) {
Resources resources = context.getResources();
Bitmap zoom = getBitmap(resource, 30);
BitmapDrawable b = new BitmapDrawable(resources, zoom);
b.setBounds(0, 0, b.getIntrinsicWidth(), b.getIntrinsicHeight());
Field mDrawable;
Field mDrawableRef;
try {
mDrawable = ImageSpan.class.getDeclaredField("mDrawable");
mDrawable.setAccessible(true);
mDrawable.set(MyImageSpan.this, b);
mDrawableRef =
DynamicDrawableSpan.class.getDeclaredField( "mDrawableRef");
mDrawableRef.setAccessible(true);
mDrawableRef.set(MyImageSpan.this, null);
picShowed = true;
callback.onSuccess();
} catch (IllegalAccessException | NoSuchFieldException e) {
e.printStackTrace();
}
}
});
}
return super.getDrawable();
}
@Override
public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end, float x,
int top, int y, int bottom, @NonNull Paint paint) {
//主要设置图片居中
Drawable b = getDrawable();
Paint.FontMetricsInt fm = paint.getFontMetricsInt();
int picY = (y + fm.descent + y + fm.ascent) / 2 - b.getBounds().bottom / 2;
canvas.save();
canvas.translate(x, picY);
b.draw(canvas);
canvas.restore();
}
/**
* 设置图片大小
*
* @param bitmap 原图片
* @param radius 半径
* @return 新图片
*/
public Bitmap getBitmap(Bitmap bitmap, int radius) {
bitmap = Bitmap.createScaledBitmap(bitmap, radius * 2, radius * 2, true);
Bitmap bm = Bitmap.createBitmap(radius * 2, radius * 2, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bm);
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
canvas.drawBitmap(bitmap, 0, 0, paint);
return bm;
}
}
上面两步完成了网络图片的图文混排,那么有些时候,还是需要点击跳转的,那么就需要了解ClickSpan,先看一下效果:
相关代码如下:
ClickableSpan clickableSpan = new ClickableSpan() {
@Override
public void onClick(@NonNull View view) {
}
@Override
public void updateDrawState(@NonNull TextPaint ds) {
super.updateDrawState(ds);
//不设置下划线
// ds.setUnderlineText(false);
// ds.clearShadowLayer();
}
};
sb.setSpan(clickableSpan,0,12,Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
tv.setMovementMethod(LinkMovementMethod.getInstance());
如果想去掉下划线,就需要重写updateDrawState方法,设置下划线,如上代码。点击回调方法是onClick方法。
另外还有一点需要注意,当网络图片下载完成以后,我们需要重新设置当前TextView与SpannableStringBuilder进行事件绑定,否则点击事件会失效 。相关代码如下:
MyImageSpan networkImageSpan = new MyImageSpan(requireContext(), "https://avatar01.jiaoliuqu.com/avatar/6b8b90a1a4d39a475cf05890620b5c54.jpg_style.240",
placeHolder, new FriendHorseRaceViewSpannableString.NetImageSpanCallback() {
@Override
public void onSuccess() {
LinkMovementMethod.getInstance().initialize(tv, ssb);
tv.setText(ssb);
}
});
核心代码:
LinkMovementMethod.getInstance().initialize(tv, ssb);
了解到这里,想必完成开篇的效果,岂不是易如反掌。有人说了:字体颜色呢,这个就简单了,使用ColorSpan,详细的就不做多演示了。下面,总结一下,核心注意点。
注意: