Android自定義動畫
在目前的移動端產(chǎn)品中,不管是app還是網(wǎng)頁一個好看酷炫的頁面總是會第一時間吸引人的眼球,那么對于android開發(fā)人員來說,要想實現(xiàn)一個好看的頁面必然需要掌握自定義控件以及自定義動畫這門技術(shù)。
1.Android原生動畫
Android下已經(jīng)給我們提供了幾種原生動畫的表現(xiàn)形式:
①補間動畫
平移:TranslateAnimation
旋轉(zhuǎn):RotateAnimation
縮放:ScaleAnimation
漸變:AlphaAnimation
②屬性動畫
ObjectAnimatior: translation(x或y),rotation(x或y),scale(x或y)
ValueAnimator:ObjectAnimatior的父類,值動畫
③幀動畫
AniamteDrawable
- 注意補間動畫和屬性動畫的最大區(qū)別:
- 補間動畫只是改變了View的顯示效果,并沒有真正改變View的屬性
- 屬性動畫是真正改變了View的屬性,比如平移效果。
- 屬性動畫是Android 3.0引入的
2.Android自定義動畫-表現(xiàn)形式一
那么什么是自定義動畫呢?其實不明覺厲,那就是自己根據(jù)需求定義的動畫效果。因為在實際開發(fā)中,大部分的復(fù)雜酷炫的動畫效果用我們Android原生提供的動畫都是滿足不了的,那么就需要我們自己去定義。
那么本篇文章將通過三個案例的形式給大家演示通過自定義動畫的第一種表現(xiàn)方式-自定義控件繪制的方式
那么接下來通過一些小demo案例來進(jìn)行演示如何實現(xiàn)一些原生動畫無法實現(xiàn)的自定義效果。
①WIFI效果
首先看下效果圖:

1)思路
根據(jù)上面的效果圖我們可以發(fā)現(xiàn)android原生動畫是無法實現(xiàn)的,所以我們需要自定義控件動態(tài)去繪制這樣的效果,那么思路可以分為以下兩步:
-
先繪制WIFI在滿信號的情況下的視圖效果,也就是靜態(tài)視圖
- 通過handler的postDelayed方法可以死循環(huán)不斷執(zhí)行view的invalidate()方法來達(dá)到動態(tài)繪制的效果(每次繪制需要控制繪制幾個信號)
2)具體實現(xiàn)
那么因為我們需要去繪制這個扇形和弧線,所以首先需要去創(chuàng)建一個自定義的view重寫它的onDraw()方法,在繪制之前可以在view創(chuàng)建的時候先將畫筆初始化出來。
具體的難點在于第二步如何動態(tài)去繪制,可以定義一個具體的數(shù)值比如 shouldExistSignalSize 來控制每次繪制的時候繪制哪個信號,從最開始的時候只繪制第一個信號(也就是扇形),接著第二次繪制的時候需要繪制第一個和第二個信號,往后就是第一個信號,第二個信號,第三個信號;當(dāng)四格信號都繪制完畢的時候記得將 shouldExistSignalSize 重置。
代碼如下:
/**
* Created by PeiHuan on 2017/6/24.
* <p>
* 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;
/**
* 初始化準(zhǔn)備
*/
private void init() {
paint = new Paint();
//畫筆顏色
paint.setColor(Color.BLACK);
//畫筆粗細(xì)
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;
/**
* 扇形或者弧的旋轉(zhuǎn)角度
*/
private float sweepAngle = 90;
/**
* 信號大小,默認(rèn)4格
*/
private int signalSize = 4;
/**每次應(yīng)該繪制的信號個數(shù)*/
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();
}
}
詳細(xì)代碼請參考GitHub倉庫:
https://github.com/zphuanlove/AnimationProject
②MUSIC效果
接下來再來看一個很常見的效果,同樣也是通過自定義控件實現(xiàn)ondraw()去動態(tài)繪制;這也是很多目前能看到的一些音樂相關(guān)的app具有的效果,效果圖如下:

1)思路
流程和思路和第一個demo類似,但產(chǎn)生動畫的策略略有不同。
-
首先繪制靜態(tài)效果圖,剖析出整個圖形由哪幾部分組成:(兩個圓,四段弧線)
通過不斷繪制產(chǎn)生動畫(不斷更改四段弧線的起始角度)
WIFI demo我們使用的是handler的postDelayed方法造成一個死循環(huán)
這次我們可以通過在onDraw方法中調(diào)用invalidate來實現(xiàn)動畫。
2)具體實現(xiàn)
具體實現(xiàn)同樣也是在構(gòu)造函數(shù)初始的時候去初始化畫筆,然后在ondraw()方法中去繪制一個大圓一個小圓以及四段弧線,這里四段弧線可以分成兩部分,及相對的兩部分,每部分由一個大弧和一個小弧組成,兩部分之間間隔180度。要繪制弧線就是要確認(rèn)弧所在圓的外接矩形的左上右下,通過下圖的計算可以很方便的計算出大弧所在的矩形的左上右下:

