自定义 View 动画实现

概述

由于人们对生活品质的高要求,现在app使用过程已经不是简单的追求流畅,还需要炫酷的动画来过渡,这里简单实现一个自定义view的动画。先看看最终效果:

demo

实现

自定义控件

首先,新建一个 Java 类,继承 android.view.View,重新三个构造函数:

public class SubmitButtonView extends View {

// 在java文件new的时候使用
public SubmitButtonView(Context context) {
this(context, null);
}

// 在xml文件使用
public SubmitButtonView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}

// 在xml文件使用并且指定了style
public SubmitButtonView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}

}

注意修改构造函数前两个的super改为this,并修改相对应的参数,这样保证初始化在第三个构造函数调用就可以。

布局分析

分析布局可知,背景为长方形,上面覆盖提示文字。

由于绘制有先后区分,所以我们先来绘制背景长方形。

绘制长方形

首先我们需要确定长方形的大小。由于在绘制之前,根据xml布局代码可知当前控件的大小,会调用view的onSizeChanged函数进行计算,因此可以通过当前函数获取长和高。

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mRectF.bottom = h;
mRectF.right = w;
}

然后根据 canvas 的 drawRect 函数绘制到界面即可。

绘制文字

绘制文字主要难点在于获取文字的位置。由于绘制文字的起始点在文字的左下角,所以我们需要特别注意。

画个草图示例:

文字起始点

这里需要解释的是 fontMetricsInt 这个函数是通过文字画笔获取文字高度。

左起点计算比较简单,通过画笔设置绘制位置为居中,在设置时,获取背景的中心点即可。

textPaint.setTextAlign(Paint.Align.CENTER);

canvas.drawText(textShow, mRectF.centerX(), (mRectF.bottom - mRectF.top - fontMetricsInt.bottom - fontMetricsInt.top) / 2, textPaint);

添加监听事件

当前 veiw 可以直接设置点击监听。首先声明监听接口:

public interface OnViewClickListener {
void animStart();

void animEnd();
}

然后设置对应的点击事件:

public void setOnViewClickListener(OnViewClickListener onViewClickListener) {
mOnViewClickListener = onViewClickListener;
}

响应点击事件:

public SubmitButtonView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (mOnViewClickListener != null) {
mOnViewClickListener.animStart();
}
}
});
initPaint();
}

然后在activity中即可使用:

buttonView.setOnViewClickListener(new SubmitButtonView.OnViewClickListener() {
@Override
public void animStart() {
Toast.makeText(MainActivity.this, "点击了控件", Toast.LENGTH_SHORT).show();
}

@Override
public void animEnd() {

}
});

动画模块

长方形过渡圆形

这个动画过程,高度不变,只是长度变化。获取变化量:

变化量

float vRate = mRectF.centerX() - mRectF.centerY();

设置动画:

ValueAnimator valueAnimator = ValueAnimator.ofFloat(vRate);

设置动画过程监听:

valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float value = (float) animation.getAnimatedValue(); // 获取变化率
mRectF.left = value; // 左边移动位置
roundRadius = value; // 圆角角度
mRectF.right = viewWidth - value; // 控件初始宽度 - 右边移动距离
invalidate(); // 绘制到界面
}
});

文字透明消失

在图形变化的同时,文字也需要修改透明度。通过使用画笔的 setAlpha 函数进行透明度修改。设置的值越小,透明度越高,取值范围为0-255。

透明度取值计算:

Float textAlpha = 255 - value / vRate * 255;

在动画更新过程添加进去即可生效:

textPaint.setAlpha(textAlpha.intValue());

控件上移

使用 ObjectAnimator 创建上移动画。注意传入的值:
起始值:当前控件在布局文件的位置,通过 getTranslationY 函数获取
终点值:和起始值进行计算

float tY = getTranslationY();
ObjectAnimator translationY = ObjectAnimator.ofFloat(this, "translationY", tY, tY - moveDistance);
translationY.setDuration(duration);
translationY.start();

当前动画执行时机:当上个动画执行完毕。这里引入对动画监听:

valueAnimator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {

}

@Override
public void onAnimationEnd(Animator animation) {
// 执行下一个动画
}

@Override
public void onAnimationCancel(Animator animation) {

}

@Override
public void onAnimationRepeat(Animator animation) {

}
});

绘制对勾

首先创建对勾路径。

对勾的起点需要算上原始控件长度,因此起点是从长方形过渡到圆形哪里计算:

float okStart = mRectF.centerX() - mRectF.centerY();

稍微美化一下路径:

okPath.moveTo(okStart + viewHeight / 8 * 3, viewHeight / 2);
okPath.lineTo(okStart + viewHeight / 2, viewHeight / 5 * 3);
okPath.lineTo(okStart + viewHeight / 3 * 2, viewHeight / 5 * 2);

然后就是我们需要测量这个路径,然后绘制。关于测量,参考:安卓自定义View进阶-PathMeasure

在这里我们需要注意这个函数:

ValueAnimator valueAnimator = ValueAnimator.ofFloat(1, 0);
valueAnimator.setDuration(duration);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float value = (float) animation.getAnimatedValue();
DashPathEffect dashPathEffect = new DashPathEffect(new float[]{mPathMeasure.getLength(), mPathMeasure.getLength()}, value * mPathMeasure.getLength());
okPaint.setPathEffect(dashPathEffect);
invalidate();
}
});

动画设置范围从1-0,原因是因为DashPathEffect类设置的虚线是先画实线,再画虚线,构造函数的第二个参数是偏移量。如果动画从0开始,一开始的偏移量就小,相当于直接把实线画出来了。

偏移量示意图

至此整个流程完成。

流程优化

整体有三个动画,我们可以使用动画集合来进行整合,而不用每个动画都进行监听更新。

mAnimatorSet.play(mUpAnim) // 执行up动画
.before(mOkAnim) // 在执行ok动画之前
.after(mStartAnim); // 在执行start动画之后

至此,上述功能完全实现。

小结

在自定义控件的路上还有很多东西需要学习了解及掌握,学会适当变通很关键。同样一个效果,实现途径是很多的,没必要固守一种方式。即便是借鉴,也需要了解背后的原理是什么,最好可以总结记录下来,以便日后分析。

源代码

GitHub : SubmitButtonAnim

© 著作权归作者所有
这个作品真棒,我要支持一下!
Android 开发路上的点滴记录
0条评论
top Created with Sketch.