美文网首页
记录一个自定义View和动画设计过程

记录一个自定义View和动画设计过程

作者: 番茄tomato | 来源:发表于2020-04-17 11:58 被阅读0次

    模糊的想法:想要做一个自定义View,在ImageView的基础上扩展,就是展示图片的时候,先是纯白的底,显示几个加载的旋转的小球,然后小球合到中间消失,再从中间画出一个圈,这个圈的内容就是最终要显示的图片


    效果

    1.开始第一步,画出小球

    刚开始不考虑动画,只静态的画出小球,以下是我们想要的效果


    image.png

    实现思路将第一个小球画在x轴上,然后将x轴旋转30度,再画第二个球在x轴上

    新建一个类PointPicView继承于ImageView,加入目前需要的变量
    ps : 这里是androidx 好像要继承AppCompatImageView

    public class PointPicView extends androidx.appcompat.widget.AppCompatImageView{
        //画笔
        Paint mPaint;
        //小球的颜色
        List<Integer> colorList;
        //小球轨迹的半径
        float defaultRadius;
        //一开始默认 第一个小球和圆心的夹角
        float defaultAngle;
        //单个小球的半径
        float circleRadius;
    ...其他代码构造函数什么的都省略...
    }
    

    写出必要的构造方法什么的
    然后我们在initView()方法中,初始化这些参数,并且在构造函数中调用initView()

        public void initView() {
            //初始化画笔
            mPaint = new Paint();
            //初始化小球的颜色 总共12个 其实不用在这写死,可以随机获取color什么的
            colorList = new ArrayList<>(
                    Arrays.asList(
                            getResources().getColor(R.color.green),
                            getResources().getColor(R.color.red),
                            getResources().getColor(R.color.pink),
                            getResources().getColor(R.color.powderblue),
                            getResources().getColor(R.color.palegoldenrod),
                            getResources().getColor(R.color.cyan),
                            getResources().getColor(R.color.lime),
                            getResources().getColor(R.color.palegreen),
                            getResources().getColor(R.color.mediumslateblue),
                            getResources().getColor(R.color.mediumvioletred),
                            getResources().getColor(R.color.sandybrown),
                            getResources().getColor(R.color.violet)
                    )
            );
    
            //小球轨迹的默认半径
            defaultRadius = 150;
            //画的第一个小球的圆心和x轴的夹角
            defaultAngle = 0;
            //小球的默认半径
            circleRadius=20;
        }
    

    接下来我们在onDraw中画出想要的效果

        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
            int width = getWidth();
            int height = getHeight();
            mPaint.setAntiAlias(true);//设置抗锯齿
            //保存坐标系原点在右上角的状态
            canvas.save();
            //将坐标系原点移动到正中央 方便计算 也就是将圆心置为(0,0)
            canvas.translate(width / 2, height / 2);
            //在画第一个小球之前旋转一个初始角(这个后边动画会用到)
            canvas.rotate(defaultAngle);
            //通过旋转坐标系 来绘制轨迹在弧上的圆 30度画一个 总共12个
            for(int color : colorList)
            {
            //设置对应的颜色画笔
                mPaint.setColor(color);
            //画小球
                canvas.drawCircle(defaultRadius, 0, circleRadius, mPaint);
           //将坐标系旋转30度
                canvas.rotate(30);
            }
        }
    

    到此我们想要的静态效果就完成了

    2.旋转吧 我的球

    效果:

    旋转的小球
    小球的旋转效果当然是通过view的不断重绘完成的,还记得我们之前看似没有什么用的参数defaultAngle吗,现在发挥大作用了

    动画实现思路:当我们固定第一个小球也就是绿色的小球和水平线默认的夹角defaultAngle为0后,绘制出绿色的小球就在圆心的正右方,然后我们在下一次重绘的时候,让defaultAngle增加一点点,那这个绿色的小球看起来就会逆时针旋转了一点点,同理,下一个红色的小球也会旋转同样的弧度。通过连续不断的改变defaultAngle的值和连续不断的重绘,我们就可以得到这样的旋转效果了

    这里我们需要用到属性动画
    把创建动画封装在initAnim()方法中,并且在onDraw中调用

        public void initAnim(){
            //判断是否播开始放动画
            isPlayAnim=true;
            //旋转的动画
            ObjectAnimator rotaAnim=ObjectAnimator.ofFloat(this,"defaultAngle",0,360);
            //动画持续时间
            rotaAnim.setDuration(3000);
            //无限循环
            rotaAnim.setRepeatCount(ValueAnimator.INFINITE);
            //重播模式:从头开始播放
            rotaAnim.setRepeatMode(ValueAnimator.RESTART);
            //设置线性变化插值器
            rotaAnim.setInterpolator(new LinearInterpolator());
            //每次数据更新后的动作 重绘
            rotaAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    postInvalidate();
                }
            });
            //启动
            rotaAnim.start();
        }
    

    可以看到多了一个参数isPlayAnim这个是用于判断动画是否开始播放的全局变量,因为我们是在onDraw中调用开始播放动画,但是动画重绘又会调用onDraw,所以为了避免initAnim()被重复调用,增加一个判断

    @Override
        protected void onDraw(Canvas canvas) {
    .....代码省略....
    
            //如果没有播放动画 则开始播放
            if(!isPlayAnim){
                initAnim();
            }
    }
    

    这里还需要增加defaultAngle的set/get方法,因为属性动画会调用,以后不单独强调

        public float getDefaultAngle() {
            return defaultAngle;
        }
    
        public void setDefaultAngle(float defaultAngle) {
            this.defaultAngle = defaultAngle;
        }
    

    到这里小球就旋转起来了

    3.然后将小球收拢并消失

    效果图
    动画实现思路:这里动画在原本旋转的基础上,增加了小球往中间靠拢,并且小球消失的新动画。我们主要改变的属性是小球轨迹半径defaultRadius从原本的值变到0,小球自身半径circleRadius从原本的值变到0,在旋转的同时,同时改变这两个属性,就可以达到效果
    为了代码更简洁,我们将defaultRadiuscircleRadius封装在LittleCircle中:
    public class LittleCircle implements Serializable {
        //单个小球的半径
        float circleRadius;
        //小球轨迹的半径
        float defaultRadius;
        public LittleCircle(float circleRadius, float defaultRadius) {
            this.circleRadius = circleRadius;
            this.defaultRadius = defaultRadius;
        }
    }
    

    然后在PointPicView中将原本的两条属性替换为LittleCircle对象。并增加get/set方法

        //单个小球的半径
       // float circleRadius;
        //小球轨迹的半径
        //float defaultRadius;
        //控制小球的一些属性 包括小球的半径,小球到圆心的距离
        LittleCircle littleCircle;
    ......
         //初始化小球的属性
        littleCircle=new LittleCircle(20,150);
    ......
        //使用littleCircle对象内的值来画小球
        canvas.drawCircle(littleCircle.defaultRadius, 0, littleCircle.circleRadius, mPaint);
    //get和set
    

    接下来我们要使用ObjectAnimator.ofObject创建缩放的动画,但是此时由于变化的属性是我们自定义的对象,系统不能像FLOA那个为我们估值,所以需要先准备好一个估值器LittleCircleEvaluator,告诉系统怎么运算

    /**
     * 估值器
     */
    public class LittleCircleEvaluator implements TypeEvaluator<LittleCircle> {
        //fraction是当前动画进度 比如50%
        @Override
        public LittleCircle evaluate(float fraction, LittleCircle startValue, LittleCircle endValue) {
            float circleRadius = startValue.circleRadius + fraction * (endValue.circleRadius - startValue.circleRadius);
            float defaultRadius = startValue.defaultRadius + fraction * (endValue.defaultRadius - startValue.defaultRadius);
            return new LittleCircle(circleRadius, defaultRadius);
        }
    }
    

    现在可以正式创建动画实例了:

        public void initAnim(){
            isPlayAnim=true;
            //旋转的动画
            ObjectAnimator rotaAnim=ObjectAnimator.ofFloat(this,"defaultAngle",0,360);
            rotaAnim.setDuration(1000);
            rotaAnim.setInterpolator(new LinearInterpolator());
            rotaAnim.setRepeatCount(1);
            rotaAnim.setRepeatMode(ValueAnimator.RESTART);
            //收缩旋转的动画 littleCircle→new LittleCircle(0,0)
            ObjectAnimator shrinkAnim=ObjectAnimator.ofObject(this,"littleCircle",new LittleCircleEvaluator(), littleCircle,new LittleCircle(0,0));
            shrinkAnim.setDuration(1000);
            shrinkAnim.setInterpolator(new LinearInterpolator());
            //旋转和收缩的set
            AnimatorSet shrinkAndRota=new AnimatorSet();
            shrinkAndRota.play(rotaAnim).with(shrinkAnim);
            //控制播放顺序
            AnimatorSet animatorSet=new AnimatorSet();
            animatorSet.play(rotaAnim).before(shrinkAndRota);
            //开始播放
            animatorSet.start();
        }
    

    修改了原本旋转动画的效果,比如取消了无限重复
    为了方便同意重绘,这里我们的重绘步骤放到了属性的set方法中

       public void setDefaultAngle(float defaultAngle) {
            this.defaultAngle = defaultAngle;
    //重绘
            postInvalidate();
        }
        public void setLittleCircle(LittleCircle littleCircle) {
            this.littleCircle = littleCircle;
    //重绘
            postInvalidate();
        }
    

    现在运行代码就可以达到上边的效果啦

    4.显示图片

    在中心画个不断扩大的透明的圆,像橡皮檫那样擦掉周围的白色,显示图片


    效果

    欸嘿 帅气的吴彦祖
    动画实现思路:先在最底层添加一张照片,在上边的收缩动画完成后,从中间画一个透明的圆慢慢扩大

    先在最底层添加图片,因为继承的ImageView所以直接添加没有问题

        <PointPicView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:scaleType="centerCrop"
            android:src="@drawable/timg"//图片资源文件
            />
    

    这里问题来了,我们的画比没有透明色的选项,也没有橡皮檫的选项,这该怎么实现透明的圆呢?

    4.1 剪切

    仔细看,我们会发现canvas有一些方法用于剪切画布:


    剪切的方法

    这里简单说明一下用法,看以下两行代码

            canvas.clipRect(0,0,100,100);
            canvas.drawColor(Color.WHITE);
    

    会出现以下效果:

    clipRect
    在以上代码中我们将画布剪切只保留了左上角(0,0)到右下角(100,100)的一个矩形,之后所有绘图的操作都只会显示在这个矩形的范围里,所以下一行画白色只覆盖到了这个矩形,而被去掉的部分则会因为没有像素点而变为黑色
    同理,clipOutRect就是刚好取相反发范围
            canvas.clipRect(0,0,100,100);
            canvas.drawColor(Color.WHITE);
    
    clipOutRect
    当然我们剪切也不是固定的矩形,这时候可以使用clipPathclipOutPath配合Path来剪切出我们想要的仍和形状

    所以我们只需要把中间一个圆剪掉,再将白色覆盖全图就可以达到一个透明的圆的效果

            Path path=new Path();
            //创建一个在正中间的圆path
            path.addCircle(width / 2, height / 2,centerCircleRadius,Path.Direction.CCW);
            //剪切画布 会在剪切的范围内绘画
            canvas.clipOutPath(path);
            //全为白色
            canvas.drawColor(Color.WHITE);
    

    ,但是问题又来了,我们只有一个画布,剪切了圆后,连背景原本该保留的照片也会被一起剪掉,如下:


    背景照片也被清空

    4.2 图层

    为了解决上边的问题,我们可以新建bitmap,然后以这个bitmap为作用对建立画布,只对这个画布进行剪切,不会影响到原本的canvas
    就像这样:

    image.png

    所以:

            //新建图层bitmapMask 大小就是view的大小
            Bitmap bitmapMask = Bitmap.createBitmap(width,height,Bitmap.Config.ARGB_8888);
            //在蒙版图层上创建画布
            Canvas canvasMask=new Canvas(bitmapMask);
    
            Path path=new Path();
            path.addCircle(width / 2, height / 2,centerCircleRadius,Path.Direction.CCW);
            //剪切画布 会在剪切的范围内绘画
            canvasMask.clipOutPath(path);
            //剪切下来全为白色
            canvasMask.drawColor(Color.WHITE);
            //在View的画布上画出蒙版图层 挡在图片和效果动画之间
            canvas.drawBitmap(bitmapMask,0,0,null);
    

    这里最后一定要记得在原本的画布上将新加的bitmap画出来,这样才能看到效果
    这里我们可以看到增加了一个参数centerCircleRadius也就是中间透明的圆的半径,我们的不断扩大效果动画就要作用于这个属性,所以get/set不用我多说了吧

    下面创建动画:

            //中间一个透明的圆不断扩大,显示图片的动画
            ObjectAnimator showPicAnim=ObjectAnimator.ofFloat(this,"centerCircleRadius",centerCircleRadius,Math.max(getMeasuredWidth(),getMeasuredHeight()));
    //这里中间透明的圆半径扩大到多少合适,我直接取的max,保证能将图片完全显示
            showPicAnim.setDuration(2000);
            showPicAnim.setInterpolator(new LinearInterpolator());
    

    现在我们已经有三个动画了分别是:
    旋转的动画:rotaAnim
    收缩的动画:shrinkAnim
    透明圆扩大的动画:showPicAnim
    需要使用AnimatorSet好好的整理一下动画播放的顺序:先旋转,然后旋转和缩放同时播放,最后播放透明圆扩大

          //同时播放旋转和收缩的AnimatorSet
          AnimatorSet shrinkAndRota=new AnimatorSet();
          shrinkAndRota.play(rotaAnim).with(shrinkAnim);
          //控制播放顺序
          AnimatorSet animatorSet=new AnimatorSet();
          animatorSet.play(rotaAnim).before(shrinkAndRota);
          animatorSet.play(showPicAnim).after(shrinkAndRota);
          animatorSet.start();
    

    到这里我们这个憨憨的自定义View就完成了,确实有很多不足的地方鸭,主要是作为安卓属性动画的一个demo作业吧
    以下是完整代码:
    layout中使用:

        <PointPicView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:scaleType="centerCrop"
            android:src="@drawable/timg"
            />
    

    源码:(LittleCircleEvaluator,LittleCircle之前完整列了 这里就不贴了)
    PointPicView

    public class PointPicView extends androidx.appcompat.widget.AppCompatImageView {
        //画笔
        Paint mPaint;
        //小球的颜色
        List<Integer> colorList;
        //一开始默认 第一个小球和圆心的夹角
        float defaultAngle;
        boolean isPlayAnim=false;
        //控制小球的一些属性 包括小球的半径,小球到圆心的距离
        LittleCircle littleCircle;
    
        //中心透明的圈半径
        float centerCircleRadius;
    
    
        public PointPicView(Context context) {
            super(context);
            initView();
        }
    
        public PointPicView(Context context, @Nullable AttributeSet attrs) {
            super(context, attrs);
            initView();
        }
    
        public void initView() {
            //初始化画笔
            mPaint = new Paint();
            //初始化小球的颜色 总共12个 其实不用在这写死,可以随机获取color什么的
            colorList = new ArrayList<>(
                    Arrays.asList(
                            getResources().getColor(R.color.green),
                            getResources().getColor(R.color.red),
                            getResources().getColor(R.color.pink),
                            getResources().getColor(R.color.powderblue),
                            getResources().getColor(R.color.palegoldenrod),
                            getResources().getColor(R.color.cyan),
                            getResources().getColor(R.color.lime),
                            getResources().getColor(R.color.palegreen),
                            getResources().getColor(R.color.mediumslateblue),
                            getResources().getColor(R.color.mediumvioletred),
                            getResources().getColor(R.color.sandybrown),
                            getResources().getColor(R.color.violet)
                    )
            );
            defaultAngle=0;
            //初始化小球的属性
            littleCircle=new LittleCircle(20,150);
            centerCircleRadius=0;
        }
    
        @RequiresApi(api = Build.VERSION_CODES.O)
        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
            int width = getWidth();
            int height = getHeight();
            mPaint.setAntiAlias(true);//设置抗锯齿
    
    
    
            //新建图层bitmapMask 大小就是view的大小
            Bitmap bitmapMask = Bitmap.createBitmap(width,height,Bitmap.Config.ARGB_8888);
            //在蒙版图层上创建画布
            Canvas canvasMask=new Canvas(bitmapMask);
    
            Path path=new Path();
            path.addCircle(width / 2, height / 2,centerCircleRadius,Path.Direction.CCW);
            //剪切画布 会在剪切的范围内绘画
            canvasMask.clipOutPath(path);
            //全为白色
            canvasMask.drawColor(Color.WHITE);
            //在View的画布上画出蒙版图层 挡在图片和效果动画之间
            canvas.drawBitmap(bitmapMask,0,0,null);
    
    
    
    
    
            //保存坐标系原点在右上角的状态
            canvas.save();
            //将坐标系原点移动到正中央 方便计算
            canvas.translate(width / 2, height / 2);
            //初始角 来画动画的
            canvas.rotate(defaultAngle);
            //通过旋转坐标系 来绘制轨迹在弧上的圆 30度画一个 总共12个
            for(int color : colorList)
            {
                mPaint.setColor(color);
                canvas.drawCircle(littleCircle.defaultRadius, 0, littleCircle.circleRadius, mPaint);
                canvas.rotate(30);
            }
    
            //如果没有播放动画 则开始播放
            if(!isPlayAnim){
                initAnim();
            }
    
    
    
        }
    
        public void initAnim(){
            isPlayAnim=true;
            //旋转的动画
            ObjectAnimator rotaAnim=ObjectAnimator.ofFloat(this,"defaultAngle",0,360);
            rotaAnim.setDuration(1000);
            rotaAnim.setInterpolator(new LinearInterpolator());
    
            //收缩的动画
            ObjectAnimator shrinkAnim=ObjectAnimator.ofObject(this,"littleCircle",new LittleCircleEvaluator(), littleCircle,new LittleCircle(0,0));
            shrinkAnim.setDuration(1000);
            shrinkAnim.setInterpolator(new LinearInterpolator());
    
    
    
            //中间一个透明的圆不断扩大,显示图片的动画
            ObjectAnimator showPicAnim=ObjectAnimator.ofFloat(this,"centerCircleRadius",centerCircleRadius,Math.max(getMeasuredWidth(),getMeasuredHeight()));
            showPicAnim.setDuration(2000);
            showPicAnim.setInterpolator(new LinearInterpolator());
    
    
            //同时播放旋转和收缩的AnimatorSet
            AnimatorSet shrinkAndRota=new AnimatorSet();
            shrinkAndRota.play(rotaAnim).with(shrinkAnim);
            //控制播放顺序
            AnimatorSet animatorSet=new AnimatorSet();
            animatorSet.play(rotaAnim).before(shrinkAndRota);
            animatorSet.play(showPicAnim).after(shrinkAndRota);
            animatorSet.start();
    
        }
    
        public float getDefaultAngle() {
            return defaultAngle;
        }
    
        public void setDefaultAngle(float defaultAngle) {
            this.defaultAngle = defaultAngle;
            postInvalidate();
        }
    
        public LittleCircle getLittleCircle() {
            return littleCircle;
        }
    
        public void setLittleCircle(LittleCircle littleCircle) {
            this.littleCircle = littleCircle;
            postInvalidate();
        }
    
        public float getCenterCircleRadius() {
            return centerCircleRadius;
        }
    
        public void setCenterCircleRadius(float centerCircleRadius) {
            this.centerCircleRadius = centerCircleRadius;
            postInvalidate();
    
        }
    }
    

    相关文章

      网友评论

          本文标题:记录一个自定义View和动画设计过程

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