简介
关于 Span ,我们常用的有 ForegroundColorSpan(前景色,即字体颜色)、BackgroundColorSpan(背景色)、AbsoluteSizeSpan(绝对大小,设置字体大小)、ImageSpan(插入图片)等,更多可参考博客:Android中各种Span的用法。然而这些都是基础的用法,如果设计给了更高要求的设计图(如下图所示),基础用法就不够用了。
实现
这篇博客,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是如何绘制文字的。
如上图所示,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>
- 图片资源
- 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。