TextView显示丰富多彩的文字(三)——自定义CharacterStyle和ParagraphStyle显示效果

TextView显示丰富多彩的文字(一)——如何使用CharacterStyle格式化字符TextView显示丰富多彩的文字(二)——如何使用ParagraphStyle格式化段落中,知道了如何格式化文本,给文本中的某一范围设置样式,比如前景、背景、图片等;或者给整个段落设置格式,比如BulletSpan、QuoteSpan等。不了解的朋友可以先去看一下。在这篇博客中,我们将自定义我们自己的格式。

两个特殊的CharacterStyle——ImageSpan和URLSpan

下图是CharacterStyle的类继承关系
CharacterStyle类继承关系
ImageSpan的特别之处在于它能显示图片吗?不是的,在TextView显示丰富多彩的文字(一)——如何使用CharacterStyle格式化字符里可以看到虽然在文本里面有“图片”两字,但是最后显示却只有图片没有文字了;URLSpan的特别之处在于它能够被点击,可以跳转到设置的URL中。可以参考ImageSpan和URLSpan的类结构。
下图是ImageSpan的继承关系:
ImageSpan类继承关系
其中DynamicDrawableSpan和ReplacementSpan都是抽象的类,DynamicDrawable的子类负责显示具体的Drawable,ReplacementSpan用于替换,所以可以解释为什么ImageSpan只显示了图片而没有显示文字。
下图是URLSpan的继承关系:
URLSpan类继承关系
URLSpan继承自CliableSpan,实现了其点击事件,源码中就是获取之前设置的URL然后再调用系统浏览器显示URL。可参考URLSpan源码中的点击事件:

@Override
public void onClick(View widget) {
        Uri uri = Uri.parse(getURL());
        Context context = widget.getContext();
        Intent intent = new Intent(Intent.ACTION_VIEW, uri);
        intent.putExtra(Browser.EXTRA_APPLICATION_ID, context.getPackageName());
        try {
            context.startActivity(intent);
        } catch (ActivityNotFoundException e) {
            Log.w("URLSpan", "Actvity was not found for intent, " + intent.toString());
        }
    }

在源码中可以看到,点击事件就是获取URL后,再调用系统浏览器显示网页。

自定义ReplacementSpan——TextReplacementSpan

我们知道SpannableString是不可以替换文字的,而SpannableStringBuilder是可以替换文字的,那么我们想实现一种文本替换的格式。因为需要实现替换的功能,所以TextReplacmentSpan继承ReplacmentSpan。可以查看ReplacementSpan的文档。先看实现代码:

/**
 * 用文字替换文字
 * Created by Xingfeng on 2016-09-11.
 */
public class TextReplacementSpan extends ReplacementSpan {

    private CharSequence mRelaceText;
    private int mReplaceTextColor;
    private int mReplaceTextSize;

    public TextReplacementSpan(CharSequence mRelaceText) {
        this(mRelaceText, Color.BLACK, 30);
    }

    public TextReplacementSpan(CharSequence mRelaceText, int mReplaceTextColor, int mReplaceTextSize) {
        this.mRelaceText = mRelaceText;
        this.mReplaceTextColor = mReplaceTextColor;
        this.mReplaceTextSize = mReplaceTextSize;
    }

     @Override
    public int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) {

        paint.setColor(mReplaceTextColor);
        paint.setTextSize(mReplaceTextSize);
        int width = (int) paint.measureText(mRelaceText, 0, mRelaceText.length());
        if (fm != null) {
            fm.ascent = (int) paint.ascent();
            fm.descent = (int) paint.descent();
            fm.top = fm.ascent;
            fm.bottom = fm.descent;
        }

        return width;
    }

    @Override
    public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) {
        paint.setTextSize(mReplaceTextSize);
        paint.setColor(mReplaceTextColor);
        canvas.drawText(mRelaceText, 0, mRelaceText.length(), x, y, paint);
    }

}

可以看到我们不仅可以设置替换的文本,还可以设置文本的颜色和文本的字体大小,并且有默认的颜色和大小。ReplacementSpan是抽象类,有两个抽象方法,getSize和draw。其中getSize的说明如下:
getSize方法说明
可以看到返回值是Span的宽度,对于我们这个就是所设置文本的宽度。另外还说了子类可以通过更新Paint.FontMetricsInt属性来设置Span的高度。如果Span覆盖了整个文字的高度,而高度没有设置的话,那么draw方法不会被调用。所以我们的实现就是根据设置的文本和文本尺寸得到宽度,并且设置FontMetricsInt的属性(参考DynamicDrawableSpan的实现)。下面是DynamicDrawableSpan的实现。

 @Override
    public int getSize(Paint paint, CharSequence text,
                         int start, int end,
                         Paint.FontMetricsInt fm) {
        Drawable d = getCachedDrawable();
        Rect rect = d.getBounds();

        if (fm != null) {
            fm.ascent = -rect.bottom; 
            fm.descent = 0; 

            fm.top = fm.ascent;
            fm.bottom = 0;
        }

        return rect.right;
    }

