记录一个自定义View和动画设计过程
模糊的想法:想要做一个自定义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,在旋转的同时,同时改变这两个属性,就可以达到效果为了代码更简洁,我们将
defaultRadius
和circleRadius
封装在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);
会出现以下效果:
在以上代码中我们将画布剪切只保留了左上角(0,0)到右下角(100,100)的一个矩形,之后所有绘图的操作都只会显示在这个矩形的范围里,所以下一行画白色只覆盖到了这个矩形,而被去掉的部分则会因为没有像素点而变为黑色
同理,
clipOutRect
就是刚好取相反发范围
canvas.clipRect(0,0,100,100);
canvas.drawColor(Color.WHITE);
clipOutRect
当然我们剪切也不是固定的矩形,这时候可以使用
clipPath
和clipOutPath
配合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
就像这样:
所以:
//新建图层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();
}
}