之前一直把基础理论与项目实践放在一起,导致文章太长失去了阅读欲望,特此精简
关联篇
可自行根据业务需求,选择适合自己的
基础场景
html 拼接(简单、便捷、适用静态显示)
这种方式我以前在 Android进阶之路 - 常用小功能 中有记录过,为了方便特在此重新记录一下
//主要代码
Html.fromHtml("第一段文本信息" + "<font color=\"#757575\">" + "第二段文本信息(为灰色)" + "</font>")
//使用方式(如要设置颜色,可以如第二段一般进行颜色填充~)
mContext.setText(Html.fromHtml("希望" + "<font color=\"#757575\">" + "我们都不会老" + "</font>"));
//展示结果:希望(黑色字体)、我们不会老(灰色字体)
//上图效果
tvRisk.setText(Html.fromHtml("您的风险偏好是" + "<font color=\"#1760EA\">" + "C1稳健型" + "</font>"));
SpannableStringBuilder 拼接(基础核心,扩展性强)
基础实现方式,务必掌握
package com.example.yongliu.spannablestringbuilder;
import android.graphics.Color;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.method.LinkMovementMethod;
import android.text.style.BackgroundColorSpan;
import android.text.style.ClickableSpan;
import android.text.style.ForegroundColorSpan;
import android.text.style.ImageSpan;
import android.text.style.RelativeSizeSpan;
import android.text.style.StrikethroughSpan;
import android.text.style.StyleSpan;
import android.text.style.SuperscriptSpan;
import android.text.style.URLSpan;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity {
private TextView mContent;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mContent = findViewById(R.id.content);
SpannableStringBuilder data = new SpannableStringBuilder("开始的开始,我们只是孩子!表情");
/* *
* 创建对应的Span样式
*/
//前景色
ForegroundColorSpan forColorSpan = new ForegroundColorSpan(Color.RED);
//背景色
BackgroundColorSpan backColorSpan = new BackgroundColorSpan(Color.BLUE);
//字体大小
RelativeSizeSpan textSizeSpan = new RelativeSizeSpan(1.0f);
//设置超链接
URLSpan urlSpan = new URLSpan("https://blog.csdn.net/qq_20451879/article/details/80094968");
//删除线
StrikethroughSpan strikethroughSpan = new StrikethroughSpan();
//上标
SuperscriptSpan superscriptSpan = new SuperscriptSpan();
//图片
Drawable drawable = getResources().getDrawable(R.mipmap.ic_launcher);
drawable.setBounds(0, 0, 42, 42);
ImageSpan imageSpan = new ImageSpan(drawable);
//设置字体Style
StyleSpan styleSpan = new StyleSpan(Typeface.BOLD);
/*
* 将上方创建好的Span样式,设置到对应的角标位
* start 起始角标值
* end 终点角标值
*/
//设置前景色
spannableBuilder.setSpan(forColorSpan, 0, 1, Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
//设置背景色
spannableBuilder.setSpan(backColorSpan, 1, 2, Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
//设置字体大小
spannableBuilder.setSpan(textSizeSpan, 0, data.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
//设置删除线
spannableBuilder.setSpan(strikethroughSpan, 10, 12, Spannable.SPAN_INCLUSIVE_INCLUSIVE);
//设置超链接
spannableBuilder.setSpan(urlSpan, 0, data.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
//设置上标
spannableBuilder.setSpan(superscriptSpan, 3, 4, Spannable.SPAN_INCLUSIVE_INCLUSIVE);
//设置图片
spannableBuilder.setSpan(imageSpan, 13, data.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
//设置字体风格
spannableBuilder.setSpan(styleSpan, 5, 7, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
//点击
spannableBuilder.setSpan(new ClickableSpan() {
@Override
public void onClick(View view) {
Toast.makeText(MainActivity.this,"我想说,你可好?",Toast.LENGTH_SHORT).show();
}
},0,9,Spannable.SPAN_INCLUSIVE_INCLUSIVE);
mContent.setText(spannableBuilder);
mContent.setMovementMethod(LinkMovementMethod.getInstance());
}
}
activity_main
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
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="com.example.yongliu.spannablestringbuilder.MainActivity">
<TextView
android:layout_width="match_parent"
android:layout_height="45dp"
android:gravity="center"
android:id="@+id/content"/>
</LinearLayout>
项目场景
文本不同字号(字体大小)
感觉当设置同等字体
Size
后,显示效果不同,主要是因为屏幕适配导致的,我们可以采用对应的dp、px转换工具
试一试
实现效果
起初我使用的就是以下 RelativeSizeSpan
声明的字体大小,但是实验效果不佳
val data = SpannableStringBuilder("您还未领取0折购基打折卡,无法享受\n0折购基的优惠,是否现在去领取?")
val startSize = RelativeSizeSpan(13.0f)
val endSize = RelativeSizeSpan(17.0f)
data.setSpan(startSize, 0, 5, Spannable.SPAN_INCLUSIVE_INCLUSIVE);
data.setSpan(endSize, 5, data.length, Spannable.SPAN_INCLUSIVE_INCLUSIVE);
content.text = data
后续我改用为 AbsoluteSizeSpan
声明的字体大小,效果不错
val data = SpannableStringBuilder("您还未领取0折购基打折卡,无法享受\n0折购基的优惠,是否现在去领取?")
data.setSpan(AbsoluteSizeSpan(13,true),0,4, Spannable.SPAN_INCLUSIVE_INCLUSIVE)
data.setSpan(AbsoluteSizeSpan(17,true),5,9, Spannable.SPAN_INCLUSIVE_EXCLUSIVE)
data.setSpan(AbsoluteSizeSpan(13,true),9,data.length, Spannable.SPAN_INCLUSIVE_INCLUSIVE)
content.text = data
文本加粗、多色、含图标、点击事件
Tips
:通过 Kotlin扩展函数
快速实现 字体颜色不同、区分加粗效果、添加图片
等多样式富文本效果
场景一
实现方式
val drawable = resources.getDrawable(R.drawable.icon_config_scheme_arrow_blue)
drawable.setBounds(0, 0, 12.dp, 12.dp);
val imageSpan = ImageSpan(drawable, ImageSpan.ALIGN_BASELINE)
val spannable = SpannableStringBuilder()
spannable.appendMore("#666666".toColorInt(), "根据你的风险偏好及期望的投资时长,通过资产配置测算,建议你")
spannable.mediumSmall {
color("#333333".toColorInt()) {
append("稳健类资产配置${"40%"},权益类资产配置${"60%"}。")
}
}
spannable.appendMore("#1760EA".toColorInt(), "了解更多>") {
//如果是在组件中的话,可以直接在这里写对应逻辑,如果是在一些容器内部就需要写接口回调了!
//callback?.invoke("")
}
spannable.setSpan(imageSpan, spannable.length-1, spannable.length, Spannable.SPAN_INCLUSIVE_INCLUSIVE)
bind.tvAssetDesc.text = spannable
bind.tvAssetDesc.movementMethod = LinkMovementMethod.getInstance();
场景二
实现方式
//尾部图标
val drawable = ContextCompat.getDrawable(this.context, R.drawable.icon_base_blue_arrow_right)
var imageSpan: ImageSpan? = null
drawable?.let {
it.setBounds(0, -5, drawable.intrinsicWidth, drawable.intrinsicHeight);
imageSpan = ImageSpan(drawable, ImageSpan.ALIGN_BOTTOM)
}
val spannable = SpannableStringBuilder()
spannable.appendMore(
//data为显示的文本
"#EA6B17".toColorInt(), data.string()
)
spannable.appendMore("#1760EA".toColorInt(), "查看分红详情>") {
//这里我是回调到外部组件使用的,如果在当前组件使用的话,直接写逻辑就行
callback?.invoke("")
}
imageSpan?.let {
spannable.setSpan(imageSpan, spannable.length - 1, spannable.length, Spannable.SPAN_INCLUSIVE_INCLUSIVE)
}
textView.text = spannable
textView.movementMethod = LinkMovementMethod.getInstance()
未读消息的小红点
TextView
尾部在特定时间加入小红点图片,此为我项目中一段代码,当做自我笔记,以防遗忘
SpannableString spannableString = new SpannableString(content);
//设置图片的方法
Drawable drawable = GarageApp.getAppContext().getResources().getDrawable(R.drawable.shape_msg_red_dot);
drawable.setBounds(0, 0, DeviceUtils.dp2px(GarageApp.getAppContext(), 8), DeviceUtils.dp2px(GarageApp.getAppContext(), 8));
ImageSpan imageSpan = new ImageSpan(drawable, ImageSpan.ALIGN_BASELINE);
spannableString.setSpan(imageSpan, content.length() - 1, content.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
tvContent.setText(spannableString);
DeviceUtils.dp2px
(尺寸转换方法)
/**
* dp 转化为 px
* @param context context
* @param dpValue dpValue
* @return int
*/
public static int dp2px(Context context, float dpValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (dpValue * scale + 0.5f);
}
/**
* px 转化为 dp
* @param context context
* @param pxValue pxValue
*/
public static int px2dp(Context context, float pxValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (pxValue / scale + 0.5f);
}
public static int px2sp(Context context, float pxValue) {
final float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
return (int) (pxValue / fontScale + 0.5f);
}
public static int sp2px(Context context, float spValue) {
final float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
return (int) (spValue * fontScale + 0.5f);
}
文本渐变
//渐变色值组
private val titleColors = intArrayOf("#F0F3FF".toColorInt(), "#9BB3FF".toColorInt())
//设置效果
textView.text = buildSpannedString { linearGradient
(colors = titleColors, orientation = LinearGradientFontSpan.VERTICAL) { append("能幼稚一生,何尝不是一种幸福}") }}
定制Span
MediumSmallSpan(加粗)
import android.graphics.Paint
import android.text.TextPaint
import android.text.style.MetricAffectingSpan
class MediumSmallSpan : MetricAffectingSpan() {
override fun updateDrawState(paint: TextPaint?) = apply(paint)
override fun updateMeasureState(paint: TextPaint) = apply(paint)
private fun apply(paint: Paint?) {
if (paint == null) return
paint.strokeWidth = 0.6f
paint.style = Paint.Style.FILL_AND_STROKE
}
}
LinearGradientFontSpan(渐变)
import android.graphics.Canvas
import android.graphics.LinearGradient
import android.graphics.Paint
import android.graphics.Shader
import android.text.style.ReplacementSpan
import android.widget.LinearLayout
class LinearGradientFontSpan(val colors: IntArray, val orientation: Int = HORIZONTAL) : ReplacementSpan() {
companion object {
const val HORIZONTAL = LinearLayout.HORIZONTAL // 水平渐变方向
const val VERTICAL = LinearLayout.VERTICAL // 垂直渐变方向
}
private var mMeasureTextWidth = 0 // 测量的文本宽度
override fun getSize(paint: Paint, text: CharSequence?, start: Int, end: Int, fontMetricsInt: Paint.FontMetricsInt?): Int {
mMeasureTextWidth = paint.measureText(text ?: "", start, end).toInt()
// 这段不可以去掉,字体高度没设置,会出现 draw 方法没有被调用的问题
// 详情请见:https://stackoverflow.com/questions/20069537/replacementspans-draw-method-isnt-called
val metrics = paint.fontMetrics
fontMetricsInt?.top = metrics.top.toInt()
fontMetricsInt?.ascent = metrics.ascent.toInt()
fontMetricsInt?.descent = metrics.descent.toInt()
fontMetricsInt?.bottom = metrics.bottom.toInt()
return mMeasureTextWidth
}
override fun draw(canvas: Canvas, text: CharSequence?, start: Int, end: Int, x: Float, top: Int, y: Int, bottom: Int, paint: Paint) {
if (text.isNullOrEmpty()) return
val linearGradient = if (orientation == VERTICAL) {
LinearGradient(0f, 0f, 0f, paint.descent() - paint.ascent(), colors, null, Shader.TileMode.REPEAT)
} else {
LinearGradient(x, 0f, x + mMeasureTextWidth, 0f, colors, null, Shader.TileMode.REPEAT)
}
val shader = paint.shader
val alpha = paint.alpha
//
paint.shader = linearGradient
paint.alpha = 255 // 如果是则设置不透明
//
canvas.drawText(text, start, end, x, y.toFloat(), paint)
//绘制完成之后将画笔的透明度还原回去
paint.shader = shader
paint.alpha = alpha
}
}
ClickableSpan(点击)
import android.text.TextPaint
import android.view.View
import cn.com.huaan.fund.acts.core.android.listener.OnClickWrapListener
class ClickableSpan(val listener: View.OnClickListener) : android.text.style.ClickableSpan() {
override fun onClick(widget: View) {
OnClickWrapListener(listener).onClick(widget)
}
override fun updateDrawState(ds: TextPaint) {
ds.isUnderlineText = false
}
}
SpannableStringBuilder扩展函数(篇中多次使用,重要!)
package cn.com.xx
import android.graphics.Typeface
import android.os.Build
import android.text.SpannableStringBuilder
import android.text.style.*
import android.view.View
import androidx.annotation.ColorInt
import androidx.annotation.RequiresApi
import androidx.core.graphics.toColorInt
import androidx.core.text.inSpans
import cn.com.huaan.fund.acts.core.android.text.spannable.ClickableSpan
import cn.com.huaan.fund.acts.core.android.text.spannable.LinearGradientFontSpan
import cn.com.huaan.fund.acts.core.android.text.spannable.MediumSmallAndLSpan
import cn.com.huaan.fund.acts.core.android.text.spannable.MediumSmallSpan
inline fun SpannableStringBuilder.mediumSmall(
builderAction: SpannableStringBuilder.() -> Unit
) = inSpans(MediumSmallSpan(), builderAction = builderAction)
inline fun SpannableStringBuilder.mediumSmallAndRelativeSize(
builderAction: SpannableStringBuilder.() -> Unit
) = inSpans(MediumSmallAndLSpan(), builderAction = builderAction)
inline fun SpannableStringBuilder.linearGradient(
colors: IntArray, orientation: Int = LinearGradientFontSpan.HORIZONTAL, builderAction: SpannableStringBuilder.() -> Unit
): SpannableStringBuilder {
return inSpans(LinearGradientFontSpan(colors, orientation), builderAction = builderAction)
}
inline fun SpannableStringBuilder.clickable(
listener: View.OnClickListener, builderAction: SpannableStringBuilder.() -> Unit
): SpannableStringBuilder {
return inSpans(ClickableSpan(listener), builderAction = builderAction)
}
//
fun SpannableStringBuilder.appendClickable(
text: CharSequence, listener: View.OnClickListener
): SpannableStringBuilder = inSpans(ClickableSpan(listener)) { append(text) }
fun SpannableStringBuilder.appendColor(
@ColorInt color: Int,
text: CharSequence,
): SpannableStringBuilder = inSpans(ForegroundColorSpan(color)) { append(text) }
fun SpannableStringBuilder.appendStrikeThrough(
text: CharSequence,
): SpannableStringBuilder = inSpans(StrikethroughSpan()) { append(text) }
fun SpannableStringBuilder.appendRelativeSize(
proportion: Float,
text: CharSequence,
): SpannableStringBuilder = inSpans(RelativeSizeSpan(proportion)) { append(text) }
fun SpannableStringBuilder.appendAbsoluteSize(
proportion: Int,
text: CharSequence,
): SpannableStringBuilder = inSpans(AbsoluteSizeSpan(proportion, true)) { append(text) }
//
fun SpannableStringBuilder.appendMore(
@ColorInt color: Int? = null,
text: CharSequence,
listener: View.OnClickListener? = null,
): SpannableStringBuilder {
val spans = mutableListOf<Any>()
color?.also { spans.add(ForegroundColorSpan(color)) }
listener?.also { spans.add(ClickableSpan(listener)) }
inSpans(*spans.toTypedArray()) { append(text) }
return this
}
fun SpannableStringBuilder.appendMediumSmallMore(
@ColorInt color: Int? = null,
text: CharSequence,
listener: View.OnClickListener? = null,
): SpannableStringBuilder {
val spans = mutableListOf<Any>()
color?.also { spans.add(ForegroundColorSpan(color)) }
listener?.also { spans.add(ClickableSpan(listener)) }
spans.add(MediumSmallSpan())
inSpans(*spans.toTypedArray()) { append(text) }
return this
}
fun SpannableStringBuilder.appendLabel(
@ColorInt color: Int? = null,
text: String,
listener: View.OnClickListener? = null,
): SpannableStringBuilder {
val splits = text.replace("</b>", "<b>").split("<b>")
splits.forEachIndexed { index, s ->
if (index % 2 == 1) {
appendMore(color = color, s, listener)
} else {
append(s)
}
}
return this
}
@RequiresApi(Build.VERSION_CODES.P)
fun SpannableStringBuilder.appendTypeface(
typeface: Typeface,
text: CharSequence,
) = inSpans(TypefaceSpan(typeface)) { append(text) }
fun SpannableStringBuilder.appendColorAbsoluteSizeMediumSmall(
text: CharSequence,
@ColorInt color: Int? = "#333333".toColorInt(),
proportion: Int = 14,
): SpannableStringBuilder {
val spans = mutableListOf<Any>()
color?.also { spans.add(ForegroundColorSpan(color)) }
proportion.also { spans.add(AbsoluteSizeSpan(proportion, true)) }
spans.add(MediumSmallSpan())
inSpans(*spans.toTypedArray()) { append(text) }
return this
}
常见问题
可能是常见使用场景,也可能常见的出错场景,希望可以帮到你我
文本中加入图标后,未与文本居中?有时候甚至错位?
我们为了让图标与文本对齐居中,通常会将其设置为 ImageSpan(drawable, ImageSpan.ALIGN_CENTER)
,但是在上方 文本加粗、多色、含图标、点击事件-场景二
出现了 部分机型图标显示错位,直接出现在首行, x距离是对的,但是y的距离差太多
,故采用如下方式
val drawable = ContextCompat.getDrawable(this.context, R.drawable.icon_base_blue_arrow_right)
drawable?.let {
//因为使用了居底模式,所以采用负值实现居中效果
it.setBounds(0, -5, drawable.intrinsicWidth, drawable.intrinsicHeight);
//设置为ALIGN_BOTTOM模式
imageSpan = ImageSpan(drawable, ImageSpan.ALIGN_BOTTOM)
}
设置字体Size和理想中有区别?
提示:我在项目中有一个需求是
不同区间,字体显示的大小有所区别
,在使用RelativeSizeSpan(相对)
方式设置后,效果不太理想,后续改为了AbsoluteSizeSpan(绝对)
方式(具体实现在该篇的项目经验
)
val data = SpannableStringBuilder("少年可知,鲜衣怒马?")
val startSize = RelativeSizeSpan(13.0f)
val endSize = RelativeSizeSpan(17.0f)
data.setSpan(startSize, 0, 4, Spannable.SPAN_INCLUSIVE_INCLUSIVE);
data.setSpan(endSize, 5, data.length, Spannable.SPAN_INCLUSIVE_INCLUSIVE);
content.text = data
样式未生效?
设置文本时
不能使用后builder的toString()方法!!!
,如果您这样做了,那么辛辛苦苦设置的样式可能就被覆盖了,并不会显示出来
- 首先检查是否设置对应的
Span
,区间是否为有效区间? - 设置
span
后,是否有调用toString()
方法?
点击事件未生效?
设置了点击事件却无效的时候, 查看是否有 setMovementMethod
,如没有的话,那么像下面一样进行设置
//mContent TextView控件
mContent.setMovementMethod(LinkMovementMethod.getInstance());
超链接要注意什么?
- 如果要使用超链接的设置,需要同时设置点击事件,不然无法触发!
- 如果超链接在点击事件内生效的话,那么会优先超链接,同时消耗此事件,其他操作点击操作将无法触发(个人Demo察觉)
- 超链接正常跳转之后你会发现你没有加入Intent的网络权限!神不神奇!~