另外一个方法draw的文档如下:
draw方法说明
该方法用于将格式添加进Canvas中,而我们这儿需要做的就是把替换的文字写进去,不过要记得在这之前先设置Paint的颜色和文字尺寸。
下面是用“阿里巴巴”替换“百度”的的前后效果。前面一张为原始效果,下面一张为替换效果。
使用TextReplacementSpan替换前的效果
使用TextReplacementSpan替换后的效果
代码如下:

 SpannableString spannableString = new SpannableString(text);
ReplacementSpan replacementSpan = new TextReplacementSpan("阿里巴巴", Color.RED,50);
spannableString.setSpan(replacementSpan,0,2, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
urlTextView.setText(spannableString);

就是使用自定义的TextReplacementSpan替换文字。
这里有个用的很多的格式,就是MetricAffectingSpan类,很多类都继承自这个类,这个类主要负责影响文字的外观,比如颜色、尺寸等。

使用广泛的ParagraphStyle——LeadingMarginSpan

TextView显示丰富多彩的文字(二)—————在段落级别改变中有很多LeadingMarginSpan的实现类,都可以实现类似paddingLeft的效果,只不过是绘画的东西不同。BulletSpan是一种类似无序段落的格式,下面我们将实现一种类似有序段落的格式。

自定义LeadingMarginSpan——OrderSpan

先看LeadingMarginSpan接口。如下:
LeadingMarginSpan接口说明
可以看到LeadingMarginSpan接口有两个方法,第一个用于绘画margin部分,第二个方法用于返回margin的宽度。==需要注意的是,当TextView每显示一行的时候都会调用这两个方法==
下面先看BulletSpan的实现,源码如下:

public class BulletSpan implements LeadingMarginSpan, ParcelableSpan {
    private final int mGapWidth;
    private final boolean mWantColor;
    private final int mColor;

    private static final int BULLET_RADIUS = 3;
    private static Path sBulletPath = null;
    public static final int STANDARD_GAP_WIDTH = 2;

    public BulletSpan() {
        mGapWidth = STANDARD_GAP_WIDTH;
        mWantColor = false;
        mColor = 0;
    }

    public BulletSpan(int gapWidth) {
        mGapWidth = gapWidth;
        mWantColor = false;
        mColor = 0;
    }

    public BulletSpan(int gapWidth, int color) {
        mGapWidth = gapWidth;
        mWantColor = true;
        mColor = color;
    }

    public BulletSpan(Parcel src) {
        mGapWidth = src.readInt();
        mWantColor = src.readInt() != 0;
        mColor = src.readInt();
    }

    public int getSpanTypeId() {
        return getSpanTypeIdInternal();
    }

    /** @hide */
    public int getSpanTypeIdInternal() {
        return TextUtils.BULLET_SPAN;
    }

    public int describeContents() {
        return 0;
    }

    public void writeToParcel(Parcel dest, int flags) {
        writeToParcelInternal(dest, flags);
    }

    /** @hide */
    public void writeToParcelInternal(Parcel dest, int flags) {
        dest.writeInt(mGapWidth);
        dest.writeInt(mWantColor ? 1 : 0);
        dest.writeInt(mColor);
    }

    public int getLeadingMargin(boolean first) {
        return 2 * BULLET_RADIUS + mGapWidth;
    }

    public void drawLeadingMargin(Canvas c, Paint p, int x, int dir,
                                  int top, int baseline, int bottom,
                                  CharSequence text, int start, int end,
                                  boolean first, Layout l) {
        if (((Spanned) text).getSpanStart(this) == start) {
            Paint.Style style = p.getStyle();
            int oldcolor = 0;

            if (mWantColor) {
                oldcolor = p.getColor();
                p.setColor(mColor);
            }

            p.setStyle(Paint.Style.FILL);

            if (c.isHardwareAccelerated()) {
                if (sBulletPath == null) {
                    sBulletPath = new Path();
                    // Bullet is slightly better to avoid aliasing artifacts on mdpi devices.
                    sBulletPath.addCircle(0.0f, 0.0f, 1.2f * BULLET_RADIUS, Direction.CW);
                }

                c.save();
                c.translate(x + dir * BULLET_RADIUS, (top + bottom) / 2.0f);
                c.drawPath(sBulletPath, p);
                c.restore();
            } else {
                c.drawCircle(x + dir * BULLET_RADIUS, (top + bottom) / 2.0f, BULLET_RADIUS, p);
            }

            if (mWantColor) {
                p.setColor(oldcolor);
            }

            p.setStyle(style);
        }
    }
}

BulletSpan的getLeadingMargin方法返回的是圆点的直径和距文本的距离。其中参数为true的,代表的是第一行,否则代表其他行。(LeadingMarginSpan.Standard区分了这个参数,所以可以实现首行缩进的功能)drawLeadingMargin就是画个圆点,其中还区分了是否设置了硬件加速。下面我们仿造BulletSpan实现我们的OrderSpan。

/**
 * 有序段落格式
 * Created by Xingfeng on 2016-09-11.
 */
public class OrderSpan implements LeadingMarginSpan {

    private final int mOrder;//序号
    private int mOrderColor;//序号颜色
    private int mGapWidth;

    private boolean hasSetColor;

    public static final int STANDARD_GAP_WIDTH = 20;

    public OrderSpan() {
        this(1);
    }

    public OrderSpan(int mOrder) {
        this(mOrder, STANDARD_GAP_WIDTH);
    }

    public OrderSpan(int mOrder, int mGapWidth) {
        this.mOrder = mOrder;
        this.mGapWidth = mGapWidth;
        hasSetColor = false;
    }

    public OrderSpan(int mOrder, int mOrderColor, int mGapWidth) {
        this.mOrder = mOrder;
        this.mOrderColor = mOrderColor;
        this.mGapWidth = mGapWidth;
        hasSetColor = true;
    }

    /**
     * 返回Margin的宽度,文本宽度+gapWidth
     *
     * @param first
     * @return
     */
    @Override
    public int getLeadingMargin(boolean first) {
        return 20 + mGapWidth;
    }

    @Override
    public void drawLeadingMargin(Canvas c, Paint p, int x, int dir, int top, int baseline, int bottom, CharSequence text, int start, int end, boolean first, Layout layout) {

        if (((Spanned) text).getSpanStart(this) == start) {
            Paint.Style style = p.getStyle();
            int oldcolor = p.getColor();

            if (hasSetColor) {
                p.setColor(mOrderColor);
            }


            String orderText = String.valueOf(mOrder);
            c.drawText(orderText, 0, orderText.length(), x, baseline, p);

            p.setColor(oldcolor);
            p.setStyle(style);
        }


    }
}

其中可以设置序号的颜色,距文本的距离,但是字体大小和文本大小一样大,不过也可以添加设置文本大小的属性。其中返回的margin是个定值,如果需要首行缩进的话,可以让非首行返回宽度0。其中绘画margin区域时,首先保存了Paint的样式,再设置了自己的样式后,写完序号后,又恢复了Paint的样式,这是因为在第一行如果我们改变了Paint的样式,而没有恢复的话,那么以后的每一行的Paint都是我们在第一行做出了改变的Paint。这是因为上面所说的,在TextView绘制每一行文本的时候,都会调用这个方法,都会传进来相同的一个Paint对象。下面看示例代码和效果。

private TextView mParagraph1;
    private TextView mParagraph2;

    private String paragraph = "中新网\t北京9月10日电 今天," +
            "连通东西部多条铁路干线的郑徐高铁将正式通车运营," +
            "中国高铁网络再次得到完善,东中西部民众的高铁出行也" +
            "更加便利。与此同时,今天开始,受郑徐高铁开通影响," +
            "全国铁路再次迎来一次运行图大调整。";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_paragraph);

        mParagraph1 = (TextView) findViewById(R.id.paragraph_1);
        mParagraph2 = (TextView) findViewById(R.id.paragraph_2);


        //默认实现,序号为1,颜色和尺寸都和文本内容一致
        OrderSpan orderSpan1 = new OrderSpan();
        SpannableString spannableString = new SpannableString(paragraph);
        spannableString.setSpan(orderSpan1, 0, paragraph.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
        mParagraph1.setText(spannableString);

        OrderSpan orderSpan2 = new OrderSpan(2, Color.RED, 40);
        spannableString.removeSpan(orderSpan1);//移除OrderSpan1
        spannableString.setSpan(orderSpan2, 0, paragraph.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);//添加OrderSpan2
        mParagraph2.setText(spannableString);
    }

