版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/zgcqflqinhao/article/details/77029150
在 Android 中我觉得除了实现很多功能性很强的需求之外,最吸引我的就是各种炫酷的自定义控件,但是自定义控件这个东西没有办法用一种固定的模式来讲解,因为自定义控件都是根据需求来定制的。同时这也说明只要程序猿牛逼,就没有实现不了的功能。
之前有看到一个效果:
刚开始看到这个我也是一头雾水,后来接触了 Paint 类、 Canvas 类和属性动画后,对这个动画的实现也有了一些自己的思路。虽然上面的博文中博主也一步一步教了如何实现,但是我还是当时看不懂,在能看懂的时候还是决定自己来实现一下这个控件。
这个控件大致思路分为三步,先是圆角矩形,然后文字慢慢消失并收缩成圆,最后圆升高并且在园中出现对勾,有这个基本思路后就可以分步来实现这个控件了。
1 准备工作
这一步不是重点,不过这一步中获取自定义属性和宽高的设置算是自定义控件的一个知识点,是一个扩展,详细的讲解可以看看洋神的博客:
在构造方法中设置画笔的各个属性:
//获取自定义属性
TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.SubmitButton, defStyleAttr, 0);
int background = typedArray.getColor(R.styleable.SubmitButton_sb_background, Color.rgb(210, 105, 30));
sbText = typedArray.getString(R.styleable.SubmitButton_sb_text);
int sbTextColor = typedArray.getColor(R.styleable.SubmitButton_sb_textColor, Color.rgb(255, 255, 255));
sbTextSize = typedArray.getDimensionPixelSize(R.styleable.SubmitButton_sb_textSize, (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_SP, 18, getResources().getDisplayMetrics()));
mPaint = new Paint();
mPaint.setColor(background);
mPaint.setAntiAlias(true);
textPaint = new Paint();
textPaint.setColor(sbTextColor);
textPaint.setTextSize(sbTextSize);
mBound = new Rect();
textPaint.getTextBounds(sbText, 0, sbText.length(), mBound);
okPaint = new Paint();
okPaint.setColor(sbTextColor);
okPaint.setStrokeWidth(10);
okPaint.setStrokeCap(Paint.Cap.ROUND);
okPaint.setStrokeJoin(Paint.Join.ROUND);
okPaint.setStyle(Paint.Style.STROKE);
重写 onMeasure() 方法获得控件的宽高:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int width;
int height;
setPadding(getPaddingLeft() + 60, getPaddingTop(), getPaddingRight() + 60, getPaddingBottom());
if (widthMode == MeasureSpec.EXACTLY) {
width = widthSize;
} else {
textPaint.getTextBounds(sbText, 0, sbText.length(), mBound);
float textWidth = mBound.width();
width = (int) (getPaddingLeft() + textWidth + getPaddingRight());
}
if (heightMode == MeasureSpec.EXACTLY) {
height = heightSize;
} else {
textPaint.getTextBounds(sbText, 0, sbText.length(), mBound);
float textHeight = mBound.height();
height = (int) (getPaddingTop() + textHeight + getPaddingBottom());
}
rectWidth = width;
rectHeight = height;
setMeasuredDimension(width, height);
}
2 圆角矩形
画圆角矩形的话是很简单,是 Paint 和 Canvas 的基本使用就可以轻松实现。我之前有一篇博客也记录了这两个类的基本使用:
根据上面获取的宽高在 onDraw() 方法中画一个圆角矩形:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
RectF rectF = new RectF(0, 0, rectWidth, rectHeight);
canvas.drawRoundRect(rectF, 45, 45, mPaint);
canvas.drawText(sbText, getWidth() / 2 - mBound.width() / 2, getHeight() / 2 + mBound.height() / 2, textPaint);
}
效果如下:
3 收缩成圆
从圆角矩形变成圆,我们需要设计一个动画,至于这个动画应该怎么做,我们首先来看一张图:
这个图应该很能说明问题了,我们可以把圆也看做是圆角矩形,只不过我们需要不断修改它 x 的起止位置,需要计算的就是 x 的起止位置。
x 需要变化的值是 getMeasuredWidth() / 2 - 圆的半径,圆的直径就是 getMeasuredHeight(),所以圆的半径是 getMeasuredHeight() / 2,那么 x 需要变化的值就是 getMeasuredWidth() / 2 - getMeasuredHeight() / 2,即 (getMeasuredWidth() - getMeasuredHeight()) / 2。
定义一个值作为这个控件的属性,用来时刻更新 x 的起止位置:
public int animationValue = 0;
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
RectF rectF = new RectF(animationValue, 0, rectWidth - animationValue, rectHeight);
canvas.drawRoundRect(rectF, 45, 45, mPaint);
canvas.drawText(sbText, getWidth() / 2 - mBound.width() / 2, getHeight() / 2 + mBound.height() / 2, textPaint);
}
public void submit() {
ObjectAnimator animationValueAnimator = ObjectAnimator.ofInt(this, "animationValue", 0, (getMeasuredWidth() - getMeasuredHeight()) / 2);
ObjectAnimator alphaAnimator = ObjectAnimator.ofInt(textPaint, "alpha", 255, 0);
AnimatorSet mAnimatorSet = new AnimatorSet();
mAnimatorSet.play(animationValueAnimator).with(alphaAnimator);
mAnimatorSet.setDuration(1000);
animationValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
invalidate();
}
});
mAnimatorSet.start();
}
我们在构造方法中为这个自定义控件添加一个 OnClickListener,让它点击后开始动画:
setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
submit();
}
});
4 升高并画对勾
升高很简单,只需要改变这个控件在 y 轴 的位置即可,关键在于后面的画对勾,我没有实现原文中的效果,而是直接画出了个勾,要实现慢慢打勾的效果暂时还不会(囧)。画对勾其实可以用画路线来实现,这一步的关键也是如何计算对勾的各个点,还是看一张图:
对勾的位置也可以自己决定,我这里提供了我的思路,首先我们应该知道圆心的位置,这个很简单,圆心在 x 轴的位置为 getMeasuredWidth() / 2,在 y 轴的位置getMeasuredHeight() / 2,然后其他关键点我们可以通过圆心来计算,黑色的两根线为以圆心为原点的 x 轴和 y 轴,垂直的两根红线和 y 轴为圆的四等分线,水平的两根红线为圆的三等分线。那么对勾的三个关键点也就一目了然了,第一个点 x 轴的位置在第一个四等分点,y 轴的位置与圆心相同,第二个点 x 轴的位置与圆心相同,y 轴的位置在第二个三等分点,第三个点 x 轴的位置在第三个四等分点,y 轴的位置在第一个三等分点。可能我描述的不太准确,但是看图应该能看明白,用代码确定位置的话是这样:
Path path = new Path();
path.moveTo(rectWidth / 2 - rectHeight / 2 / 2, rectHeight / 2);
path.lineTo(rectWidth / 2, rectHeight / 3 * 2);
path.lineTo(rectWidth / 2 + rectHeight / 2 / 2, rectHeight / 3);
为了升高这个控件,我们需要在原来的动画集中添加一个动画,即该控件在 y 轴位置的变化动画,这个动画之后再显示对勾,因为我都是在一直重画这个控件,所以在前两步时画的是圆角矩形,最后一步需要多画对勾,所以我定义了一个 boolean 类型的变量来让它决定是画圆角矩形还是对勾。
private boolean drawOK = false;
public void submit() {
ObjectAnimator animationValueAnimator = ObjectAnimator.ofInt(this, "animationValue", 0, (getMeasuredWidth() - getMeasuredHeight()) / 2);
ObjectAnimator yAnimator = ObjectAnimator.ofFloat(SubmitButton.this, "y", getY(), getY() - 200);
ObjectAnimator alphaAnimator = ObjectAnimator.ofInt(textPaint, "alpha", 255, 0);
AnimatorSet mAnimatorSet = new AnimatorSet();
mAnimatorSet.play(animationValueAnimator).with(alphaAnimator).before(yAnimator);
mAnimatorSet.setDuration(1000);
animationValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
invalidate();
}
});
yAnimator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
drawOK = true;
invalidate();
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
mAnimatorSet.start();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (drawOK) {
RectF rectF = new RectF(animationValue, 0, rectWidth - animationValue, rectHeight);
canvas.drawRoundRect(rectF, 90, 90, mPaint);
Path path = new Path();
path.moveTo(rectWidth / 2 - rectHeight / 2 / 2, rectHeight / 2);
path.lineTo(rectWidth / 2, rectHeight / 3 * 2);
path.lineTo(rectWidth / 2 + rectHeight / 2 / 2, rectHeight / 3);
canvas.drawPath(path, okPaint);
} else {
RectF rectF = new RectF(animationValue, 0, rectWidth - animationValue, rectHeight);
canvas.drawRoundRect(rectF, 45, 45, mPaint);
canvas.drawText(sbText, getWidth() / 2 - mBound.width() / 2, getHeight() / 2 + mBound.height() / 2, textPaint);
}
}
5 总结
从事实际开发一年多,其实感觉自己的技术成长得很慢,很多东西自己还是处于模仿的阶段,很希望自己能够创新出一种库,自己能够让技术流行起来。离职一段时间也发现工作中开发和凭兴趣开发真的是有区别的,有任务在身的时候那种紧张感和责任感更强烈,所以实际工作经验真的很重要,希望自己接下来找工作顺利,同时也希望在今后一直坚持写博客记录自己在开发道路上的成长。
6 源码
附上完整源码:
public class SubmitButton extends android.support.v7.widget.AppCompatButton {
private Paint mPaint;
private Paint textPaint;
private Paint okPaint;
private int rectWidth;
private int rectHeight;
public int animationValue = 0;
private String sbText;
private Rect mBound;
private int sbTextSize;
private boolean drawOK = false;
public void setAnimationValue(int animationValue) {
this.animationValue = animationValue;
}
public SubmitButton(Context context) {
this(context, null);
}
public SubmitButton(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public SubmitButton(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//获取自定义属性
TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.SubmitButton, defStyleAttr, 0);
int background = typedArray.getColor(R.styleable.SubmitButton_sb_background, Color.rgb(210, 105, 30));
sbText = typedArray.getString(R.styleable.SubmitButton_sb_text);
int sbTextColor = typedArray.getColor(R.styleable.SubmitButton_sb_textColor, Color.rgb(255, 255, 255));
sbTextSize = typedArray.getDimensionPixelSize(R.styleable.SubmitButton_sb_textSize, (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_SP, 18, getResources().getDisplayMetrics()));
mPaint = new Paint();
mPaint.setColor(background);
mPaint.setAntiAlias(true);
textPaint = new Paint();
textPaint.setColor(sbTextColor);
textPaint.setTextSize(sbTextSize);
mBound = new Rect();
textPaint.getTextBounds(sbText, 0, sbText.length(), mBound);
okPaint = new Paint();
okPaint.setColor(sbTextColor);
okPaint.setStrokeWidth(10);
okPaint.setStrokeCap(Paint.Cap.ROUND);
okPaint.setStrokeJoin(Paint.Join.ROUND);
okPaint.setStyle(Paint.Style.STROKE);
setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
submit();
}
});
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int width;
int height;
setPadding(getPaddingLeft() + 60, getPaddingTop(), getPaddingRight() + 60, getPaddingBottom());
if (widthMode == MeasureSpec.EXACTLY) {
width = widthSize;
} else {
textPaint.getTextBounds(sbText, 0, sbText.length(), mBound);
float textWidth = mBound.width();
width = (int) (getPaddingLeft() + textWidth + getPaddingRight());
}
if (heightMode == MeasureSpec.EXACTLY) {
height = heightSize;
} else {
textPaint.getTextBounds(sbText, 0, sbText.length(), mBound);
float textHeight = mBound.height();
height = (int) (getPaddingTop() + textHeight + getPaddingBottom());
}
rectWidth = width;
rectHeight = height;
setMeasuredDimension(width, height);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (drawOK) {
RectF rectF = new RectF(animationValue, 0, rectWidth - animationValue, rectHeight);
canvas.drawRoundRect(rectF, 90, 90, mPaint);
Path path = new Path();
path.moveTo(rectWidth / 2 - rectHeight / 2 / 2, rectHeight / 2);
path.lineTo(rectWidth / 2, rectHeight / 3 * 2);
path.lineTo(rectWidth / 2 + rectHeight / 2 / 2, rectHeight / 3);
canvas.drawPath(path, okPaint);
} else {
RectF rectF = new RectF(animationValue, 0, rectWidth - animationValue, rectHeight);
canvas.drawRoundRect(rectF, 45, 45, mPaint);
canvas.drawText(sbText, getWidth() / 2 - mBound.width() / 2, getHeight() / 2 + mBound.height() / 2, textPaint);
}
}
public void submit() {
ObjectAnimator animationValueAnimator = ObjectAnimator.ofInt(this, "animationValue", 0, (getMeasuredWidth() - getMeasuredHeight()) / 2);
ObjectAnimator yAnimator = ObjectAnimator.ofFloat(SubmitButton.this, "y", getY(), getY() - 200);
ObjectAnimator alphaAnimator = ObjectAnimator.ofInt(textPaint, "alpha", 255, 0);
AnimatorSet mAnimatorSet = new AnimatorSet();
mAnimatorSet.play(animationValueAnimator).with(alphaAnimator).before(yAnimator);
mAnimatorSet.setDuration(1000);
animationValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
invalidate();
}
});
yAnimator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
drawOK = true;
invalidate();
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
mAnimatorSet.start();
}
}