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;

使用

  • 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

猜你喜欢

转载自blog.csdn.net/wangxiaocheng16/article/details/100660262