代碼如下:
/**
* Created by PeiHuan on 2017/6/24.
* <p>
* 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);
//畫筆粗細(xì)
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;
/**
* 當(dāng)自定義控件脫離窗體,即將銷毀
*/
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
isDetached = true;
}
}
詳細(xì)代碼請參考GitHub倉庫:
https://github.com/zphuanlove/AnimationProject
③文字水波效果
最后一個效果就要酷炫一些了,不過代碼實現(xiàn)起來也很簡單,因為這里主要是要用到android中的一個關(guān)鍵技術(shù):著色器實現(xiàn)。
我們先來看看效果:

1)思路
自定義TextView,在TextView的文本上添加遮蓋物,并讓著色器不斷平移和下降,當(dāng)下降完全顯示文字后再移動著色器到文字頂部繼續(xù)重復(fù)執(zhí)行。
2)著色器Shader
簡單鋪墊下shader的作用,如果想要深入學(xué)習(xí)的同學(xué)可以下去自行查閱資料,這里不是本篇的重點了。
我們在用Android中的Canvas繪制各種圖形時,可以通過Paint.setShader(shader)方法為畫筆Paint設(shè)置shader,這樣就可以繪制出多彩的圖形。那么Shader是什么呢?Shader就是著色器的意思。我們可以這樣理解,Canvas中的各種drawXXX方法定義了圖形的形狀,畫筆中的Shader則定義了圖形的著色、外觀,二者結(jié)合到一起就決定了最終Canvas繪制的被色彩填充的圖形的樣子。
類android.graphics.Shader有五個子類:
- LinearGradient 線性梯度漸變
- RadialGradient 環(huán)形梯度漸變或者燈光渲染
- SweepGradient 掃描梯度漸變
- BitmapShader 圖片渲染
- ComposeShader 組合渲染
這里重點介紹下BitmapShader,因為該案例需要使用到它。
BitmapShader,顧名思義,就是用Bitmap對繪制的圖形進(jìn)行渲染著色,其實就是用圖片對圖形進(jìn)行貼圖。如果親自裝修過或者看過裝修的同學(xué)應(yīng)該知道新房在噴漆的時候會將一些不需要噴漆的地方用報紙進(jìn)行包住來遮擋,那么BitmapShader也是通過的道理,比如我們想要對文字Loading進(jìn)行噴色,著色,那么只需要將文字其余的地方遮住,只取圖片上的顏色來對文字進(jìn)行著色即可。
從字面上理論的角度理解后,再從它的構(gòu)造函數(shù)上進(jìn)行理解:
/**
* 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)
第一個參數(shù)是Bitmap對象,該Bitmap決定了用什么圖片對繪制的圖形進(jìn)行貼圖,著色。
第二個參數(shù)和第三個參數(shù)都是Shader.TileMode類型的枚舉值,有以下三個取值:CLAMP 、REPEAT 和 MIRROR。
- CLAMP表示,當(dāng)所畫圖形的尺寸大于Bitmap的尺寸的時候,會用Bitmap四邊的顏色填充剩余空間。
- REPEAT表示,當(dāng)我們繪制的圖形尺寸大于Bitmap尺寸時,會用Bitmap重復(fù)平鋪整個繪制的區(qū)域。
- MIRROR 與REPEAT類似,當(dāng)繪制的圖形尺寸大于Bitmap尺寸時,MIRROR也會用Bitmap重復(fù)平鋪整個繪圖區(qū)域,與REPEAT不同的是,兩個相鄰的Bitmap互為鏡像。
3)代碼實現(xiàn)
首先創(chuàng)建一個自定義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() {
//讓當(dāng)前的TextView的字體為美術(shù)字體
Typeface typeface = Typeface.createFromAsset(getResources().getAssets(), "Satisfy-Regular.ttf");
setTypeface(typeface);
//Matrix:矩陣,可以實現(xiàn)視圖的平移旋轉(zhuǎn)等效果
matrix = new Matrix();
//創(chuàng)建一個著色器
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());
//創(chuàng)建一個畫布,為了將wave的圖片顏色數(shù)據(jù)寫入到Bitmap中
Canvas canvas=new Canvas(bitmap);
//設(shè)置畫布的顏色從而控制文字的著色顏色
canvas.drawColor(Color.RED);
canvas.drawBitmap(originalBitmap,new Matrix(),getPaint());
//CLAMP:使用原來的那張圖片整體
//REPEAT:將原來的圖片復(fù)制無數(shù)份
//MIRROR:鏡像,將原來的圖片鏡像后,寫入,再鏡像...
shader = new BitmapShader(bitmap, Shader.TileMode.REPEAT, Shader.TileMode.CLAMP);
getPaint().setShader(shader);
shaderX = 0;
shaderY = -waveHeight/2;
}
}
接著讓波浪動起來,通過ondraw()方法調(diào)用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();
}
詳細(xì)代碼請參考GitHub倉庫:
https://github.com/zphuanlove/AnimationProject
總結(jié)
該篇文章通過三個案例效果給大家演示了如何實現(xiàn)android下的自定義動畫效果,不過這僅僅只是android自定義動畫的第一種表現(xiàn)形式,接下來還會在下篇文章中繼續(xù)講解第二種形式的展現(xiàn)。
文中的案例在github上均有詳細(xì)以及擴展代碼展示,三個案例匯總到一個項目中。
Thanks!

