在目前的移动端产品中,不管是app还是网页一个好看酷炫的页面总是会第一时间吸引人的眼球,那么对于android开发人员来说,要想实现一个好看的页面必然需要掌握自定义控件以及自定义动画这门技术。
Android下已经给我们提供了几种原生动画的表现形式:
①补间动画
平移:TranslateAnimation
旋转:RotateAnimation
缩放:ScaleAnimation
渐变:AlphaAnimation
②属性动画
ObjectAnimatior: translation(x或y),rotation(x或y),scale(x或y)
ValueAnimator:ObjectAnimatior的父类,值动画
③帧动画
AniamteDrawable
那么什么是自定义动画呢?其实不明觉厉,那就是自己根据需求定义的动画效果。因为在实际开发中,大部分的复杂酷炫的动画效果用我们Android原生提供的动画都是满足不了的,那么就需要我们自己去定义。
那么本篇文章将通过三个案例的形式给大家演示通过自定义动画的第一种表现方式-自定义控件绘制的方式
那么接下来通过一些小demo案例来进行演示如何实现一些原生动画无法实现的自定义效果。
首先看下效果图:
根据上面的效果图我们可以发现android原生动画是无法实现的,所以我们需要自定义控件动态去绘制这样的效果,那么思路可以分为以下两步:
那么因为我们需要去绘制这个扇形和弧线,所以首先需要去创建一个自定义的view重写它的onDraw()方法,在绘制之前可以在view创建的时候先将画笔初始化出来。
具体的难点在于第二步如何动态去绘制,可以定义一个具体的数值比如 shouldExistSignalSize 来控制每次绘制的时候绘制哪个信号,从最开始的时候只绘制第一个信号(也就是扇形),接着第二次绘制的时候需要绘制第一个和第二个信号,往后就是第一个信号,第二个信号,第三个信号;当四格信号都绘制完毕的时候记得将 shouldExistSignalSize 重置。
代码如下:
/**
* Created by PeiHuan on 2017/6/24.
*
* WIFI控件
*/
public class WIFIView extends View {
public WIFIView(Context context) {
this(context, null);
}
public WIFIView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public WIFIView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private Paint paint;
/**
* 初始化准备
*/
private void init() {
paint = new Paint();
//画笔颜色
paint.setColor(Color.BLACK);
//画笔粗细
paint.setStrokeWidth(6);
//抗锯齿
paint.setAntiAlias(true);
handler.postDelayed(new Runnable() {
@Override
public void run() {
invalidate();
handler.postDelayed(this,500);
}
},500);
}
private Handler handler = new Handler();
/**WIFI控件宽高较小一边的长度*/
private int wifiLength;
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
wifiLength = Math.min(w, h);
}
/**
* 开始角度
*/
private float startAngle = -135;
/**
* 扇形或者弧的旋转角度
*/
private float sweepAngle = 90;
/**
* 信号大小,默认4格
*/
private int signalSize = 4;
/**每次应该绘制的信号个数*/
private float shouldExistSignalSize = 0f;
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
shouldExistSignalSize++;
if(shouldExistSignalSize>4){
shouldExistSignalSize=1;
}
canvas.save();
//计算最小的扇形信号所在的圆的半径
float signalRadius = wifiLength/2/signalSize;
//向下平移画布,保证绘制的图形能够完全显示
canvas.translate(0,signalRadius);
for (int i = 0; i < signalSize; i++) {
if(i>=signalSize-shouldExistSignalSize) {
//定义每个信号所在圆的半径
float radius = signalRadius * i;
RectF rectf = new RectF(radius, radius, wifiLength - radius, wifiLength - radius);
if (i < signalSize - 1) {
paint.setStyle(Paint.Style.STROKE);
canvas.drawArc(rectf, startAngle, sweepAngle, false, paint);
} else {
paint.setStyle(Paint.Style.FILL);
canvas.drawArc(rectf, startAngle, sweepAngle, true, paint);
}
}
}
canvas.restore();
}
}
详细代码请参考GitHub仓库:
https://github.com/zphuanlove/AnimationProject
接下来再来看一个很常见的效果,同样也是通过自定义控件实现ondraw()去动态绘制;这也是很多目前能看到的一些音乐相关的app具有的效果,效果图如下:
流程和思路和第一个demo类似,但产生动画的策略略有不同。
首先绘制静态效果图,剖析出整个图形由哪几部分组成:(两个圆,四段弧线)
通过不断绘制产生动画(不断更改四段弧线的起始角度)
WIFI demo我们使用的是handler的postDelayed方法造成一个死循环
这次我们可以通过在onDraw方法中调用invalidate来实现动画。
具体实现同样也是在构造函数初始的时候去初始化画笔,然后在ondraw()方法中去绘制一个大圆一个小圆以及四段弧线,这里四段弧线可以分成两部分,及相对的两部分,每部分由一个大弧和一个小弧组成,两部分之间间隔180度。要绘制弧线就是要确认弧所在圆的外接矩形的左上右下,通过下图的计算可以很方便的计算出大弧所在的矩形的左上右下:
代码如下:
/**
* Created by PeiHuan on 2017/6/24.
*
* Music控件
*/
public class MusicView extends View {
private Paint paint;
private int length;
public MusicView(Context context) {
this(context, null);
}
public MusicView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public MusicView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
/**
* 初始化操作
*/
private void init() {
paint = new Paint();
//画笔颜色
paint.setColor(Color.BLACK);
//画笔粗细
paint.setStrokeWidth(2);
//抗锯齿
paint.setAntiAlias(true);
//不填充
paint.setStyle(Paint.Style.STROKE);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
length = Math.min(w, h);
bigCircleRadius = length / 2;
bigAngelRadius = length / 3;
smallAngelRadius = length / 4;
}
/**
* 大圆的半径
*/
private float bigCircleRadius;
/**
* 小圆的半径
*/
private float smallCircleRadius = 5f;
/**
* 两段大弧的半径
*/
private float bigAngelRadius;
/**
* 两段小弧的半径
*/
private float smallAngelRadius;
private float startAngle1 = 0;
private float startAngle2 = 180;
private float sweepAngle = 60;
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//绘制两个圆
canvas.drawCircle(bigCircleRadius, bigCircleRadius, bigCircleRadius - smallCircleRadius, paint);
//小圆粗一些
paint.setStrokeWidth(3);
canvas.drawCircle(bigCircleRadius, bigCircleRadius, smallCircleRadius, paint);
//绘制四段弧线
//两段大弧,弧度相差180度
RectF rectF = new RectF(bigCircleRadius-bigAngelRadius,bigCircleRadius-bigAngelRadius,bigCircleRadius+bigAngelRadius,bigCircleRadius+bigAngelRadius);
canvas.drawArc(rectF,startAngle1,sweepAngle,false,paint);
canvas.drawArc(rectF,startAngle2,sweepAngle,false,paint);
//两段小弧,弧度相差180度
RectF rectFSmaller = new RectF(bigCircleRadius-smallAngelRadius,bigCircleRadius-smallAngelRadius,bigCircleRadius+smallAngelRadius,bigCircleRadius+smallAngelRadius);
canvas.drawArc(rectFSmaller,startAngle1,sweepAngle,false,paint);
canvas.drawArc(rectFSmaller,startAngle2,sweepAngle,false,paint);
startAngle1+=5;
startAngle2+=5;
if(!isDetached) {
invalidate();
}
}
/**
* 自定义控件是否脱离窗体
*/
private boolean isDetached;
/**
* 当自定义控件脱离窗体,即将销毁
*/
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
isDetached = true;
}
}
详细代码请参考GitHub仓库:
https://github.com/zphuanlove/AnimationProject
最后一个效果就要酷炫一些了,不过代码实现起来也很简单,因为这里主要是要用到android中的一个关键技术:着色器实现。
我们先来看看效果:
自定义TextView,在TextView的文本上添加遮盖物,并让着色器不断平移和下降,当下降完全显示文字后再移动着色器到文字顶部继续重复执行。
简单铺垫下shader的作用,如果想要深入学习的同学可以下去自行查阅资料,这里不是本篇的重点了。
我们在用Android中的Canvas绘制各种图形时,可以通过Paint.setShader(shader)方法为画笔Paint设置shader,这样就可以绘制出多彩的图形。那么Shader是什么呢?Shader就是着色器的意思。我们可以这样理解,Canvas中的各种drawXXX方法定义了图形的形状,画笔中的Shader则定义了图形的着色、外观,二者结合到一起就决定了最终Canvas绘制的被色彩填充的图形的样子。
类android.graphics.Shader有五个子类:
这里重点介绍下BitmapShader,因为该案例需要使用到它。
BitmapShader,顾名思义,就是用Bitmap对绘制的图形进行渲染着色,其实就是用图片对图形进行贴图。如果亲自装修过或者看过装修的同学应该知道新房在喷漆的时候会将一些不需要喷漆的地方用报纸进行包住来遮挡,那么BitmapShader也是通过的道理,比如我们想要对文字Loading进行喷色,着色,那么只需要将文字其余的地方遮住,只取图片上的颜色来对文字进行着色即可。
从字面上理论的角度理解后,再从它的构造函数上进行理解:
/**
* Call this to create a new shader that will draw with a bitmap.
*
* @param bitmap The bitmap to use inside the shader
* @param tileX The tiling mode for x to draw the bitmap in.
* @param tileY The tiling mode for y to draw the bitmap in.
*/
public BitmapShader(@NonNull Bitmap bitmap, TileMode tileX, TileMode tileY)
第一个参数是Bitmap对象,该Bitmap决定了用什么图片对绘制的图形进行贴图,着色。
第二个参数和第三个参数都是Shader.TileMode类型的枚举值,有以下三个取值:CLAMP 、REPEAT 和 MIRROR。
首先创建一个自定义view继承TextView,并且初始化字体,初始化着色器,代码如下:
/**
* Created by PeiHuan on 2017/6/25.
*
* 水面下降效果控件
*/
public class WaterTextView extends android.support.v7.widget.AppCompatTextView {
public WaterTextView(Context context) {
this(context,null);
}
public WaterTextView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs,0);
}
public WaterTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
//让当前的TextView的字体为美术字体
Typeface typeface = Typeface.createFromAsset(getResources().getAssets(), "Satisfy-Regular.ttf");
setTypeface(typeface);
//Matrix:矩阵,可以实现视图的平移旋转等效果
matrix = new Matrix();
//创建一个着色器
createShader();
}
private void createShader() {
Bitmap originalBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.wave);
int waveWidth = originalBitmap.getWidth();
waveHeight = originalBitmap.getHeight();
Bitmap bitmap=Bitmap.createBitmap(waveWidth, waveHeight, originalBitmap.getConfig());
//创建一个画布,为了将wave的图片颜色数据写入到Bitmap中
Canvas canvas=new Canvas(bitmap);
//设置画布的颜色从而控制文字的着色颜色
canvas.drawColor(Color.RED);
canvas.drawBitmap(originalBitmap,new Matrix(),getPaint());
//CLAMP:使用原来的那张图片整体
//REPEAT:将原来的图片复制无数份
//MIRROR:镜像,将原来的图片镜像后,写入,再镜像...
shader = new BitmapShader(bitmap, Shader.TileMode.REPEAT, Shader.TileMode.CLAMP);
getPaint().setShader(shader);
shaderX = 0;
shaderY = -waveHeight/2;
}
}
接着让波浪动起来,通过ondraw()方法调用invalidate(),控制着色器的Y轴平移,代码如下:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
repeatShader();
}
private void repeatShader() {
shaderX +=5;
shaderY +=0.1;
if(shaderY >-waveHeight/2+height){
shaderY = -waveHeight/2;
}
matrix.setTranslate(shaderX, shaderY);
shader.setLocalMatrix(matrix);
invalidate();
}
详细代码请参考GitHub仓库:
https://github.com/zphuanlove/AnimationProject
该篇文章通过三个案例效果给大家演示了如何实现android下的自定义动画效果,不过这仅仅只是android自定义动画的第一种表现形式,接下来还会在下篇文章中继续讲解第二种形式的展现。
文中的案例在github上均有详细以及扩展代码展示,三个案例汇总到一个项目中。
Thanks!