自定义Veiw实战《渐变的文字》

前言

首先跟文字相关的,我们大多数会使用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()
}

结合ViewPage实现渐变Tab联动

这个不给大家实现了,我一说大家就都知道了
先看ViewPage的这个方法的解释
ViewPage通过监听可以回调下面三个方法。

mViewPager.setOnPageChangeListener(new OnPageChangeListener()
		{
    
    
			@Override
			public void onPageSelected(int position){
    
    }
 
			@Override
			public void onPageScrolled(int position, float positionOffset,
					int positionOffsetPixels){
    
    	}
 
			@Override
			public void onPageScrollStateChanged(int state){
    
    }
		});

其中

public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels)

的三个参数表示为:

- position表示当前所在的页面位置,取值范围为0至页面总数减1- positionOffset表示当前页面相对于整个页面的偏移量,取值范围为0.01.0- positionOffsetPixels表示当前页面相对于整个页面的像素偏移量。这个参数可以用来实现一些动画效果。

这下大家知道了吧,我们可以把positionOffset作为我们的process传入我们的渐变色文字View。从而使用。
核心代码如下:

	@Override
	public void onPageScrolled(int position, float positionOffset,
							   int positionOffsetPixels) {
    
    
		if (positionOffset > 0) {
    
    
			ColorTrackView left = mTabs.get(position);
			ColorTrackView right = mTabs.get(position + 1);

			left.setDirection(1);
			right.setDirection(0);
			Log.e("TAG", positionOffset+"");
			left.setProgress( 1-positionOffset);
			right.setProgress(positionOffset);
		}

总结:CustomTextView完整代码

不多逼逼,来拿代码
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()
        }
    }
}

猜你喜欢

转载自blog.csdn.net/weixin_45112340/article/details/131433430