效果如图所示:
可见要实现次效果,需要自定义View继承TextView(因为有文字,继承这个会好一些)。
外圈是一个橙色的背景,形状为圆角矩形,以及底部有一个小直角三角形倒过来的形状。里面是变色文字。这里我们先实现外圈的橙色背景。
绿色背景:
- 在res文件夹中的values,新建一个文件attrs.xml, 这个是对于自定义view的声明文件。
- 属性包括,圆角矩形的圆角半径长度、小直角三角形对角线一半的长度,以及颜色、圆角矩形高度、标题栏滑动方向。
代码如下:
<declare-styleable name="LoryMessageTitle">
<attr name="roudedRadius" format="dimension"></attr><!--矩形圆角半径径-->
<attr name="triangleLengthHalf" format="dimension"></attr><!--直角三角形对角线长度一半-->
<attr name="rectHeight" format="dimension"></attr><!--圆角矩形高度-->
<attr name="colorAll" format="color"></attr><!--控件颜色-->
<attr name="changeDirection"> <!--控件变化方向-->
<enum name="LEFT_TO_RIGHT" value="1"></enum>
<enum name="RIGHT_TO_LEFT" value="2"></enum>
</attr>
</declare-styleable>
3.自定义view名 LoryMessageTitle继承Textview,代码如下:
public class LoryMessageTitle extends androidx.appcompat.widget.AppCompatTextView {
//背景图案
private float progress = 1f;//移动比例参数progress,默认是0
private int mRoundedRadius = 3;//圆角矩形直径默认长度
private int mTraigleLength = 4;//直角三角形对角线边默认一半长度
private int mRectHeight = 20;//圆角矩形默认高度
private int mRectWidth = 20;//圆角矩形默认长度
private int mColor = getResources().getColor(R.color.colorGreener,null);//控件默认颜色
private int mDirection = 1; //默认变化从左到右
private Paint mPaint;//控件绘制的画笔,由于控件背景色颜色统一,所以用一支画笔就可以了
public LoryMessageTitle(Context context){
this(context,null);
}
public LoryMessageTitle(Context context, AttributeSet attrs){
this(context,attrs,0);
}
public LoryMessageTitle(Context context,AttributeSet attrs,int delStyleAttr){
super(context,attrs,delStyleAttr);
//初始化
init(context,attrs);
}
//初始化函数
public void init(Context context,AttributeSet attrs){
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.LoryMessageTitle);
mRoundedRadius = array.getDimensionPixelSize(R.styleable.LoryMessageTitle_roudedRadius,sp2px(mRoundedRadius));//圆角半径大小
mTraigleLength = array.getDimensionPixelSize(R.styleable.LoryMessageTitle_triangleLengthHalf,sp2px(mTraigleLength));//三角形对角线长度一半
mRectHeight = array.getDimensionPixelSize(R.styleable.LoryMessageTitle_rectHeight,sp2px(mRectHeight));//圆角矩形高度
mColor = array.getColor(R.styleable.LoryMessageTitle_colorAll,mColor);//控件颜色
mDirection = array.getInteger(R.styleable.LoryMessageTitle_changeDirection,mDirection);//控件移动方向
mPaint = new Paint();
mPaint.setAntiAlias(true);//抗锯齿
mPaint.setDither(true);
mPaint.setColor(mColor);
mDirection = array.getInteger(R.styleable.LoryMessageTitle_changeDirection,mDirection);
}
//sp to px 不多说,就是 sp转化为 px,要是控件使用者用dp怎么办,那就让他自己添加个dp转sp的函数,就你事多
public int sp2px(int sp){
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, this.getResources().getDisplayMetrics());
}
//测量方法
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthmode = MeasureSpec.getMode(widthMeasureSpec);//宽度模式
int heightmode = MeasureSpec.getMode(heightMeasureSpec);//高度模式
int widthsize = MeasureSpec.getSize(widthMeasureSpec);//宽度大小
int heightsize = MeasureSpec.getSize(heightMeasureSpec);//高度大小
//要是用户填的wrap_content,宽度、高度至少时
if (widthmode==MeasureSpec.AT_MOST){
int widthsize = sp2px(mRectWidth)+getPaddingLeft()+getPaddingRight();//背景宽度
}
if (heightmode==MeasureSpec.AT_MOST){
int heightsize = mRectHeight+mTraigleLength+getPaddingTop()+getPaddingBottom();
}
setMeasuredDimension(widthsize,heightsize);
}
//绘制方法,一开始绘制一个静态的控件,然后这个控件会移动,有个0-1的参数,决定这个控件移动的比例,引入这个参数叫 progress。默认是0;
@Override
protected void onDraw(Canvas canvas) {
//静态控件如下
// //首先绘制静态的圆角矩形
// { canvas.save();
// RectF rectf = new RectF(getPaddingLeft(), getPaddingTop(), getWidth() - getPaddingRight(), getHeight() - getPaddingBottom() - mTraigleLength);
// canvas.drawRoundRect(rectf, mRoundedRadius, mRoundedRadius, mPaint);
// canvas.restore();
// //然后绘制三角形,是一个倒过来的直角三角形,可以用一个倒过来的正方形代替,对角线和矩形重合即可,也可以通过路径绘制
// canvas.save();
// Path path = new Path();
// path.moveTo(getWidth() / 2 - mTraigleLength, getHeight() - getPaddingBottom() - mTraigleLength);//起点
// path.lineTo(getWidth() / 2, getHeight() - getPaddingBottom());//往下移动
// path.lineTo(getWidth() / 2 + mTraigleLength, getHeight() - getPaddingBottom() - mTraigleLength);//往右移动
// path.close();//路径闭合
// canvas.drawPath(path, mPaint);
// canvas.restore();
// }
//颜色更改后,画笔也要及时更新哈
mPaint.setColor(mColor);
//绘制动态圆角矩形,随着progress运动,圆角矩形的左边x参数在增加,右边参数也在增加,由于控件长度固定,
//所以可以出现圆角矩形右侧被吞噬的样子
float x1;//圆角矩形左侧坐标
float x2;//圆角矩形右侧坐标
Rect bounds = new Rect();
if (mDirection==2){
//如果移动方向是从右到左
x1 = (1-progress)*(getWidth())+getPaddingLeft();
x2 = (2-progress)*(getWidth())-getPaddingRight();
}else {
//移动方向为从左到右
x1 = (progress-1)*(getWidth())+getPaddingLeft();
x2 = progress*(getWidth())-getPaddingRight();
}
canvas.save();
RectF rectf =new RectF(x1,getPaddingTop(),
x2,getHeight()-getPaddingBottom()-mTraigleLength);//矩形
canvas.drawRoundRect(rectf,mRoundedRadius,mRoundedRadius,mPaint);//绘制圆角矩形
//然后绘制三角形,是一个倒过来的直角三角形,可以用一个旋转的正方形代替,对角线和矩形重合即可,也可以通过路径绘制
//我个人更喜欢路径Path,关于Path的方法百度上有
canvas.save();
Path path = new Path();
path.moveTo(getWidth()/2-mTraigleLength+x1-getPaddingLeft(),getHeight()-getPaddingBottom()-mTraigleLength);//起点,三角形左边起点
path.lineTo(getWidth()/2+x1-getPaddingLeft(),getHeight()-getPaddingBottom());//往下移动
path.lineTo(getWidth()/2+mTraigleLength+x1-getPaddingLeft(),getHeight()-getPaddingBottom()-mTraigleLength);//往右移动,三角形右边的点
path.close();//路径闭合,将绘制的三个点闭合
canvas.drawPath(path,mPaint);//绘制三角形
canvas.restore();
}
// 最后就是这个控件随着progress变化而变化,每次变化都要重新绘制,
//invalidate方法会再次调用ondraw方法
public void changeProgress(float progresser){
//设置改变进度的方法
progress = progresser;
invalidate();//重新绘制
}
// 也有人说,我不想在xml文件中设置控件状态,我想java文件中改变控件的圆角矩形高度、圆角大小,小三角形对角线宽一半,颜色等
//你丫的需求真多呀,就你多事
public void setSate(int roundedRadius,int roundedheight,int traigleLengthHalf,int color){
//改变控件状态
mRoundedRadius = roundedRadius;
mRectHeight = roundedheight;
mTraigleLength = traigleLengthHalf;
mColor = color;
invalidate();
}
public void setdirction(int c_direction){
//设置控件变化方向
mDirection = c_direction;
invalidate();
}
public void setmColor(int color){
//设置控件颜色
mColor = color;
invalidate();
}
}
好了这个控件定义完了,那么效果如何?就把它当作textview就测试一下,要是能够绘制出这个效果就说明可用了。
接下来就是字体的绘制,我们再造一个自定view,也是继承TextView.
可变色字体的绘制:
两种颜色的字体,而且可以随着progress变化而变化,那么如何实现呢?
很简单,只要将画布Canvas分割,裁成两个区域,一个是灰色,一个是白色,然后区域会随着progress变化而变化。
- 在attrs.xml文件中声明一个字体的自定义view
<declare-styleable name="ChangeColorText">
<attr name="changeColor" format="color"></attr><!--文字改变后的颜色-->
<attr name="orignColor" format="color"></attr><!--文字原颜色-->
<attr name="changeDirection"> <!--控件变化方向-->
<enum name="LEFT_TO_RIGHT" value="1"></enum>
<enum name="RIGHT_TO_LEFT" value="2"></enum>
</attr>
</declare-styleable>
- 自定义view, ChangeColorText继承TextView:
public class ChangeColorText extends androidx.appcompat.widget.AppCompatTextView {
private float progress = 1f;//移动比例参数progress,默认是0
//字体
private int mOrignColor = getResources().getColor(R.color.colorChange,null);//默认字体原始颜色
private int mChangeColor = Color.WHITE;//默认字体变化颜色
private Paint mOrignPaint,mChangePaint;//绘制两种字体的画笔
private int textsize = 50;//默认原字体大小px
private int textsize1 = 55;//默认字体变化的大小
private int mDirection = 1; //默认变化从左到右
public ChangeColorText(Context context){
this(context,null);
}
public ChangeColorText(Context context, AttributeSet attrs){
this(context,attrs,0);
}
public ChangeColorText(Context context,AttributeSet attrs,int delStyleAttr){
super(context,attrs,delStyleAttr);
//初始化
init(context,attrs);
}
//初始化函数
public void init(Context context,AttributeSet attrs){
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.LoryMessageTitle);
//字体
mOrignColor = array.getColor(R.styleable.LoryMessageTitle_orignColor,mOrignColor);//原始颜色
mChangeColor = array.getColor(R.styleable.LoryMessageTitle_changeColor,mChangeColor);//变化颜色
mDirection = array.getInteger(R.styleable.LoryMessageTitle_changeDirection,mDirection);//变化方向
//orignPaint画笔初始化
mOrignPaint = new Paint();
mOrignPaint.setColor(mOrignColor);
mOrignPaint.setAntiAlias(true);
mOrignPaint.setDither(true);
mOrignPaint.setTextSize(textsize);
//changePaint画笔初始化
mChangePaint = new Paint();
mChangePaint.setColor(mChangeColor);
mChangePaint.setAntiAlias(true);
mChangePaint.setDither(true);
mChangePaint.setTextSize(textsize1);
}
//sp to px 不多说,就是 sp转化为 px,要是控件使用者用dp怎么办,那就让他自己添加个dp转sp的函数,就你事多
public int sp2px(int sp){
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, this.getResources().getDisplayMetrics());
}
//测量方法
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthmode = MeasureSpec.getMode(widthMeasureSpec);
int heightmode = MeasureSpec.getMode(heightMeasureSpec);
int widthsize = MeasureSpec.getSize(widthMeasureSpec);
int heightsize = MeasureSpec.getSize(heightMeasureSpec);
//文字的长宽,用画笔测量文字的宽度,长度
Rect bounds = new Rect();
mChangePaint.getTextBounds(getText().toString(),0,getText().length(),bounds);
//要是用户填的wrap_content,宽度、高度至少
if (widthmode==MeasureSpec.AT_MOST){
int widthsize = bounds.width() + getPaddingLeft()+getPaddingRight();//文字宽度
}
if (heightmode==MeasureSpec.AT_MOST){
int heightsize = bounds.height() + +getPaddingTop()+getPaddingBottom();
}
setMeasuredDimension(widthsize,heightsize);
}
@Override
protected void onDraw(Canvas canvas) {
//文字的横坐标
int x = getWidth()/2-bounds.width()/2;
Paint.FontMetricsInt fontMetricsInt = mOrignPaint.getFontMetricsInt();
int dy = (fontMetricsInt.bottom - fontMetricsInt.top)/2 - fontMetricsInt.bottom;
int baseline = getHeight()/2+dy;//基线
//在绘制变色文字
if(mDirection==1){
//从左到右
canvas.save();
canvas.clipRect(0,0,progress*getWidth()-getPaddingLeft(),getHeight());//裁剪坐左边的区域
//绘制左边文字
canvas.drawText(getText().toString(),x,baseline,mChangePaint);
canvas.restore();
canvas.save();
canvas.clipRect(progress*getWidth()-getPaddingLeft(),0,getWidth(),getHeight());//裁剪右边的区域
canvas.drawText(getText().toString(),x,baseline,mOrignPaint);
canvas.restore();//绘制右边的文字
}else{
//从右到左
canvas.save();
canvas.clipRect(0,0,(1-progress)*getWidth()+getPaddingRight(),getHeight());
canvas.drawText(getText().toString(),x,baseline,mOrignPaint);
canvas.restore();
canvas.save();
canvas.clipRect((1-progress)*getWidth()+getPaddingRight(),0,getWidth(),getHeight());
canvas.drawText(getText().toString(),x,baseline,mChangePaint);
canvas.restore();
}
}
// 最后就是这个控件随progress变化,而重新绘制。
public void changeProgress(float progresser){
progress = progresser;
invalidate();//重新绘制
}
public void setmOrignColor(int c_color){
//原文字颜色
this.mOrignColor = c_color;
invalidate();
}
public void setmChangeColor(int c_color){
//变化文字颜色
this.mChangeColor = c_color;
invalidate();
}
//设置绘制时字体 大小 粗细于一体
public void setmWordSize(int orignSize,int changSize){
this.textsize = orignSize;
this.textsize1 = changSize;
invalidate();
}
public void setdirction(int c_direction){
mDirection = c_direction;
invalidate();
}
}
文字也绘制出来了,那么如何将两种效果放在一个控件中呢?简单呀,将两种控件的属性放到一个控件就可以了。这个工作就交给读者吧,我想你通过这两个自定义view的绘制后,应该不难。当然,期间你肯定也会有一些和我当初学的时候的坑。
大坑汇集:
- 标题1在过度标题2 的过程中, 标题1的颜色由白色逐渐变为灰色,标题2的颜色由灰色逐渐变为白色,但是这两个标题的OriginColor和ChangeColor代码中都写成了一样的,导致标题1的颜色由白色逐渐变为灰色,标题2的颜色由白色逐渐变为灰色,这样并没有文字变色移动的效果,理论上 标题1的OriginColor本应该为白色,而标题2的OriginColor应该为灰色,如何解决这个冲突呢?
答:OriginColor为灰色,ChangeColor为白色。 标题1的移动方向为“从右往左”,而标题2的移动方向设置为“从左到右”。因为代码中,direction的变化会让原色和变色转换。从左边往右边,标题1是由白色变灰色,而从右边往左边,标题1是由灰色变白色。并且, 标题1的进度=1-progress,标题2的进度为 progress.
- 可变颜色字体和橙色背景结合时,控件的宽高测量冲突怎么解决?
答: 可变颜色字体的宽高测量是通过画笔测量单个文字后给出的Bounds决定的,而橙色背景的宽高是人为给定的。要让文字和背景的放置比例位置合理,是一个头疼的问题。作者的方法是基于文字的宽高,再加上固定的长度。至于固定的长度是多长,这个需要读者自己测试把握。作者并没有好的办法。
- 标题1移动到标题2过程中,颜色变化和橙色背景移动都是由参数progress决定的,那这个progress的数据来源从哪来呢?换句话说,我怎样可以用手滑动屏幕时,progress随滑动比例而变化呢?
答: 可滑动标题栏下方的内容是一个ViewPager, viewPager可以适应多个适配器Adapter,至于用哪个Adapter看你个人习惯,不过由于ViewPager内放的是Fragment,所以我建议用FragmentPageAdapter(名字有没有打错哈)。ViewPager有3个关于滑动方法:
看到第一个方法,那个postionOffset,这个就是ViewPager监听滑动比例,范围由(0—1),progress的数据来源就是它。
以上就是我遇到的坑,其他问题要是读者遇到了,那就评论区留言吧