效果展示
分析
- 动态效果,使用Animator实现
- 自定义View
- 继承TextView
- 使用TextView的测量,不重写
- 使用TextView的布局,不重写
- 绘制-重写绘制
- 使用两种颜色绘制文本
- 颜色占比不同,百分比从0~1
实现
自定义属性
从需求可知,需要定义两种颜色。
- 在attrs.xml中使用declare-styleable标签声明属性。
- 自定义View.java中使用TypedArray获取xml属性值(在构造函数中获取)
注意:获取值后,需要使用typedArray.recycle()回收。
- xml中使用自定义View及其自定义属性
创建画笔
绘制,就是在画布上用画笔绘制内容。
要绘制两种颜色的文字,需要两支画笔,画笔有不同的颜色和大小,使用颜色的大小创建画笔。
绘制文字
画布Canvas类中提供了绘制文字的方法。
text:要绘制的文本。--继承自TextView,通过getText().toString()获取。
x:绘制文本水平方向的开始位置。 -- 可以从0开始,
y:文字的基线baseline。
paint:画笔(颜色、大小)-- 已创建
什么是基线?
来源于四线三格。
基线就是第三条线。
Paint.FontMetrics类说明
Paint.FontMetrics类说明,通过paint.getFontMetrics()获取。
红色框是mPaint.getTextBounds(textStr,0,textStr.length(),rect1),获取文字的Rect。就是图中的Rect r。
蓝色的框是文字真实大小,去除padding的。
通过paint.getFontMetrics()获取的是图中FontMetricInt的值,都是以Baseline为起点获取的值,向上是负值(top),向下是正值(bottom)。
基线的计算方法?
获取基线的方式有多种,此处使用FontMetrics的top、bottom以及getHeight()获取基线。
先获取下图dy的高度,然后使用高度中线(即图中红线的位置 高度/2)+dy获取基线的坐标。
获取dy的方法:float dy = (fontMetrics.bottom-fontMetrics.top)/2-fontMetrics.bottom
基线位置1:baseline = getHeight()/2+dy 注意:此方法文本永远在TextView中居中,即使paddingTop和paddingBottom值不一致的时候。
基线位置2:float baseline = (fontMetrics.bottom-fontMetrics.top)/2+dy+getPaddingTop(); 此方法考虑了paddingTop和paddingBottom值不一致的时候
裁剪画布-如何绘制部分文字
那么如何将文字分为两部分绘制,且使用不同的画笔。
裁剪画布,在onDraw(Canvas canvas)中是的画布大小是View所能使用的整个区域,要分左右两部分绘制,那就通过裁剪画布的方式实现,将画布裁剪为左右两部分,分别使用不同的画笔绘制。
- 裁剪画布前先创建画布副本 canvas.save(); 这样画布可以恢复。
- 创建画布副本后,绘制的内容,在画布清空后,已绘制的内容仍然生效。
- 画布可以分层
- 超出画布大小的部分,即使绘制,也不显示。
- 清空画布 canvas.restore();
//两种颜色各绘制一半
int point = (int) (getWidth()*0.5f);
drawText(canvas, mChangePaint, 0, point);
drawText(canvas, mOriginalPaint, point, getWidth());
/**
* 绘制文本
* 创建画布副本
* 裁剪画布
* 绘制文本 只能绘制画布大小,超出的部分不绘制,画布清空后已绘制的内容仍存在
* 清空画布 恢复画布大小/清空画布内容
*/
private void drawText(Canvas canvas, Paint paint,int start,int end) {
//创建画布副本
canvas.save();
//将画布裁剪为Rect大小
Rect rect = new Rect(start,0,end,getHeight());
canvas.clipRect(rect);
String text = getText().toString();
//判空
if(TextUtils.isEmpty(text)) return;
//绘制文本 计算baseline
float dy = 0;
Paint.FontMetrics fontMetrics = paint.getFontMetrics();
dy = (fontMetrics.bottom-fontMetrics.top)/2-fontMetrics.bottom;
float baseline = getHeight()/2+dy;
canvas.drawText(text,0,baseline,paint);
//将画布清空(内容和大小)
canvas.restore();
}
设置颜色占比
需将两种颜色绘制文字的占比,能自定义,范围是0~1,动态设置占比。、
注意:在设置后,需要刷新重绘,否则不生效。
设置颜色变化方向
为了实现从左到右变化,和从右到左变化,需要设置方向。
动画实现动效
使用动画,实现颜色占比0~1的变化。
源码
package com.learn.customui.custom;
import android.animation.ValueAnimator;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.view.animation.LinearInterpolator;
import android.widget.TextView;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.AppCompatTextView;
import com.learn.customui.R;
public class ColorChangeTextView extends AppCompatTextView {
//原始颜色画笔
private Paint mOriginalPaint;
//结束颜色画笔
private Paint mChangePaint;
//颜色占比
private float mCurrentProgress = 0f;
//方向
private Direction mDirection = Direction.LEFT_TO_RIGHT;
public enum Direction{
LEFT_TO_RIGHT,RIGHT_TO_LEFT
}
public ColorChangeTextView(Context context) {
this(context,null);
}
public ColorChangeTextView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs,0);
}
public ColorChangeTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context,attrs);
}
//初始化View 获取属性值
private void init(Context context,AttributeSet attrs) {
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.ColorChangeTextView);
int originalColor = typedArray.getColor(R.styleable.ColorChangeTextView_originalColor, Color.BLACK);
int changeColor = typedArray.getColor(R.styleable.ColorChangeTextView_changeColor,Color.BLACK);
mOriginalPaint = getPaint(originalColor);
mChangePaint = getPaint(changeColor);
//一定要回收?为什么?
typedArray.recycle();
}
//创建Paint 颜色、大小
private Paint getPaint(int color){
Paint paint = new Paint();
paint.setColor(color);
paint.setTextSize(getTextSize());
return paint;
}
@Override
protected void onDraw(Canvas canvas) {
int point = (int) (getWidth()*mCurrentProgress);
if(mDirection == Direction.LEFT_TO_RIGHT) {
drawText(canvas, mChangePaint, 0, point);
drawText(canvas, mOriginalPaint, point, getWidth());
}else if(mDirection == Direction.RIGHT_TO_LEFT){
drawText(canvas, mOriginalPaint, 0, getWidth()-point);
drawText(canvas, mChangePaint, getWidth()-point, getWidth());
}
}
/**
* 绘制文本
* 创建画布副本
* 裁剪画布
* 绘制文本 只能绘制画布大小,超出的部分不绘制,画布清空后已绘制的内容仍存在
* 清空画布 恢复画布大小/清空画布内容
*/
private void drawText(Canvas canvas, Paint paint,int start,int end) {
//创建画布副本
canvas.save();
//将画布裁剪为Rect大小
Rect rect = new Rect(start,0,end,getHeight());
canvas.clipRect(rect);
String text = getText().toString();
//判空
if(TextUtils.isEmpty(text)) return;
//绘制文本 计算baseline
float dy = 0;
Paint.FontMetrics fontMetrics = paint.getFontMetrics();
dy = (fontMetrics.bottom-fontMetrics.top)/2-fontMetrics.bottom;
float baseline = getHeight()/2+dy;
canvas.drawText(text,0,baseline,paint);
//将画布清空(内容和大小)
canvas.restore();
}
//设置颜色改变点
public void setCurrentProgress(float mCurrentProgress) {
this.mCurrentProgress = mCurrentProgress;
//刷新 重绘 否则动画不生效
invalidate();
}
//设置颜色改变方向
public void setDirection(Direction mDirection) {
this.mDirection = mDirection;
}
//设置动画 实现动态改变文字颜色的过程
public void setAnimator(ColorChangeTextView.Direction direction){
setDirection(direction);
ValueAnimator valueAnimator = ValueAnimator.ofFloat(0,1);//值变化范围
valueAnimator.setDuration(2000);//时长2s
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float currentProgress = (float)animation.getAnimatedValue();
setCurrentProgress(currentProgress);
}
});
valueAnimator.start();
}
}
package com.learn.customui;
import androidx.appcompat.app.AppCompatActivity;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.animation.LinearInterpolator;
import android.widget.TextView;
import com.learn.customui.custom.ColorChangeTextView;
public class MainActivity extends AppCompatActivity {
ColorChangeTextView tv;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
tv = (ColorChangeTextView) findViewById(R.id.tv);
}
public void leftToRight(View view) {
tv.setAnimator(ColorChangeTextView.Direction.LEFT_TO_RIGHT);
}
public void rightToLeft(View view) {
tv.setAnimator(ColorChangeTextView.Direction.RIGHT_TO_LEFT);
}
}
<?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"
android:orientation="vertical"
tools:context=".MainActivity">
<com.learn.customui.custom.ColorChangeTextView
android:id="@+id/tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:background="@color/red"
android:paddingTop="10dp"
android:paddingBottom="5dp"
android:text="中华人民共和国"
app:originalColor="@color/black"
app:changeColor="@color/red" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="左到右"
android:onClick="leftToRight"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="右到左"
android:onClick="rightToLeft"/>
</LinearLayout>
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="ColorChangeTextView">
<attr name="originalColor" format="color"/>
<attr name="changeColor" format="color"/>
</declare-styleable>
</resources>