零开始玩转SpannableString

想必我们在开发的会遇到图文混排的需求,如下图:
在这里插入图片描述
注意这里的头像是网络图片,并且头像和昵称、直播间都是可点击跳转的。这种效果的实现其实离不开SpannableString,对于这个类android开发者都不会陌生,使用它很容易实现这个效果!真的吗???一眼看去确实离不开这个类,但是它能够填充网络图片吗? 这是我们需要考虑的问题。那我们先从简单的开始

一、本地图片,图文混排

1、首先简单实现如下效果:

在这里插入图片描述
代码如下:

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 方法,第二个和第三个参数,代表图片显示在字符的第几个位置。细心的可能发现,我的文本前面是有一个空格的,所以它就占据了空格的位置。

2、复杂的图文混排

此时,如果我想在文本的尾部再添加一张图片,该怎么办呢?简单,如下:
在这里插入图片描述
同样的套路呀,代码如下:

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,详细的就不做多演示了。下面,总结一下,核心注意点。

注意:

  • 使用SpannableStringBuilder实现复杂的图文混排
  • 通过发射动态设置网络图片,反射成功后相关TextView需要重新设置SpannableStringBuilder
  • 添加图片占位,需要设置占位图片大小
  • 使用tv.setMovementMethod(LinkMovementMethod.getInstance())与ClickSpan,设置点击
  • 网络图片设置成功后,需要重新绑定TextView与SpannableStringBuilder,才能触发点击事件, LinkMovementMethod.getInstance().initialize(tv, ssb);
  • 去除点击背景颜色,使用 tv.setHighlightColor(ContextCompat.getColor(requireContext(),android.R.color.transparent));设置为透明。

你可能感兴趣的:(技术分享)