Android自定义view之属性动画初见
序言:初到新公司,暂时工作没有那么忙,每天都在看公司的代码,在看代码以及效果的同时发现一个很大的问题,就是打开新的Activity的时候都会有一段progressDialog显示,刚开始我以为是他们自己自定义的view,后来才发现原来是帧动画实现的,LZ比较有强迫症,大量的图片汇集在一起生成一个帧动画,怎么想都觉得有点划不来,而且大量的图片处理不当的话会造成系统卡顿和OOM,加之这两天在学习属性动画的一些知识,所以就想着能不能换成属性动画来实现,所以就拿这个栗子来练练手,往下看:
项目需求上图为项目中的需求,两个小球横向来回移动(不是3D旋转的那种),所以看起来比较容易实现,讲一下思路:
注:首先这个效果我知道的有两种方法可以实现(其实讲到底也可以说是一种,自定义view+线程),一是自定义view+线程,二是自定义view+属性动画;
实现思路:首先有两个球,初始化的时候可以把它们都放在中心位置,然后改变两个圆心的位置(这里知道一个圆心位置的改变就可以了,因为两边是对称的)进行重绘界面。
自定义view+线程:利用线程来控制小球的来回平移,每次计算小球圆心的变化,记录圆心的值
自定义view+属性动画:利用属性动画来控制小球的来回平移,每次都改变小球圆心位置这一属性
下面来看看实现方法:
线程控制:
@Override
public void run() {
while (isStart) { //线程是否开启
try {
if (isDisjoint) { //判断两个小球是否处于相离状态
//判断左边的小球有没有"走"到最左边(人为给定)
if (blueX <= getWidth() / 2 && blueX >= getWidth() / 2 - 40 && redX >= getWidth() / 2 && redX <= getWidth() / 2 + 40) {
blueX -= 3;
redX += 3;
postInvalidate();
} else {
blueX = getWidth() / 2 - 40;
redX = getWidth() / 2 + 40;
isDisjoint = false;
}
} else {
//判断右边的小球有没有"走"到最右边(人为给定)
if (blueX <= getWidth() / 2 && blueX >= getWidth() / 2 - 40 && redX >= getWidth() / 2 && redX <= getWidth() / 2 + 40) {
blueX += 3;
redX -= 3;
postInvalidate();
} else {
isDisjoint = true;
blueX = getWidth() / 2;
redX = getWidth() / 2;
}
}
Thread.sleep(15);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
我觉得这样做还是比较容易理解的,当左边小球的圆心到达最左边时(当右边小球的圆心到达最右边时)设置往回走,这样循环往复,就可以维持一个来回移动的状态,这种就是使用线程来完成的一个动画的效果,整个源码如下:
public class CustomDialogView extends View implements Runnable {
private Paint mPaint;
private boolean isStart;
private float blueX, redX; //蓝红色小球的圆点x值,默认的y值为getHeight()/2
private boolean isDisjoint = true;
private boolean isFirst = true;
public CustomDialogView(Context context) {
this(context, null);
}
public CustomDialogView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CustomDialogView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setColor(Color.BLUE);
mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
mPaint.setAntiAlias(true);
if (isFirst) {
isFirst = false;
blueX = getWidth() / 2;
redX = getWidth() / 2;
}
canvas.drawCircle(blueX, getHeight() / 2, 20, mPaint);
mPaint.setColor(Color.RED);
canvas.drawCircle(redX, getHeight() / 2, 20, mPaint);
}
//控制线程的开始
public boolean isStart() {
return isStart;
}
public void setStart(boolean start) {
isStart = start;
}
@Override
public void run() {
while (isStart) { //线程是否开启
try {
if (isDisjoint) { //判断两个小球是否处于相离状态
//判断左边的小球有没有"走"到最左边(人为给定)
if (blueX <= getWidth() / 2 && blueX >= getWidth() / 2 - 40 && redX >= getWidth() / 2 && redX <= getWidth() / 2 + 40) {
blueX -= 3;
redX += 3;
postInvalidate();
} else {
blueX = getWidth() / 2 - 40;
redX = getWidth() / 2 + 40;
isDisjoint = false;
}
} else {
//判断右边的小球有没有"走"到最右边(人为给定)
if (blueX <= getWidth() / 2 && blueX >= getWidth() / 2 - 40 && redX >= getWidth() / 2 && redX <= getWidth() / 2 + 40) {
blueX += 3;
redX -= 3;
postInvalidate();
} else {
isDisjoint = true;
blueX = getWidth() / 2;
redX = getWidth() / 2;
}
}
Thread.sleep(15);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
下面来看看使用属性动画来做的这样一个效果。首先呢,我们要知道什么是属性动画,顾名思义,就是通过修改某个属性而达到某种效果(某些效果)。这里呢,讲一个本次实验中用到的一个类TypeEvaluator,这个类可以帮我们完成一个功能,就是告诉系统如何从初始值过渡到结束值,我们要自定义一个这样的TypeEvaluator,然后重写它里面的evaluate()方法:
public class CustomPointEvaluator implements TypeEvaluator {
/**
*
* @param fraction 系数
* @param startValue 起始值
* @param endValue 终点值
* @return
*/
@Override
public Object evaluate(float fraction, Object startValue, Object endValue) {
CustomPoint startPoint = (CustomPoint) startValue;
CustomPoint endPoint = (CustomPoint) endValue;
float x = startPoint.getX() + fraction * (endPoint.getX() - startPoint.getX());
float y = startPoint.getY() + fraction * (endPoint.getY() - startPoint.getY());
CustomPoint point = new CustomPoint(x, y);
return point;
}
}
可以看到,这里我们是对CustomPoint对象操作的,所以最终返回的对象也是一个CustomPoint对象,其实evaluate()方法中的逻辑还是很好理解的,将startValue和endValue强转成CustomPoint对象,这里的CustomPoint表示的是一个点的坐标,也就是两个球的圆心的坐标,然后根据fraction系数,计算出当前动画的x和y的值,下面给出CustomPoint的代码:
public class CustomPoint {
private float x;
private float y;
public CustomPoint(float x, float y) {
this.x = x;
this.y = y;
}
public float getX() {
return x;
}
public void setX(float x) {
this.x = x;
}
public float getY() {
return y;
}
public void setY(float y) {
this.y = y;
}
}
完成自定义TypeEvaluator之后,我们就可以来尝试一下如何通过对CustomPoint对象进行动画操作:
//开始动画
private void startAnimation() {
CustomPoint startPoint = new CustomPoint(getWidth() / 2, getHeight() / 2);
CustomPoint endPoint = new CustomPoint(getWidth() / 2 - 2 * RADIUS, getHeight() / 2);
anim = ValueAnimator.ofObject(new CustomPointEvaluator(), startPoint, endPoint, startPoint);
anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
currentPointBlue = (CustomPoint) animation.getAnimatedValue();
invalidate();
}
});
anim.setDuration(600);
anim.setRepeatCount(Animation.INFINITE);
}
首先先创建两个点,startPoint和endPoint,这里讲清楚一下,这两个点并不是红蓝两个球的圆心,而是一个球的起始位置,我们知道了一个球的平移轨迹,另外一个也就知道了,在画圆的时候花两个圆就好了,这个方法中还有一个比较重要的就是ValueAnimator.AnimatorUpdateListener监听事件,事件中的onAnimationUpdate方法是在动画中每一帧更新的时候调用,监听这个接口可以使用动画播放过程中由ValueAnimator计算出来的值。为了使用这个值,使用传递给事件的ValueAnimator的对象的getAnimatedValue()接口来获取当前的动画值。如果你使用 ValueAnimator,必须实现这个方法。可以看到,我在这个方法中获得了一个CustomPoint对象,这个对象的属性值是在动画播放的过程中改变的,所以我们调用重新绘制方法来重绘界面,这样也能达到以上的目的,我们来看看完整的代码:
public class CustomPropertyAnimationView extends View {
private static final float RADIUS = 25; //小球半径
private CustomPoint currentPointBlue; //蓝色的小球
private CustomPoint currentPointRed; //红色的小球
private Paint mPaint; //蓝色小球的画笔
private static final int BOUNDARY = 70; //白色的边界长度
private ValueAnimator anim; //动画
public CustomPropertyAnimationView(Context context) {
this(context, null);
}
public CustomPropertyAnimationView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CustomPropertyAnimationView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView();
}
private void initView() {
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setAntiAlias(true);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (currentPointBlue == null) {
currentPointBlue = new CustomPoint(getWidth() / 2, getHeight() / 2);
currentPointRed = new CustomPoint(getWidth() / 2, getHeight() / 2);
drawCircle(canvas); //画圆
if (isFirst) {
startAnimation(); //开始动画
isFirst = false;
}
} else {
drawCircle(canvas);
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int mode = MeasureSpec.getMode(widthMeasureSpec);
int size = MeasureSpec.getSize(widthMeasureSpec);
int width;
int height;
if (mode == MeasureSpec.EXACTLY) { //表示确定的值或者MATCH_PARENT
width = size;
} else { //表示WARP_CONTENT
width = (int) (2 * RADIUS + 2 * BOUNDARY + getPaddingLeft() + getPaddingRight());
}
mode = MeasureSpec.getMode(heightMeasureSpec);
size = MeasureSpec.getSize(heightMeasureSpec);
if (mode == MeasureSpec.EXACTLY) {
height = size;
} else { //表示WARP_CONTENT
height = (int) (4 * RADIUS + getPaddingTop() + getPaddingBottom());
}
setMeasuredDimension(width, height);
}
//外部调用的地方可以控制动画的开始、暂停与停止
public void startCustomAnim() {
if (!anim.isStarted() || anim.isPaused()) {
anim.start();
}
}
public void stopCustomAnim() {
if (anim.isStarted()) {
anim.end();
}
}
public void pauseCustomAnim() {
if (!anim.isPaused()) {
anim.pause();
}
}
//画圆
private void drawCircle(Canvas canvas) {
float blueX = currentPointBlue.getX();
float redX = Math.abs(currentPointBlue.getX() - getWidth() / 2) + getWidth() / 2;
currentPointRed.setX(redX + getWidth() / 2);
mPaint.setColor(Color.BLUE);
canvas.drawCircle(blueX, getHeight() / 2, RADIUS, mPaint);
mPaint.setColor(Color.RED);
canvas.drawCircle(redX, getHeight() / 2, RADIUS, mPaint);
}
//开始动画
private void startAnimation() {
CustomPoint startPoint = new CustomPoint(getWidth() / 2, getHeight() / 2);
CustomPoint endPoint = new CustomPoint(getWidth() / 2 - 2 * RADIUS, getHeight() / 2);
anim = ValueAnimator.ofObject(new CustomPointEvaluator(), startPoint, endPoint, startPoint);
anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
currentPointBlue = (CustomPoint) animation.getAnimatedValue();
invalidate();
}
});
anim.setDuration(600);
anim.setRepeatCount(Animation.INFINITE);
}
}
使用方法:
CustomPropertyAnimationView acpaAnim;
两个按钮的点击事件
case R.id.acpa_btn_start:
acpaAnim.startCustomAnim();
break;
case R.id.acpa_btn_pause:
acpaAnim.pauseCustomAnim();
break;
有一些属性你可以自己设定,从xml中获取,这里我为了方便演示,就直接用了确切的值。最后我们来比较一下这两种方法,这两种方法都是以线程为基础的,动画内部也是有线程的,只不过它内部会维护,性能可能会比较好一点,如果你也有好的方法,请私戳我一起交流。基础的属性动画篇就讲到这里,后面我还会继续深入学习Android属性动画。
公众号:Android技术经验分享