Android 自定义 view 之属性动画初见

563 阅读7分钟

序言:初到新公司,暂时工作没有那么忙,每天都在看公司的代码,在看代码以及效果的同时发现一个很大的问题,就是打开新的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);
}

首先先创建两个点,startPointendPoint,这里讲清楚一下,这两个点并不是红蓝两个球的圆心,而是一个球的起始位置,我们知道了一个球的平移轨迹,另外一个也就知道了,在画圆的时候花两个圆就好了,这个方法中还有一个比较重要的就是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小白的群,各位有兴趣的小白欢迎加群共同学习,也欢迎各位大神进群指导,共勉。群号:541144061