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

2020-04-17  本文已影响0人  番茄tomato

模糊的想法:想要做一个自定义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();

    }
}
上一篇下一篇

猜你喜欢

热点阅读