自定义Veiw实战《渐变的文字》
- 前言
- SpannableString
- 什么时候使用
- 常用场景
- 自定义TextView控件
- 什么时候使用
- 实战歌词渐变的文字
- 先看效果
- 第一步:初始化
- 第二步:测量文字,并确认文字绘制的起点
- 第三步:绘制文字
- 第四步:调用
- 总结:完整代码
前言
首先跟文字相关的,我们大多数会使用TextView来解决,但是我们的TextView实现的效果又很固定。
当我们想实现一些独特的效果的时候,大多数为两种方法
1、一种是系统提供给我们的API:SpannableString
2、第二种是:自定义View
本文结构为:简单说一下SpannableString,之后实战用自定义View实现歌词效果。
为什么要讲一下SpannableString,因为很多人不知道这个API会直接想去自定义View把问题搞复杂。如果SpannableString能解决拿来用岂不是很舒服。
SpannableString
什么时候使用
有的时候,我们要给一个TextView中的显示的文字设置不同的样式或者响应事件,比如同一个TextView中,有的字是红色,有的字是蓝色,有的字点击之后有响应事件,有的点击之后没有响应事件,甚至我们想在TextView中显示一个数学公式等等,这个时候就需要使用SpannableString来解决这个问题。(当然HTML也可以)
比较常见的地方:所有APP的权限同意,都是不同颜色的文字,并且点击协议可以跳转到协议界面。
常用场景
首先列举一下可以实现的场景再拿出来两个说一下使用:
1.设置TextView的背景颜色
2.给文本设置点击事件
3.设置文本颜色
4.设置删除线效果
5.设置下划线效果
6.在TextView中设置图片
7.基于X轴的缩放
8.设置字体粗体样式
9.上下标的使用
10.设置超链接
1、设置TextView的背景颜色
textView1 = findViewById<View>(R.id.textView1) as TextView
val ss = SpannableString("设置背景颜色")
ss.setSpan(
BackgroundColorSpan(Color.parseColor("#FFD700")), 0,
ss.length, Spanned.SPAN_EXCLUSIVE_INCLUSIVE
)
textView1.setText(ss)
首先拿到一个TextView,然后构造一个 SpannableString,构造方法中传入的参数就是我们要显示的文字,然后就是一个最重要的方法,通过setSpan来设置背景色,第一个参数是我们要设置的背景颜色,第二第三个参数是我们要给哪一段的文字设置背景(该段文字的startIndex和endIndex),最后一个参数有四个值:
public static final int SPAN_INCLUSIVE_EXCLUSIVE = SPAN_MARK_MARK;
public static final int SPAN_INCLUSIVE_INCLUSIVE = SPAN_MARK_POINT;
public static final int SPAN_EXCLUSIVE_EXCLUSIVE = SPAN_POINT_MARK;
public static final int SPAN_EXCLUSIVE_INCLUSIVE = SPAN_POINT_POINT;
1.前面包括,后面不包括,即在文本前插入新的文本会应用该样式,而在文本后插入新文本不会应用该样式
2.前面包括,后面包括,即在文本前插入新的文本会应用该样式,而在文本后插入新文本也会应用该样式
3.前面不包括,后面不包括
4.前面不包括,后面包括
2、给文本设置点击事件
如果我们只想给一个TextView中的某几个文字设置点击事件,而不想给整个TextView设置点击事件,那么该怎么做?看下面的代码
textView2 = findViewById<View>(R.id.textView2) as TextView
val ss = SpannableString("点我吧123456")
ss.setSpan(object : ClickableSpan() {
override fun onClick(widget: View) {
Toast.makeText(this, "点我呀", Toast.LENGTH_SHORT)
.show()
}
}, 0, 3, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
textView2.setText(ss)
// 设置tv2为可点击状态
textView2.setMovementMethod(LinkMovementMethod.getInstance())
setSpan方法中,第一个参数是一个ClickableSpan对象,这里有一个onClick方法,该方法中就是我们对点击事件的响应,后面几个参数跟前面一样,是位置和模式。
行就说到这,还需要什么场景就去查一下吧。
自定义TextView控件
什么时候使用
说白了就是TextView和SpannableString实现不了效果的时候,比如我想实现一个跟进度条联动的渐变歌词。
实战歌词渐变的文字
先看效果
核心思想:
画布canvas有一个方法canvas.clipRect(),调用了这个方法后接下来只会在这个区域内画内容,超出这个区域的内容就不画了。那么对于我们歌词渐变。
我们先用默认颜色画出全部文本,然后呢,根据变量progress(渐变比例,范围[0,1])和方向direction(确定从左到右渐变还是从右到左)计算出要变色的区域,然后用渐变颜色再画一次文本即可。
开始实现:(后面会附带完整的代码)
第一步:初始化
自定义View首先要初始化我们的自定义属性,
自定义属性CustomTextView
attr.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--第一步-->
<declare-styleable name="CustomTextView">
<attr name="text" format="string"/>
<attr name="text_size" format="dimension" />
<attr name="text_origin_color" format="color|reference" />
<attr name="text_change_color" format="color|reference" />
<attr name="progress" format="float" />
<attr name="direction">
<enum name="left" value="0" />
<enum name="right" value="1" />
<enum name="top" value="2" />
<enum name="bottom" value="3" />
</attr>
</declare-styleable>
</resources>
使用时在XML中设置我们的自定义属性
<com.example.meng.view.CustomTextView
android:id="@+id/tv_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:background="#44ff0000"
android:padding="10dp"
mql:progress="0"
mql:text="小孟来码-Android"
mql:text_change_color="#ffff0000"
mql:text_origin_color="#ff000000"
mql:text_size="20sp" />
CustomTextView构造函数中获取
//XML中正常使用时候
constructor(context: Context, attrs: AttributeSet) : super(context, attrs){
//初始化
val at = context.obtainStyledAttributes(attrs, R.styleable.CustomTextView)
mText = at.getString(R.styleable.CustomTextView_text) ?: ""
textSize = at.getDimension(R.styleable.CustomTextView_text_size, SizeUtil.spToPx(context, 16f))
defaultColor = at.getColor(R.styleable.CustomTextView_text_origin_color, defaultColor)
changeColor = at.getColor(R.styleable.CustomTextView_text_change_color, changeColor)
direction = at.getInt(R.styleable.CustomTextView_direction, DIRECTION_LEFT)
progress = at.getFloat(R.styleable.CustomTextView_progress, 0f)
at.recycle()
initPaint()
}
private fun initPaint() {
paint.textSize = textSize
}
上面就成功的通过自定义的属性获取到了我们需要的自定义属性
第二步:测量文字,并确认文字绘制的起点
这里说一下我们利用draw去绘制文字时,他的起始坐标的问题。
drawText(String text, float x, float y, Paint paint)
方法的参数很简单: text 是文字内容,x 和 y 是文字的坐标。但需要注意:这个坐标并不是文字的左上角,而是一个与左下角比较接近的位置。大概在这里:
记住这个来看下面获取宽度高度和找起点坐标的代码吧
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
getMeasuredText()
val width = measure(widthMeasureSpec, true)
val height = measure(heightMeasureSpec, false)
setMeasuredDimension(width, height)
// 绘制Text的起始坐标 这是为了将文字水平居中对齐。
// 首先,将控件的中心点作为起始点,即getMeasuredWidth() / 2
// 然后,将文本的宽度除以2,即mTextWidth / 2
// 得到文本的一半宽度。最后,将文本的一半宽度从起始点左移,
// 即mTextStartX = getMeasuredWidth() / 2 - mTextWidth / 2,就可以将文本水平居中对齐
textStartX = measuredWidth / 2 - textWidth / 2
textStartY = measuredHeight / 2 + textHeight / 2
}
private fun getMeasuredText() {
//获取文字高度,设置给onMeasure 需要告诉View画多大
//这里如果你是继承的TextView那么这个可以不用做,因为TextView中的onMeasure会帮你做好。
val rect = Rect()
paint.getTextBounds(mText, 0, mText.length, rect)
textHeight = rect.height()
textWidth = paint.measureText(mText).toInt()
}
private fun measure(measureSpec: Int, isWidth: Boolean): Int {
val mode = MeasureSpec.getMode(measureSpec)
val size = MeasureSpec.getSize(measureSpec)
var result: Int = 0
when (mode) {
//精准模式
MeasureSpec.EXACTLY -> {
result = size
}
//最大模式 未指定
MeasureSpec.AT_MOST, MeasureSpec.UNSPECIFIED -> {
result = if (isWidth) {
textWidth
} else {
textHeight
}
}
}
return if (isWidth) result + paddingLeft + paddingRight else result + paddingTop + paddingBottom
}
第三步:绘制文字
绘制文字就是利用canvas.drawText去绘制了。
使用canvas.clipRect()去控制区域,可以看到他的参数
public boolean clipRect(int left, int top, int right, int bottom)
顾名思义就是区域的四个顶点了。通过这个就可以去控制他的区域了
之后就是计算出来他的绘制区域,这里是通过progress来进行计算。
比如textStartX + progress * textWidth:就是起始的X加上progress(0 - 1) 乘 全部字体的宽度。
我们通过改变progress就能控制他的绘制的结束位置。
代码如下:
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
if (direction == DIRECTION_LEFT){
drawChangeLeft(canvas)
drawOriginLeft(canvas)
} else if (direction == DIRECTION_RIGHT){
drawChangeRight(canvas)
drawOriginRight(canvas)
}
}
private fun drawChangeRight(canvas: Canvas) {
drawText(canvas, changeColor, (textStartX + (1 - progress) * textWidth).toInt(), textStartX + textWidth)
}
private fun drawOriginRight(canvas: Canvas){
drawText(canvas, defaultColor, textStartX, (textStartX + (1 - progress) * textWidth).toInt())
}
private fun drawChangeLeft(canvas: Canvas){
drawText(canvas, changeColor, textStartX, (textStartX + progress * textWidth).toInt())
}
private fun drawOriginLeft(canvas: Canvas) {
drawText(canvas, defaultColor, (textStartX + progress * textWidth).toInt(), textStartX + textWidth)
}
private fun drawText(canvas: Canvas, color: Int, startX: Int, endX: Int) {
paint.color = color
canvas.save()
canvas.clipRect(startX, 0, endX, measuredHeight)
canvas.drawText(mText, textStartX.toFloat(), textStartY.toFloat(), paint)
canvas.restore()
}
//设置进度(动态的改变需要set这个属性)
fun setProgress(progress: Float) {
this.progress = progress
invalidate() //重绘
}
第四步:调用
MainActivity
setContentView(R.layout.activity_main)
val tvContent: CustomTextView = findViewById(R.id.tv_content)
val Button1: Button = findViewById(R.id.button_1)
val Button2: Button = findViewById(R.id.button_2)
Button1.setOnClickListener {
tvContent.textDirection = 0
ObjectAnimator.ofFloat(tvContent, "progress", 0f, 1f).setDuration(4000).start()
}
Button2.setOnClickListener {
tvContent.textDirection = 1
ObjectAnimator.ofFloat(tvContent, "progress", 1f, 0f).setDuration(4000).start()
}
总结:完整代码
不多逼逼,来拿代码
attr.xml的代码上面有,放在values下的attr.xml文件中就行
CustomTextView.kt
const val DIRECTION_LEFT = 0
const val DIRECTION_RIGHT = 1
const val DIRECTION_TOP = 2
const val DIRECTION_BOTTOM = 3
class CustomTextView : View {
private var mText: String = "小孟来码"
private var textSize = SizeUtil.spToPx(context, 30f)
private var defaultColor: Int = 0xff000000.toInt()
private var changeColor: Int = 0xffff0000.toInt()
private var direction = DIRECTION_LEFT
private var progress = 0f
private var paint: Paint = Paint(Paint.ANTI_ALIAS_FLAG)
private var textHeight = 0
private var textWidth = 0
private var textStartX = 0
private var textStartY = 0
//利用对象创建的时候
constructor(context: Context) : super(context)
//XML中正常使用时候
constructor(context: Context, attrs: AttributeSet) : super(context, attrs){
//初始化
val at = context.obtainStyledAttributes(attrs, R.styleable.CustomTextView)
mText = at.getString(R.styleable.CustomTextView_text) ?: ""
textSize = at.getDimension(R.styleable.CustomTextView_text_size, SizeUtil.spToPx(context, 16f))
defaultColor = at.getColor(R.styleable.CustomTextView_text_origin_color, defaultColor)
changeColor = at.getColor(R.styleable.CustomTextView_text_change_color, changeColor)
direction = at.getInt(R.styleable.CustomTextView_direction, DIRECTION_LEFT)
progress = at.getFloat(R.styleable.CustomTextView_progress, 0f)
at.recycle()
initPaint()
}
private fun initPaint() {
paint.textSize = textSize
}
private fun getMeasuredText() {
//获取文字高度,设置给onMeasure 需要告诉View画多大
//这里如果你是继承的TextView那么这个可以不用做,因为TextView中的onMeasure会帮你做好。
val rect = Rect()
paint.getTextBounds(mText, 0, mText.length, rect)
textHeight = rect.height()
textWidth = paint.measureText(mText).toInt()
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
getMeasuredText()
val width = measure(widthMeasureSpec, true)
val height = measure(heightMeasureSpec, false)
setMeasuredDimension(width, height)
// 绘制Text的起始坐标 这是为了将文字水平居中对齐。
// 首先,将控件的中心点作为起始点,即getMeasuredWidth() / 2
// 然后,将文本的宽度除以2,即mTextWidth / 2
// 得到文本的一半宽度。最后,将文本的一半宽度从起始点左移,
// 即mTextStartX = getMeasuredWidth() / 2 - mTextWidth / 2,就可以将文本水平居中对齐
textStartX = measuredWidth / 2 - textWidth / 2
textStartY = measuredHeight / 2 + textHeight / 2
}
private fun measure(measureSpec: Int, isWidth: Boolean): Int {
val mode = MeasureSpec.getMode(measureSpec)
val size = MeasureSpec.getSize(measureSpec)
var result: Int = 0
when (mode) {
//精准模式
MeasureSpec.EXACTLY -> {
result = size
}
//最大模式 未指定
MeasureSpec.AT_MOST, MeasureSpec.UNSPECIFIED -> {
result = if (isWidth) {
textWidth
} else {
textHeight
}
}
}
return if (isWidth) result + paddingLeft + paddingRight else result + paddingTop + paddingBottom
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
if (direction == DIRECTION_LEFT){
drawChangeLeft(canvas)
drawOriginLeft(canvas)
} else if (direction == DIRECTION_RIGHT){
drawChangeRight(canvas)
drawOriginRight(canvas)
}
}
private fun drawChangeRight(canvas: Canvas) {
drawText(canvas, changeColor, (textStartX + (1 - progress) * textWidth).toInt(), textStartX + textWidth)
}
private fun drawOriginRight(canvas: Canvas){
drawText(canvas, defaultColor, textStartX, (textStartX + (1 - progress) * textWidth).toInt())
}
private fun drawChangeLeft(canvas: Canvas){
drawText(canvas, changeColor, textStartX, (textStartX + progress * textWidth).toInt())
}
private fun drawOriginLeft(canvas: Canvas) {
drawText(canvas, defaultColor, (textStartX + progress * textWidth).toInt(), textStartX + textWidth)
}
private fun drawText(canvas: Canvas, color: Int, startX: Int, endX: Int) {
paint.color = color
canvas.save()
canvas.clipRect(startX, 0, endX, measuredHeight)
canvas.drawText(mText, textStartX.toFloat(), textStartY.toFloat(), paint)
canvas.restore()
}
//设置进度(动态的改变需要set这个属性)
fun setProgress(progress: Float) {
this.progress = progress
invalidate() //重绘
}
}
activity_main.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:mql="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<com.example.meng.view.CustomTextView
android:id="@+id/tv_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:background="#44ff0000"
android:padding="10dp"
mql:progress="0"
mql:text="小孟来码-Android"
mql:text_change_color="#ffff0000"
mql:text_origin_color="#ff000000"
mql:text_size="20sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:gravity="center"
android:orientation="horizontal" >
<Button
android:id="@+id/button_1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="StartLeft" />
<Button
android:id="@+id/button_2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toRightOf="@id/button_1"
android:text="StartRight" />
</LinearLayout>
</RelativeLayout>
MainActivity.kt
class MainActivity : AppCompatActivity(){
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val tvContent: CustomTextView = findViewById(R.id.tv_content)
val Button1: Button = findViewById(R.id.button_1)
val Button2: Button = findViewById(R.id.button_2)
Button1.setOnClickListener {
tvContent.textDirection = 0
ObjectAnimator.ofFloat(tvContent, "progress", 0f, 1f).setDuration(4000).start()
}
Button2.setOnClickListener {
tvContent.textDirection = 1
ObjectAnimator.ofFloat(tvContent, "progress", 1f, 0f).setDuration(4000).start()
}
}
}