美文网首页Android自定义控件效果Android知识
Android自定义view之属性动画初见

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

作者: 24K纯帅豆 | 来源:发表于2017-01-10 17:29 被阅读1103次

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

    相关文章

      网友评论

      • BillBian:这里的属性动画,就是通过控制当个点的变化过程,然后在重绘制的时候,重新绘制那两个点。是不是?
        24K纯帅豆:@被风追的猪 :+1:
        BillBian:@24K纯帅豆 不错,又学了一招:smile:
        24K纯帅豆:是的,因为两个点是对称的,所以这里只需要知道一个点的轨迹就行了

      本文标题:Android自定义view之属性动画初见

      本文链接:https://www.haomeiwen.com/subject/bcefbttx.html