自定义view之歌词渐变文本控件
前言
本文是自定义view的练习,默认读者掌握了自定义view的知识
本文的参考文章
http://blog.youkuaiyun.com/lmj623565791/article/details/44098729
使用方法:
https://github.com/CCY0122/lyrictextview
效果
实现
第一步吧我们要对外提供属性,让用户自由设置,这个一般是自定义view的老套路。在res下的values下新建的attrs.xml里定义好可以自定义的属性:
<declare-styleable name="LyricTextView">
<attr name="text" format="string" />
<attr name="text_size" format="dimension" />
<attr name="default_color" format="color|reference" />
<attr name="changed_color" format="color|reference" />
<attr name="progress" format="float" />
<attr name="direction">
<enum name="left" value="0" />
<enum name="right" value="1" />
</attr>
</declare-styleable>
以上属性分别是文本、文本大小、默认颜色、渐变颜色、渐变百分比、渐变方向。不多说啦。然后新建LyricTextView继承view,重写前三个构造,在构造里获取属性:
public LyricTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray t = context.obtainStyledAttributes(attrs, R.styleable.LyricTextView);
text = t.getString(R.styleable.LyricTextView_text);
if(text == null){text = "";}
textSize = t.getDimension(R.styleable.LyricTextView_text_size, sp2px(16));
defaultColor = t.getColor(R.styleable.LyricTextView_default_color, DEFAULT_COLOR);
changeColor = t.getColor(R.styleable.LyricTextView_changed_color, CHANGED_COLOR);
direction = t.getInt(R.styleable.LyricTextView_direction, LEFT);
progress = t.getFloat(R.styleable.LyricTextView_progress,0);
t.recycle();
initPaint();
measureText();
}
可以看到首先是获取了xml里自定义好的属性,设置给对应变量。然后这个构造方法里还调用了两个方法initPaint(); measureText();
一个是用来初始化画笔的,一个是用来测量文本宽高的。代码如下。
private void initPaint() {
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setTextSize(textSize); //必须在measureText前设置
}
/**
* 测量内容文本的宽高,当改变textSize时应重新调用此方法
*/
private void measureText() {
Rect r = new Rect();
paint.getTextBounds(text, 0, text.length(), r);//一个坑:rect.width(或者说rect.right-rect.left)得到的值会比实际字长度小一点点,因此这里使用paint.measureText方法获取宽度
textHeight = r.bottom - r.top;
textWidth = (int) paint.measureText(text, 0, text.length());
}
在初始化画笔时,首先要给paint设置好字体大小,因为后面measureText
里调用的方法都是在文字大小设置好的前提下获取的才是正确的值。
paint.getTextBounds
是测量文字的宽高值然后放入rect里,但是实测发现一个坑:宽度会比实际宽度小一点,这会导致后面颜色渐变到100%时还有一丢丢字体没变色。因此获取文字宽度使用了paint.measureText
方法。
接下来是onMeasure方法。先看代码。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
width = measure(widthMeasureSpec, true);
height = measure(heightMeasureSpec, false);
setMeasuredDimension(width, height);
}
private int measure(int measureSpec, boolean isWidth) {
int mode = MeasureSpec.getMode(measureSpec);
int size = MeasureSpec.getSize(measureSpec);
switch (mode) {
case MeasureSpec.EXACTLY:
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.UNSPECIFIED:
if (isWidth) {
size = textWidth;
} else {
size = textHeight;
}
break;
}
return isWidth ?
(size + getPaddingLeft() + getPaddingRight())
: (size + getPaddingTop() + getPaddingBottom());
}
如果设定了确定值(EXACTLY),那就设置之,如果没设,则控件大小就是文本的大小。
好了,核心部分onDraw来了.
本控件其实知识点就一个,怎么实现字体渐变?
答:画布canvas有一个方法canvas.clipRect()
,调用了这个方法后接下来只会在这个区域内画内容,超出这个区域的内容就不画了。那么对于我们歌词渐变,我们先用默认颜色画出全部文本,然后呢,根据变量progress(渐变比例,范围[0,1])和方向direction(确定从左到右渐变还是从右到左)计算出要变色的区域,然后用渐变颜色再画一次文本即可。
先放上代码。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
drawText(canvas, direction, progress);
}
private void drawText(Canvas canvas, int direction, float progress) {
int startX;
int endX;
int realWidth = (getMeasuredWidth() - getPaddingLeft() - getPaddingRight());
int realHeight = (getMeasuredHeight() - getPaddingTop() - getPaddingBottom());
int textLeft = getPaddingLeft() + realWidth / 2 - textWidth / 2; //文本在控件中的起始x位置
int textRight = getPaddingLeft() + realWidth / 2 + textWidth / 2; // 文本在控件中的结束x位置
int textBottom = getPaddingTop() + realHeight / 2 + textHeight / 2; //文本在控件中的结束y位置
if(progress < 0 ){progress = 0;}
if(progress > 1 ){progress = 1;}
int changedWidth = (int) (textWidth * progress);
if (direction == LEFT) {
startX = textLeft;
endX = textLeft + changedWidth;
} else {
startX = textRight - changedWidth;
endX = textRight;
}
//画正常的文字内容
paint.setTextSize(textSize);
Paint.FontMetrics fontMetrics = paint.getFontMetrics();
canvas.save();
paint.setColor(defaultColor);
canvas.drawText(text, textLeft, textBottom, paint);
canvas.restore();
//画渐变部分的文字
canvas.save(Canvas.CLIP_SAVE_FLAG);
paint.setColor(changeColor);
canvas.clipRect(startX, 0, endX, getMeasuredHeight());
canvas.drawText(text, textLeft, textBottom, paint);
canvas.restore();
}
哇,贼简单。一些位置的计算,细心读一下应该能懂。如果你没理解,那可以去看看开头的参考文章,咱们的鸿洋大大写的。
小优化
我们先直接看看目前的效果吧:
<com.example.lyrictextview.LyricTextView
android:id="@+id/lyric"
android:layout_width="match_parent"
android:layout_height="60dp"
android:background="#44000000"
app:changed_color="#ff0000"
app:default_color="#000000"
app:direction="left"
app:progress="0.7"
app:text="按时大大的飒飒的"
app:text_size="26sp" />
破费特,效果可以
那我们在用wrap_content看看。
<com.example.lyrictextview.LyricTextView
android:id="@+id/lyric"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#44000000"
app:changed_color="#ff0000"
app:default_color="#000000"
app:direction="left"
app:progress="0.7"
app:text="按时大大的飒飒的"
app:text_size="26sp" />
好了,问题来了,文字下面有一小部分没显示呀。
对于这个问题,看图(图片来自他人博客):
上图是android中文本绘制的各种线,我们平时绘制一个文本时呢,就是从baseLine最左端开始绘制的(上图中红点)。关于这个文本绘制的细节,已经有很多完整分析的文章了。
看上面这张图,字母g、j、p都是会超出baseLine的,这也就是为什么当我们宽高为wrap_content时文字下面一小部分不显示的原因了。
解决方法1:如果你不需要非常精细,直接给控件设置个默认padding就可以,就几个dp差不多就能把底部显示出来。
解决方法2:
1)修改measureText
:
private void measureText() {
Rect r = new Rect();
paint.getTextBounds(text, 0, text.length(), r);//一个坑:rect.width会比实际字长度小一点点
// textHeight = r.bottom - r.top;
Paint.FontMetrics fontMetrics = paint.getFontMetrics();
textHeight = (int) (-fontMetrics.ascent + fontMetrics.descent);
textWidth = (int) paint.measureText(text, 0, text.length());
}
Paint.FontMetric里包含了文本绘制的各种线。因为绘制基线是baseLine,基线为0,坐标轴向下为正,故在其之上的是负数。
2)修改onDraw绘制部分:
//画正常的文字内容
paint.setTextSize(textSize);
Paint.FontMetrics fontMetrics = paint.getFontMetrics();
canvas.save();
paint.setColor(defaultColor);
// canvas.drawText(text, textLeft, textBottom, paint);
canvas.drawText(text, textLeft, textBottom - fontMetrics.descent, paint);
canvas.restore();
//画渐变部分的文字
canvas.save(Canvas.CLIP_SAVE_FLAG);
paint.setColor(changeColor);
canvas.clipRect(startX, 0, endX, getMeasuredHeight());
// canvas.drawText(text, textLeft, textBottom , paint);
canvas.drawText(text, textLeft, textBottom - fontMetrics.descent, paint);
canvas.restore();
好了,就是绘制文字时向上偏移一定距离,即fontMetrics.descent
这样,当控件宽高为wrap_content时文字也能显示完整啦
最后再贴上对外公开的setter、getter方法:
/以下settre getter供外部设置属性,别忘记invalidate();
//ps:若要使用属性动画控制progress,前提得有progress的setter getter
public void setProgress(float progress) {
this.progress = progress;
invalidate();
}
public float getProgress() {
return progress;
}
public void setTextSize(float size) {
textSize = size;
initPaint();
measureText();
requestLayout();//wrap_content情况下文字大小改变后需重新onMeausre
invalidate();
}
public float getTextSize() {
return textSize;
}
public int getDirection() {
return direction;
}
public void setDirection(int direction) {
this.direction = direction;
invalidate();
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
requestLayout(); //wrap_content情况下文字长度改变后需重新onMeausre
invalidate();
}
public int getDefaultColor() {
return defaultColor;
}
public void setDefaultColor(int defaultColor) {
this.defaultColor = defaultColor;
invalidate();
}
public void setAll(float progress, String text, float textSize, int defaultColor, int changeColor, int direction) {
this.progress = progress;
this.text = text;
this.textSize = textSize;
this.defaultColor = defaultColor;
this.changeColor = changeColor;
this.direction = direction == LEFT ? LEFT : RIGHT;
initPaint();
measureText();
requestLayout();
invalidate();
}
public int getChangeColor() {
return changeColor;
}
public void setChangeColor(int changeColor) {
this.changeColor = changeColor;
invalidate();
}
//工具
private float dp2px(int dp) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics());
}
private float sp2px(int sp) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, getResources().getDisplayMetrics());
}
这是一个简单的小控件,写它主要是打算写一个仿今日头条指示器的控件~过些天应该会写吧~~~
源码https://github.com/CCY0122/lyrictextview