TextView之自定义Span—MarkerViewSpan,TextView中添加View

简介

关于 Span ,我们常用的有 ForegroundColorSpan(前景色,即字体颜色)、BackgroundColorSpan(背景色)、AbsoluteSizeSpan(绝对大小,设置字体大小)、ImageSpan(插入图片)等,更多可参考博客:Android中各种Span的用法。然而这些都是基础的用法,如果设计给了更高要求的设计图(如下图所示),基础用法就不够用了。

MarkerViewSpan效果图

实现

这篇博客,TextView里画世界——ReplacementSpan实践,实现了比较好的Span效果,如下图所示:
在这里插入图片描述
这种方式实现已经非常好用了,但是我想,其实可以将TextView和ImageView的绘制在TextView中,可以更加方便地设置图片和文字的样式。也就是,可以整一个ViewSpan,传入外部的View,把这个View绘制在想要的位置。既然可以放一个View,那么为什么不直接放一个LinearLayout,绘制在TextView中,在LinearLayout中再放入View,这样的话,不是既可以设置View的样式,又可以控制View的位置。说干就干,仿照上面博客的代码,参考其他博客,于是 MarkerViewSpan 诞生了。

代码非常简单:

import android.graphics.Canvas;
import android.graphics.Paint;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.style.ReplacementSpan;
import android.view.View;
import android.view.ViewGroup;

public class MarkerViewSpan extends ReplacementSpan {

    protected View view;

    public MarkerViewSpan(View view ) {
        super();
        this.view = view;

        this.view.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
    }

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

        int widthSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
        int heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
        view.measure(widthSpec, heightSpec);
        view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight());

        if (fm != null) {
            int height = view.getMeasuredHeight();
            fm.ascent = fm.top = -height / 2;
            fm.descent = fm.bottom = height / 2;
        }
        return view.getRight();
    }

    @Override
    public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, @NonNull Paint paint) {

        Paint.FontMetricsInt fm = paint.getFontMetricsInt();
        int transY = (y + fm.descent + y + fm.ascent) / 2 - view.getMeasuredHeight() / 2;//计算y方向的位移

        canvas.save();
        canvas.translate(x, transY);
        view.draw(canvas);
        canvas.restore();
    }

}

上面代码中,我们同样继承了 ReplacementSpan ,通过构造方法传入了一个View,在 getSize() 方法中测量View的尺寸,设置 fm(Paint.FontMetricsInt)的 ascent 和 descent;在 draw() 方法中定位位置,调用 view.draw(canvas),绘制出 View 的视图。

上面代码中涉及到了文字的绘制,下面看一下Android是如何绘制文字的。

text_4lines
如上图所示,Android中绘制文字是根据基线定位的。

draw(@NonNull Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, @NonNull Paint paint)

方法中的 x 和 y 就是图中绿点的 x和y的坐标,y就是基线位置。ascent 和 descent 的含义上面图片中有文字提示。具体相关知识,参考:博客笔记:自定义View之绘图(1)–drawText

在 getSize() 方法中我们已经设置了 ascent 和 descent。我们需要定位View的位置。下面是通过中线(centerLine)位置计算基线 (baseLine),现在我们知道了基线位置 y,和 ascent 和 descent。可以计算出 centerLine 位置。

给出中线位置绘制文字

①centerLine作为ascentLine和descentLine的中间线
centerLineY = (ascentLineY + descentLineY)/2
<=> centerLineY = (ascent + baselineY + descent + baselineY)/2
<=> centerLineY = baselineY + (ascent + descent)/2
<=>baselineY = centerLineY - (ascent + descent)/2
∵ ascent = fontMetrics.ascent, descent = fontMetrics.descent
∴ baseLineY = centerLineY - (fontMetrics.ascent + fontMetrics.descent)/2

得出centerLinerY的位置:centerLineY = (ascent + baselineY + descent + baselineY)/2 = (fm.ascent + y + fm.descent + y ) / 2。

由于画笔在基线位置开始绘制,所以需要找到 View 的开始绘制位置,将画笔移到那个位置绘制View,再恢复。而 View 需要与文字居中对齐,所以,需要让中线位置再减去 view 高度的一半。这样就得出了画笔需要移动的位置:

int transY = (y + fm.descent + y + fm.ascent) / 2 - view.getMeasuredHeight() / 2;

其他实现方式

参考 DrawableMarginSpan 源码,实现适用于段落的 ViewSpan,个人感觉比上一种实现方式更好用,代码如下:

import android.graphics.Canvas
import android.graphics.Paint
import android.text.Layout
import android.text.style.LeadingMarginSpan
import android.text.style.LineHeightSpan
import android.text.style.ParagraphStyle
import android.text.style.ReplacementSpan
import android.view.View
import android.view.ViewGroup
import androidx.annotation.NonNull

/**
 * 作用于段落的 ViewSpan,只显示在首行。
 *
 * @see LeadingMarginSpan
 * @see android.text.style.DrawableMarginSpan
 */
class MarkerViewSpan constructor(@NonNull val view: View) : LeadingMarginSpan {


    init {
        view.layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
        val widthSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
        val heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
        view.measure(widthSpec, heightSpec)
        view.layout(0, 0, view.measuredWidth, view.measuredHeight)
    }


    override fun getLeadingMargin(first: Boolean): Int {
        if (first) {
            return view.measuredWidth
        }
        return 0
    }

    override fun drawLeadingMargin(c: Canvas?, p: Paint?, x: Int, dir: Int, top: Int, baseline: Int, bottom: Int, text: CharSequence?, start: Int, end: Int, first: Boolean, layout: Layout?) {
        if (first) {
            val fm = p?.fontMetrics
            val transY = (baseline + fm?.descent!! + baseline + fm?.ascent!!) / 2f - view.measuredHeight / 2f
            c?.save()
            c?.translate(x.toFloat(), transY)
            view.draw(c)
            c?.restore()
        }

    }
}

使用

  • activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/tv_item_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="中秋佳节,月饼好圆......"
        android:layout_marginLeft="15dp"
        android:layout_marginTop="8dp"
        android:textColor="#333333"
        android:textSize="18dp"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
  • item_marker_fore_title.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:orientation="horizontal"
    android:gravity="center_vertical">

    <TextView
        android:id="@+id/item_degree_marker"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginRight="6dp"
        android:background="@drawable/shape_degree_marker"
        android:textSize="12dp"
        android:gravity="center"
        android:paddingLeft="3dp"
        android:paddingRight="3dp"
        android:paddingBottom="2dp"
        android:textColor="#FFFFFF"
        android:text="最热"/>

    <ImageView
        android:id="@+id/item_img_marker"
        android:layout_width="16dp"
        android:layout_height="16dp"
        android:layout_marginRight="6dp"
        android:src="@drawable/ic_img"
        android:scaleType="fitXY"/>



</LinearLayout>

  • 图片资源
    ic_img
  • shape_degree_marker.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">

    <corners android:radius="2dp" />
    <solid android:color="#b154f2" />

</shape>
  • MainActivity.java
public class MainActivity extends AppCompatActivity {

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

        final TextView tvItemTitle = (TextView) findViewById(R.id.tv_item_title);
        View markerView = LayoutInflater.from(this).inflate(R.layout.item_marker_fore_title, null);
        final ImageView imgMarker = (ImageView) markerView.findViewById(R.id.item_img_marker);
        final TextView tvDegreeMarker = (TextView) markerView.findViewById(R.id.item_degree_marker);
        tvDegreeMarker.setText("最新");

        SpannableStringBuilder builder = new SpannableStringBuilder();
        final String REPLACE_TEXT = " ";
        builder.append(REPLACE_TEXT);
        builder.setSpan(new MarkerViewSpan(markerView), 0, REPLACE_TEXT.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        String oriText = tvItemTitle.getText().toString();
        builder.append(oriText);
        tvItemTitle.setText(builder);

        tvItemTitle.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                tvDegreeMarker.setText("最热");
                imgMarker.setVisibility(View.GONE);
                tvItemTitle.postInvalidate();
            }
        });
    }
}

可以改变 图片和文字的隐藏显示,文字的内容等,不过需要调用 TextView 的 postInvalidate() 方法,重绘一下TextView。

参考

  1. TextView里画世界——ReplacementSpan实践
  2. 博客笔记:自定义View之绘图(1)–drawText
  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值