Custom Veiw actual combat "gradient text"

foreword

First of all, related to text, most of us will use TextView to solve it, but the effect of our TextView is very fixed.
When we want to achieve some unique effects, most of them are two methods

1. One is the API provided to us by the system: SpannableString
2. The second is: custom View

The structure of this article is: briefly talk about SpannableString, and then use the custom View to realize the lyrics effect in actual combat.
Why talk about SpannableString, because many people don't know this API and will directly want to customize the View to complicate the problem. Wouldn't it be very comfortable if SpannableString could be solved and used.

SpannableString

when to use

Sometimes, we need to set different styles or respond to events for the text displayed in a TextView. For example, in the same TextView, some words are red, some are blue, and some words have response events after clicking. Some clicks do not respond to events, and even we want to display a mathematical formula in TextView, etc. At this time, we need to use SpannableString to solve this problem. (Of course, HTML is also available.)
More common places: all APP permission consents are written in different colors, and clicking on the agreement can jump to the agreement interface.

Common scene

First, list the scenarios that can be realized, and then take out two to talk about their use:

1. Set the background color of the TextView
2. Set the click event for the text
3. Set the text color
4. Set the strikethrough effect 5. Set the underline
effect
6. Set the picture in the TextView
7. Zoom based on the X axis
8. Set the font bold Style
9. Use of subscripts and subscripts
10. Setting hyperlinks

1. Set the background color of 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)

First get a TextView, and then construct a SpannableString. The parameter passed in the construction method is the text we want to display, and then the most important method is to set the background color through setSpan. The first parameter is the background we want to set Color, the second and third parameters are the text we want to set the background for (the startIndex and endIndex of the text), and the last parameter has four values:

    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. Include in the front, exclude in the back, that is, inserting new text before the text will apply the style, but inserting new text after the text will not apply the style 2. Include in the front, include in the back, that is, insert new text before the
text The style will be applied, and inserting new text after the text will also apply the style
3. The front is not included, and the back is not included
4. The front is not included, and the back is included

2. Set click events for text
If we only want to set click events for certain texts in a TextView, but not for the entire TextView, what should we do? look at the code below

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())

In the setSpan method, the first parameter is a ClickableSpan object. There is an onClick method, which is our response to the click event. The next few parameters are the same as the previous ones, which are the position and mode.
That's all for now, let's look up any scenes you need.

Custom TextView control

when to use

To put it bluntly, it is when TextView and SpannableString can't achieve the effect, for example, I want to realize a gradient lyrics linked with the progress bar.

Gradient Text of Actual Lyrics

first look at the effect

insert image description here

Core idea:
Canvas has a method canvas.clipRect(). After calling this method, only the content will be drawn in this area, and the content beyond this area will not be drawn. Then for our lyric gradient.
We first draw all the text with the default color, and then calculate the color-changing area according to the variable progress (gradient ratio, range [0,1]) and direction (determine the gradient from left to right or from right to left), Then paint the text again with the gradient color.

Start to implement: (full code will be attached later)

Step 1: Initialization

Custom View must first initialize our custom attributes,
custom attributes 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>

Set our custom attribute in XML when using

<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" />

Obtained in the CustomTextView constructor

//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
}

The above successfully obtained the custom attributes we need through the custom attributes

Step 2: Measure the text and confirm the starting point of the text drawing

Let me talk about the problem of its starting coordinates when we use draw to draw text.

drawText(String text, float x, float y, Paint paint)

The parameters of the method are simple: text is the text content, and x and y are the coordinates of the text. But it should be noted that this coordinate is not the upper left corner of the text, but a position closer to the lower left corner. Probably here:
insert image description here
remember this and look at the code below to get the width and height and find the coordinates of the starting point

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
}

Step 3: Drawing Text

Drawing text is to use canvas.drawText to draw.
Use canvas.clipRect() to control the area, you can see its parameters

public boolean clipRect(int left, int top, int right, int bottom)

As the name implies, it is the four vertices of the area. Through this, you can control his area,
and then calculate his drawing area, which is calculated by progress here.
For example, textStartX + progress * textWidth: It is the initial X plus progress(0 - 1) multiplied by the width of the entire font.
We can control the end position of his drawing by changing the progress.
code show as below:

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() //重绘
}

Step 4: Call

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()
}

Combined with ViewPage to realize gradient Tab linkage

This will not be realized for everyone, as soon as I say it, everyone will know.
First look at the explanation of this method of ViewPage.
ViewPage can call back the following three methods through monitoring.

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){
    
    }
		});

in

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

The three parameters of are expressed as:

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

Now everyone knows, we can pass positionOffset as our process into our gradient text View. thus use.
The core code is as follows:

	@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);
		}

Summary: CustomTextView complete code

Not much force, let’s get the code
attr.xml above, just put it in the attr.xml file under values
​​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()
        }
    }
}

Guess you like

Origin blog.csdn.net/weixin_45112340/article/details/131433430