我们第一个使用的是OrderSpan的默认实现,第二个是使用了自己设置的参数,将序号颜色变为红色,并且设置了距文本的距离。效果如下:
OrderSpan使用示例
可以看到第二个序号为红色,且距离文字更远了。

总结

通过上面的例子,我们自定义了自己的TextReplacementSpan和OrderSpan,掌握了该如何实现自己的格式。代码可以到我的github查看,链接

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
要在Android自定义TextView显示全部内容,可以使用以下两种方法: 1. 使用setEllipsize()方法 通过设置setEllipsize()方法,可以在TextView的末尾添加省略号,从而指示文本被截断。你可以使用以下代码来实现: ``` yourTextView.setEllipsize(TextUtils.TruncateAt.END); yourTextView.setSingleLine(true); ``` 上述代码将设置TextView显示一行并在末尾添加省略号。 2. 自定义TextView 你可以从TextView类继承一个新类,并覆盖onMeasure()方法以测量控件的高度和宽度。 你可以使用以下代码实现: ``` public class CustomTextView extends TextView { public CustomTextView(Context context) { super(context); } public CustomTextView(Context context, AttributeSet attrs) { super(context, attrs); } public CustomTextView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); //获取TextView的内容 CharSequence text = getText(); if (text != null) { //测量TextView的高度 int width = getMeasuredWidth(); int height = getMeasuredHeight(); int lineCount = getLineCount(); int lineHeight = getLineHeight(); int totalHeight = lineCount * lineHeight; if (totalHeight > height) { setMeasuredDimension(width, totalHeight); } } } } ``` 上述代码将测量TextView的高度,如果文本的高度超出了TextView的高度,则调整TextView的高度以适应文本。然后你可以使用此自定义TextView显示你的文本。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值