1. 概述
對(duì)于Drawable掺喻,相信大家都不陌生,而且用起來(lái)非常方便颇玷。在Android中Drawable代表可以在canvas上被繪制的一般抽象,與View或者其子類(lèi)相比就缆,不需要進(jìn)行measure帖渠、layout操作,僅僅只需要進(jìn)行draw操作违崇。
除了簡(jiǎn)單的draw操作之外阿弃,Drawable還提供了許多通用的機(jī)制,以便與正在繪制的圖形進(jìn)行交互:
1> 為了告訴Drawable繪制的位置以及它的大小羞延,setBounds方法必須在繪制之前被調(diào)用。通称⒒梗可以通過(guò)getIntrinsicHeight和getIntrinsicWidth方法找到某些Drawable的首選大小伴箩,比如BitmapDrawable通過(guò)getIntrinsicHeight和getIntrinsicWidth方法獲取bitmap的高和寬。
2> getPadding方法可以從某些Drawables返回關(guān)于如何放置其內(nèi)容的信息鄙漏。 例如嗤谚,一個(gè)想要作為button widget的Drawable將需要返回將label正確放置在其內(nèi)部的padding;LayerDrawable通過(guò)子Drawable的getPadding方法獲取子Drawable的padding怔蚌,從而進(jìn)行padding的積累巩步,后面會(huì)講解LayerDrawable 中padding的積累導(dǎo)致的設(shè)計(jì)缺陷。
3> setState方法可以告知Drawable在哪個(gè)狀態(tài)下繪制桦踊,例如“focus”椅野,“selected”等。一些drawables可以根據(jù)所選狀態(tài)修改其圖像籍胯,比如StateListDrawable竟闪,關(guān)于StateListDrawable的使用可以參考Drawable Resources與Color State List Resource 中的StateListDrawable部分,如果想要知道StateListDrawable中是如何根據(jù)狀態(tài)繪制的杖狼,可以參考StateListDrawable 類(lèi)的onStateChange方法炼蛤。
4> setLevel方法允許提供單個(gè)連續(xù)控制器來(lái)修改Drawable的顯示,例如電池電量蝶涩。 一些drawables可以根據(jù)當(dāng)前l(fā)evel修改其圖像理朋,比如LevelListDrawable絮识,關(guān)于LevelListDrawable的使用可以參考可繪制對(duì)象資源,如果想要知道LevelListDrawable中是如何根據(jù)level繪制的嗽上,可以參考LevelListDrawable類(lèi)的onLevelChange方法次舌。
5> Drawable可以通過(guò)Drawable.Callback接口回調(diào)其client來(lái)執(zhí)行動(dòng)畫(huà)。 所有client都應(yīng)該支持此interface(通過(guò)setCallback)炸裆,以便動(dòng)畫(huà)可以正常工作垃它。 一個(gè)簡(jiǎn)單的方法是通過(guò)系統(tǒng)設(shè)施,如android.view.View#setBackground(Drawable)和 android.widget.ImageView烹看。在下面自定義Drawable的講解中會(huì)詳細(xì)介紹通過(guò)Drawable.Callback怎么實(shí)現(xiàn)動(dòng)畫(huà)国拇。
從API 24開(kāi)始,自定義的Drawable也可以在XML中使用惯殊,考慮到兼容性的問(wèn)題酱吝,該特性目前并沒(méi)有什么用,希望以后Google可以將該特性兼容到低版本土思。
2. 預(yù)備知識(shí)
2.1 Paint中setPathEffect(PathEffect effect)方法的使用
顧名思義务热,PathEffect是用來(lái)對(duì)Paint繪制的Path設(shè)置樣式的,PathEffect本身并沒(méi)有具體實(shí)現(xiàn)己儒,我們通常使用其子類(lèi):
接下來(lái)就來(lái)看看這六個(gè)子類(lèi)的具體效果:
上圖中第一條曲線是原始的Path崎岂,從第二條開(kāi)始依次使用了DashPathEffect、CornerPathEffect闪湾、DiscretePathEffect冲甘、PathDashPathEffect、ComposePathEffect和SumPathEffect途样。
實(shí)現(xiàn)代碼如下所示:
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test_path);
pathView = findViewById(R.id.view_path);
dashPathEffectView = findViewById(R.id.view_dash_path_effect);
cornerPathEffectView = findViewById(R.id.view_corner_path_effect);
discretePathEffectView = findViewById(R.id.view_discrete_path_effect);
pathDashPathEffectView = findViewById(R.id.view_path_dash_path_effect);
composePathEffectView = findViewById(R.id.view_compose_path_effect);
sumPathEffectView = findViewById(R.id.view_sum_path_effect);
Path path = new Path();
path.moveTo(50, 30);
path.lineTo(200, 230);
path.lineTo(400, 30);
path.lineTo(600, 230);
path.lineTo(800, 30);
path.lineTo(1000, 230);
float dashGap = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3, getResources().getDisplayMetrics());
float dashWidth = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3, getResources().getDisplayMetrics());
DashPathEffect dashPathEffect = new DashPathEffect(new float[]{dashGap, dashWidth}, 0);
CornerPathEffect cornerPathEffect = new CornerPathEffect(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10, getResources().getDisplayMetrics()));
DiscretePathEffect discretePathEffect = new DiscretePathEffect(3, 5);
PathDashPathEffect pathDashPathEffect = new PathDashPathEffect(
makeConvexArrow(24.0f, 14.0f), // "stamp"
36.0f, // advance, or distance between two stamps
0.0f, // phase, or offset before the first stamp
PathDashPathEffect.Style.ROTATE); // how to transform each stamp
ComposePathEffect composePathEffect = new ComposePathEffect(dashPathEffect, cornerPathEffect);
SumPathEffect sumPathEffect = new SumPathEffect(cornerPathEffect, dashPathEffect);
TrackShape trackShape = new TrackShape(path, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2, getResources().getDisplayMetrics()));
TrackShapeDrawable trackShapeDrawable = new TrackShapeDrawable(trackShape);
pathView.setBackground(trackShapeDrawable);
trackShape = new TrackShape(path, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2, getResources().getDisplayMetrics()), dashPathEffect);
trackShapeDrawable = new TrackShapeDrawable(trackShape);
dashPathEffectView.setBackground(trackShapeDrawable);
trackShape = new TrackShape(path, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2, getResources().getDisplayMetrics()), cornerPathEffect);
trackShapeDrawable = new TrackShapeDrawable(trackShape);
cornerPathEffectView.setBackground(trackShapeDrawable);
trackShape = new TrackShape(path, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2, getResources().getDisplayMetrics()), discretePathEffect);
trackShapeDrawable = new TrackShapeDrawable(trackShape);
discretePathEffectView.setBackground(trackShapeDrawable);
trackShape = new TrackShape(path, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2, getResources().getDisplayMetrics()), pathDashPathEffect);
trackShapeDrawable = new TrackShapeDrawable(trackShape);
pathDashPathEffectView.setBackground(trackShapeDrawable);
trackShape = new TrackShape(path, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2, getResources().getDisplayMetrics()), composePathEffect);
trackShapeDrawable = new TrackShapeDrawable(trackShape);
composePathEffectView.setBackground(trackShapeDrawable);
trackShape = new TrackShape(path, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2, getResources().getDisplayMetrics()), sumPathEffect);
trackShapeDrawable = new TrackShapeDrawable(trackShape);
sumPathEffectView.setBackground(trackShapeDrawable);
}
private Path makeConvexArrow(float length, float height) {
Path p = new Path();
p.moveTo(0.0f, -height / 2.0f);
p.lineTo(length - height / 4.0f, -height / 2.0f);
p.lineTo(length, 0.0f);
p.lineTo(length - height / 4.0f, height / 2.0f);
p.lineTo(0.0f, height / 2.0f);
p.lineTo(0.0f + height / 4.0f, 0.0f);
p.close();
return p;
}
上面代碼中的TrackShapeDrawable和TrackShape在下面的自定義Drawable中會(huì)詳細(xì)講解江醇,最終PathEffect會(huì)通過(guò)paint.setPathEffect(pathEffect);方式進(jìn)行應(yīng)用。
下面我們來(lái)分析一下PathEffect六個(gè)子類(lèi)的使用方法:
DashPathEffect -- 用來(lái)實(shí)現(xiàn)Path的虛線效果
構(gòu)造方法簽名如下:
public DashPathEffect(float intervals[], float phase)
虛線是由一系列的線段組成何暇,intervals數(shù)組長(zhǎng)度必須是大于等于2的偶數(shù)陶夜,偶數(shù)索引指定線段長(zhǎng)度,而奇數(shù)索引指定線段之間間隔長(zhǎng)度裆站。
例如 PathEffect effects = new DashPathEffect(new float[] { 1, 2, 4, 8}, 1);表示
繪制長(zhǎng)度1的線段,再繪制長(zhǎng)度2的空白,再繪制長(zhǎng)度4的線段,再繪制長(zhǎng)度8的空白,依次重復(fù)条辟。
phase參數(shù)表示起始位置的偏移量(通過(guò)這個(gè)值可以實(shí)現(xiàn)虛線滾動(dòng)的動(dòng)畫(huà))
CornerPathEffect -- 用來(lái)實(shí)現(xiàn)Path拐角處的圓角效果
構(gòu)造方法簽名如下:
public CornerPathEffect(float radius)
1. radius 圓角的半徑
DiscretePathEffect -- 用來(lái)實(shí)現(xiàn)Path的離散效果
構(gòu)造方法簽名如下:
public DiscretePathEffect(float segmentLength, float deviation)
將Path分割成長(zhǎng)度為segmentLength的片段,然后在原始Path的基礎(chǔ)上發(fā)生deviation大小的隨機(jī)偏離
PathDashPathEffect -- 用來(lái)實(shí)現(xiàn)Path的虛線效果(虛線中每一個(gè)線段都用Path對(duì)應(yīng)的圖形代替)
構(gòu)造方法簽名如下:
public PathDashPathEffect(Path shape, float advance, float phase, Style style)
1. shape參數(shù):虛線中每一個(gè)線段對(duì)應(yīng)的Path
2. advance參數(shù):每一個(gè)shape之間的間距
3. phase參數(shù):起始位置的偏移量(通過(guò)這個(gè)值可以實(shí)現(xiàn)虛線滾動(dòng)的動(dòng)畫(huà))
4. style參數(shù):代表每一個(gè)shape之間是如何銜接的遏插,一共有如下三種類(lèi)型:
TRANSLATE 平移的方式
ROTATE 旋轉(zhuǎn)的方式
MORPH 變形的方式
上面三種方式的具體效果捂贿,大家有興趣可以嘗試一下
ComposePathEffect、SumPathEffect -- 用來(lái)將上面提到的PathEffect進(jìn)行組合胳嘲,然后作用于Path上
它們之間不同在于組合的方式厂僧,ComposePathEffect(PathEffect outerpe, PathEffect innerpe)會(huì)先將
innerpe的效果應(yīng)用到Path上,然后再將outerpe的效果應(yīng)用到 應(yīng)用了innerpe效果的Path 上了牛,
即outer(inner(path))颜屠;而SumPathEffect(PathEffect first, PathEffect second)則會(huì)依次應(yīng)用
兩個(gè)效果到Path上辰妙,即 first(path) + second(path)。
2.2 Paint中setShader(Shader shader)方法的使用
顧名思義甫窟,Paint繪制任何對(duì)象(除了bitmap)時(shí)都是通過(guò)Shader得到color值密浑,Shader本身并沒(méi)有具體實(shí)現(xiàn),我們通常使用其子類(lèi):
在講解這五個(gè)子類(lèi)之前粗井,必須先了解一下Shader和Shader中的TileMode:
Shader:前面說(shuō)過(guò)Paint繪制任何對(duì)象(除了bitmap)時(shí)都是通過(guò)Shader得到color值尔破,
Shader獲取指定坐標(biāo)的color值是基于View布局區(qū)域(屏幕區(qū)域中排出狀態(tài)欄區(qū)域和應(yīng)用標(biāo)題欄區(qū)域后
剩余的區(qū)域)對(duì)應(yīng)的坐標(biāo)系。
Shader的原始區(qū)域:也就是Shader子類(lèi)(除了BitmapShader)的漸變區(qū)域浇衬,
BitmapShader的原始區(qū)域就是第一張bitmap繪制的區(qū)域懒构。
CLAMP : 如果繪制區(qū)域超出Shader的原始區(qū)域,則通過(guò)復(fù)制邊緣顏色來(lái)填充超過(guò)的區(qū)域耘擂。
REPEAT : 如果繪制區(qū)域超出Shader的原始區(qū)域胆剧,則通過(guò)在水平和垂直方向重復(fù)Shader的原始區(qū)域
來(lái)填充超過(guò)的區(qū)域。
MIRROR : 和REPEAT類(lèi)似醉冤,不過(guò)是以鏡像的方式重復(fù)秩霍。
注意:
1 BitmapShader可以在水平和垂直方向上同時(shí)設(shè)置TileMode,那么Shader會(huì)先在垂直方向上進(jìn)行
TileMode對(duì)應(yīng)的操作蚁阳,然后再在水平方向上進(jìn)行TileMode對(duì)應(yīng)的操作铃绒。
2 繪制區(qū)域是不可能超過(guò)SweepGradient的原始區(qū)域。
3 LinearGradient和RadialGradient只在一個(gè)方向上進(jìn)行漸變螺捐,因此應(yīng)用一個(gè)TileMode就足夠了匿垄。
4 在創(chuàng)建Shader子類(lèi)的實(shí)例時(shí),TileMode不可以為null归粉,否則會(huì)報(bào)NullPointerException異常。
原理講了這么多漏峰,接下來(lái)我們就通過(guò)例子來(lái)加以驗(yàn)證:
1. BitmapShader
水平和垂直方向上的TileMode都是CLAMP時(shí):
上圖中左上角的圖片就是BitmapShader的原始區(qū)域糠悼。
實(shí)現(xiàn)代碼如下
public class BitmapShaderView extends View {
private Paint mPaint;
public BitmapShaderView(Context context) {
super(context);
}
public BitmapShaderView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public BitmapShaderView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.bitmap_shader);
mPaint.setShader(new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP));
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawRect(0, 0, 1000, 1800, mPaint);
}
}
結(jié)合運(yùn)行截圖和代碼可以證明:
對(duì)于CLAMP,如果繪制區(qū)域超出Shader的原始區(qū)域浅乔,則通過(guò)復(fù)制邊緣顏色來(lái)填充超過(guò)的區(qū)域倔喂。
水平和垂直方向上的TileMode分別是CLAMP,MIRROR:
上圖中左上角的圖片就是BitmapShader的原始區(qū)域靖苇。
實(shí)現(xiàn)代碼和上面基本相同席噩,不同的地方如下:
mPaint.setShader(new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.MIRROR));
結(jié)合運(yùn)行截圖和代碼可以證明:
1> 對(duì)于MIRROR,如果繪制區(qū)域超出Shader的原始區(qū)域贤壁,則通過(guò)在水平和垂直方向以鏡像的方式重復(fù)Shader的原始區(qū)域來(lái)填充超過(guò)的區(qū)域悼枢。
2> BitmapShader可以在水平和垂直方向上同時(shí)設(shè)置TileMode,那么Shader會(huì)先在垂直方向上進(jìn)行TileMode對(duì)應(yīng)的操作脾拆,然后再在水平方向上進(jìn)行TileMode對(duì)應(yīng)的操作
水平和垂直方向上的TileMode都是REPEAT:
上圖中左上角的圖片就是BitmapShader的原始區(qū)域馒索。
實(shí)現(xiàn)代碼和上面基本相同莹妒,不同的地方如下:
mPaint.setShader(new BitmapShader(bitmap, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT));
結(jié)合運(yùn)行截圖和代碼可以證明:
對(duì)于REPEAT,如果繪制區(qū)域超出Shader的原始區(qū)域绰上,則通過(guò)在水平和垂直方向重復(fù)Shader的原始區(qū)域來(lái)填充超過(guò)的區(qū)域旨怠。
2. LinearGradient 線性漸變
上圖第二條傾斜的漸變區(qū)域就是LinearGradient的原始區(qū)域。
實(shí)現(xiàn)代碼與BitmapShader中的基本相同蜈块,不同的地方如下:
mPaint.setShader(new LinearGradient(200, 200, 400, 400, new int[]{Color.RED, Color.YELLOW}, null, Shader.TileMode.REPEAT));
代碼中LinearGradient的前四個(gè)參數(shù)決定了LinearGradient的原始區(qū)域?yàn)樯蠄D第二條傾斜的漸變區(qū)域鉴腻,第五個(gè)參數(shù)代表漸變的顏色值,關(guān)于最后連個(gè)參數(shù)百揭,有興趣的同學(xué)可以自己研究一下爽哎。
3. RadialGradient 徑向漸變
上圖最小的圓區(qū)域就是RadialGradient的原始區(qū)域。
實(shí)現(xiàn)代碼和上面基本相同信峻,不同的地方如下:
mPaint.setShader(new RadialGradient(400, 400, 100, new int[]{Color.RED, Color.YELLOW}, null, Shader.TileMode.REPEAT));
代碼中RadialGradient的前三個(gè)參數(shù)決定了RadialGradient的原始區(qū)域?yàn)樯蠄D最小的圓區(qū)域倦青,第四個(gè)參數(shù)代表漸變的顏色值,關(guān)于最后連個(gè)參數(shù)盹舞,有興趣的同學(xué)可以自己研究一下产镐。
4. SweepGradient 梯度漸變
實(shí)現(xiàn)代碼和上面基本相同,不同的地方如下:
mPaint.setShader(new SweepGradient(400, 400, new int[]{Color.RED, Color.YELLOW}, null));
代碼中SweepGradient的前兩個(gè)參數(shù)決定了SweepGradient的原始區(qū)域?yàn)闊o(wú)限大區(qū)域(從而證明了 繪制區(qū)域是不可能超過(guò)SweepGradient的原始區(qū)域)踢步,第四個(gè)參數(shù)代表漸變的顏色值癣亚,關(guān)于最后一個(gè)參數(shù),有興趣的同學(xué)可以自己研究一下获印。
5. ComposeShader
顧名思義述雾,ComposeShader是用來(lái)組合使用上面的任意兩種Shader。
實(shí)現(xiàn)代碼和上面基本相同兼丰,不同的地方如下:
mPaint.setShader(new ComposeShader(new BitmapShader(bitmap, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT),
new LinearGradient(200, 200, 400, 400, new int[]{Color.RED, Color.TRANSPARENT}, null, Shader.TileMode.REPEAT), PorterDuff.Mode.SRC_OVER));
代碼中ComposeShader的前兩個(gè)參數(shù)代表兩個(gè)被組合的Shader玻孟,第三個(gè)參數(shù)代表組合的方式,首先我們通過(guò)下圖直觀的看一下所有組合的方式:
代碼中ComposeShader的第一個(gè)參數(shù)對(duì)應(yīng)上圖中的Des鳍征,第二個(gè)參數(shù)對(duì)應(yīng)上圖中的Src黍翎,講到這里相信大家就應(yīng)該可以看的懂上面的代碼了。
講到這里艳丛,大家有沒(méi)有這樣的疑問(wèn):怎么對(duì)通過(guò)Shader繪制的圖形進(jìn)行縮放匣掸、平移、旋轉(zhuǎn)和錯(cuò)切操作呢氮双?那么這是就要用到Shader的setLocalMatrix方法了碰酝,使用方式和下面2.3中講到的用法一樣,這里就不再贅敘了戴差。
2.3 Path中transform方法的使用
Path中的transform方法是實(shí)現(xiàn)Path的scale送爸、rotate、translate和skew動(dòng)畫(huà)的關(guān)鍵,下面舉個(gè)例子來(lái)說(shuō)明一下transform方法是如何使用的碱璃,首先看一下運(yùn)行截圖:
實(shí)現(xiàn)代碼如下:
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test_path_transform);
// 用來(lái)顯示坐標(biāo)軸的
coordinateView = findViewById(R.id.view_coordinate);
// 用來(lái)顯示左上角的矩形
testPathTransformView = findViewById(R.id.view_test_path);
// 用來(lái)顯示右上角的矩形
testPathScaleView = findViewById(R.id.view_test_path_scale);
// 用來(lái)顯示左下角的矩形
testPathRotateView = findViewById(R.id.view_test_path_rotate);
// 用來(lái)顯示右下角的矩形
testPathSkewView = findViewById(R.id.view_test_path_skew);
// 構(gòu)建坐標(biāo)軸對(duì)應(yīng)的Path
Path coordinate = new Path();
coordinate.moveTo(0, 100);
coordinate.lineTo(1000, 100);
coordinate.lineTo(980, 80);
coordinate.moveTo(1000, 100);
coordinate.lineTo(980, 120);
coordinate.moveTo(100, 0);
coordinate.lineTo(100, 1000);
coordinate.lineTo(80, 980);
coordinate.moveTo(100, 1000);
coordinate.lineTo(120, 980);
TrackShape coordinateShape = new TrackShape(coordinate, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1, getResources().getDisplayMetrics()));
TrackShapeDrawable coordinateShapeDrawable = new TrackShapeDrawable(coordinateShape);
coordinateView.setBackground(coordinateShapeDrawable);
// 構(gòu)建左上角矩形對(duì)應(yīng)的Path
Path rectPath = new Path();
rectPath.addRect(new RectF(200, 200, 400, 400), Path.Direction.CW);
TrackShape rectTrackShape = new TrackShape(rectPath, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8, getResources().getDisplayMetrics()));
TrackShapeDrawable rectShapeDrawable = new TrackShapeDrawable(rectTrackShape);
rectShapeDrawable.getPaint().setShader(new SweepGradient(300, 300, new int[]{Color.WHITE, 0xFF00C7B2}, null));
testPathTransformView.setBackground(rectShapeDrawable);
Matrix translateMatrix1 = new Matrix();
translateMatrix1.setTranslate(400, 0);
Path scaleRectPath = new Path();
// 將左上角矩形對(duì)應(yīng)的Path向右平移400個(gè)像素
rectPath.transform(translateMatrix1, scaleRectPath);
Matrix scaleMatrix = new Matrix();
scaleMatrix.setScale(0.5f, 0.5f, 700, 300);
// 將平移后得到的Path相對(duì)于矩形的中心點(diǎn)縮小一半
scaleRectPath.transform(scaleMatrix);
TrackShape scaleRectTrackShape = new TrackShape(scaleRectPath, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8, getResources().getDisplayMetrics()));
TrackShapeDrawable scaleRectShapeDrawable = new TrackShapeDrawable(scaleRectTrackShape);
scaleRectShapeDrawable.getPaint().setShader(new SweepGradient(700, 300, new int[]{Color.WHITE, 0xFF00C7B2}, null));
testPathScaleView.setBackground(scaleRectShapeDrawable);
Matrix translateMatrix2 = new Matrix();
translateMatrix2.setTranslate(0, 400);
Path rotateRectPath = new Path();
// 將左上角矩形對(duì)應(yīng)的Path向下平移400個(gè)像素
rectPath.transform(translateMatrix2, rotateRectPath);
Matrix rotateMatrix = new Matrix();
rotateMatrix.setRotate(60, 300, 700);
// 將平移后得到的Path相對(duì)于矩形的中心點(diǎn)旋轉(zhuǎn)180度
rotateRectPath.transform(rotateMatrix);
TrackShape rotateRectTrackShape = new TrackShape(rotateRectPath, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8, getResources().getDisplayMetrics()));
TrackShapeDrawable rotateRectShapeDrawable = new TrackShapeDrawable(rotateRectTrackShape);
rotateRectShapeDrawable.getPaint().setShader(new SweepGradient(300, 700, new int[]{Color.WHITE, 0xFF00C7B2}, null));
testPathRotateView.setBackground(rotateRectShapeDrawable);
Matrix translateMatrix3 = new Matrix();
translateMatrix3.setTranslate(400, 400);
Path skewRectPath = new Path();
// 將左上角矩形對(duì)應(yīng)的Path向上向下分別平移400個(gè)像素
rectPath.transform(translateMatrix3, skewRectPath);
Matrix skewMatrix = new Matrix();
skewMatrix.setSkew(-0.5f, 0, 700, 700);
// 將平移后得到的Path相對(duì)于矩形的中心點(diǎn)在水平方向上錯(cuò)切矩形寬度的一半
skewRectPath.transform(skewMatrix);
TrackShape skewRectTrackShape = new TrackShape(skewRectPath, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8, getResources().getDisplayMetrics()));
TrackShapeDrawable skewRectShapeDrawable = new TrackShapeDrawable(skewRectTrackShape);
skewRectShapeDrawable.getPaint().setShader(new SweepGradient(700, 700, new int[]{Color.WHITE, 0xFF00C7B2}, null));
testPathSkewView.setBackground(skewRectShapeDrawable);
}
activity_test_path_transform.xml代碼如下:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<View
android:id="@+id/view_coordinate"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<View
android:id="@+id/view_test_path"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<View
android:id="@+id/view_test_path_scale"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<View
android:id="@+id/view_test_path_rotate"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<View
android:id="@+id/view_test_path_skew"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
上面的注釋非常清晰了弄痹,這里就不再贅敘了。上面代碼中的TrackShapeDrawable和TrackShape在下面的自定義Drawable中會(huì)詳細(xì)講解嵌器。
3. Drawable繪制過(guò)程源碼分析
前面已經(jīng)講到過(guò)肛真,Drawable與View或者其子類(lèi)相比,不需要進(jìn)行measure爽航、layout操作蚓让,僅僅只需要進(jìn)行draw操作,因此我們只要明白Drawable的繪制過(guò)程讥珍,基本上就可以實(shí)現(xiàn)自定義Drawable历极。Drawable是用來(lái)在View上顯示的,因此View的繪制過(guò)程中就會(huì)繪制View上要顯示的Drawable(即調(diào)用Drawable的onDraw方法)衷佃。在前面的一篇博客Android View的測(cè)量趟卸、布局、繪制流程源碼分析及自定義View實(shí)例演示中講解了View的測(cè)量氏义、布局锄列、繪制流程,希望有興趣的同學(xué)可以看看惯悠,下面我就直接從View的drawBackground方法(該方法繪制View的繪制過(guò)程中被調(diào)用)入手研究Drawable的繪制過(guò)程邻邮,顧名思義,drawBackground方法是用來(lái)繪制View的背景的克婶,首先我們先看一下繪制View的背景時(shí)序圖:
上圖表明繪制View的背景的任務(wù)主要分為兩部分:
1> 設(shè)置View的背景對(duì)應(yīng)的Drawable的大小筒严,即設(shè)置Drawable繪制區(qū)域的大小 (上圖中的1、2情萤、3鸭蛙、4步)
2> 在畫(huà)布上繪制View的背景對(duì)應(yīng)的Drawable (上圖中的5步)
首先看一下上圖中第1步的drawBackground方法的源碼:
private void drawBackground(Canvas canvas) {
final Drawable background = mBackground;
if (background == null) {
return;
}
// 設(shè)置View的背景對(duì)應(yīng)的Drawable的大小,即設(shè)置Drawable繪制區(qū)域的大小
setBackgroundBounds();
// Attempt to use a display list if requested.
if (canvas.isHardwareAccelerated() && mAttachInfo != null
&& mAttachInfo.mHardwareRenderer != null) {
mBackgroundRenderNode = getDrawableRenderNode(background, mBackgroundRenderNode);
final RenderNode renderNode = mBackgroundRenderNode;
if (renderNode != null && renderNode.isValid()) {
setBackgroundRenderNodeProperties(renderNode);
((DisplayListCanvas) canvas).drawRenderNode(renderNode);
return;
}
}
final int scrollX = mScrollX;
final int scrollY = mScrollY;
//當(dāng)沒(méi)有發(fā)生滑動(dòng)時(shí),直接進(jìn)行繪制筋岛,如果發(fā)生了滑動(dòng)偏移规惰,就先將畫(huà)布的坐標(biāo)系進(jìn)行對(duì)應(yīng)的偏移。
if ((scrollX | scrollY) == 0) {
background.draw(canvas);
} else {
canvas.translate(scrollX, scrollY);
background.draw(canvas);
canvas.translate(-scrollX, -scrollY);
}
}
上面的注釋很清楚泉蝌,就不做贅敘了。
接著我們來(lái)看上圖中第2步中setBackgroundBounds方法的源碼:
void setBackgroundBounds() {
if (mBackgroundSizeChanged && mBackground != null) {
// 將View的背景對(duì)應(yīng)的Drawable的大小設(shè)置為View的大小揩晴,即設(shè)置Drawable繪制區(qū)域的大小
mBackground.setBounds(0, 0, mRight - mLeft, mBottom - mTop);
mBackgroundSizeChanged = false;
rebuildOutline();
}
}
上面的注釋很清楚勋陪,就不做贅敘了。
接著我們來(lái)看看第3步中的源碼:
/**
* Specify a bounding rectangle for the Drawable. This is where the drawable
* will draw when its draw() method is called.
*/
public void setBounds(int left, int top, int right, int bottom) {
Rect oldBounds = mBounds;
if (oldBounds == ZERO_BOUNDS_RECT) {
oldBounds = mBounds = new Rect();
}
if (oldBounds.left != left || oldBounds.top != top ||
oldBounds.right != right || oldBounds.bottom != bottom) {
if (!oldBounds.isEmpty()) {
// first invalidate the previous bounds
invalidateSelf();
}
mBounds.set(left, top, right, bottom);
// 在自定義Drawable時(shí)就是通過(guò)重載這個(gè)方法獲取到自定義Drawable的繪制區(qū)域
onBoundsChange(mBounds);
}
}
上面的注釋很清楚硫兰,就不做贅敘了诅愚。
回到第1步的drawBackground方法中,設(shè)置完View的背景對(duì)應(yīng)的Drawable的繪制區(qū)域后(即setBackgroundBounds方法調(diào)用結(jié)束后),接著就是在繪制區(qū)域上繪制圖像(即上圖中的第5步违孝,Drawable的draw方法會(huì)被調(diào)用)刹前,Drawable的draw方法是一個(gè)抽象方法,因此自定義Drawable時(shí)必須實(shí)現(xiàn)該方法:
public abstract void draw(@NonNull Canvas canvas);
4. 自定義Drawable
首先讓大家看看我通過(guò)自定義Drawable實(shí)現(xiàn)的一個(gè)動(dòng)畫(huà):
大多數(shù)開(kāi)發(fā)者者通常第一個(gè)會(huì)想到通過(guò)自定義View來(lái)實(shí)現(xiàn)上面的動(dòng)畫(huà)效果雌桑,雖然可以實(shí)現(xiàn)上面的動(dòng)畫(huà)喇喉,但是通用性不是很好,而且實(shí)現(xiàn)的復(fù)雜度也很高校坑;通過(guò)自定義Drawable的方式實(shí)現(xiàn)上面的動(dòng)畫(huà)效果的好處如下:
- 通用性好:實(shí)現(xiàn)的動(dòng)畫(huà)效果可以適用于View及其子類(lèi)
- 實(shí)現(xiàn)起來(lái)很簡(jiǎn)單:與自定義View相比拣技,不需要進(jìn)行measure、layout操作耍目,僅僅只需要進(jìn)行draw操作
1> 分析上面的動(dòng)畫(huà)
- 動(dòng)畫(huà)的初始狀態(tài):是兩個(gè)圖層疊加的想過(guò)膏斤,底層是一個(gè)50%透明度的白色圓形圖層(后面統(tǒng)一稱(chēng)為A層)茄菊,上層是一個(gè)居中顯示鬧鐘圖片的圖層(后面統(tǒng)一稱(chēng)為B層)朝巫。
- 動(dòng)畫(huà)的中間過(guò)程:A層的透明度變成50%,然后B層中鬧鐘圖片上方顯示了一個(gè)與鬧鐘圖片大小相同的透明度為20%的白色圓形圖層(后面統(tǒng)一稱(chēng)為C層)结啼,然后在C層上面動(dòng)態(tài)的放大一個(gè)與C層上圓形半徑相同的灰色的圓環(huán)(后面統(tǒng)一稱(chēng)為D層)毅访,然后在然后在D層上面動(dòng)態(tài)的繪制一個(gè)與D層上圓形半徑相同的綠色圓環(huán)(后面統(tǒng)一稱(chēng)為E層)沮榜,接著在E層的中心位置動(dòng)態(tài)繪制一個(gè)從小變大的綠色對(duì)勾(后面統(tǒng)一稱(chēng)作F層),接著就是顯示動(dòng)畫(huà)的結(jié)束狀態(tài)俺抽。
- 動(dòng)畫(huà)的結(jié)束狀態(tài):A層的透明度變成0敞映,B層與初始狀態(tài)一樣, C磷斧、D振愿、E、F層不在顯示弛饭。
通過(guò)上面的分析可知冕末,上面的動(dòng)畫(huà)效果要通過(guò)6個(gè)圖層實(shí)現(xiàn),因此可以通過(guò)6個(gè)Drawable的疊加來(lái)實(shí)現(xiàn)侣颂,A档桃、B、C層由于沒(méi)有動(dòng)畫(huà)效果(A層只不過(guò)是在動(dòng)畫(huà)開(kāi)始和結(jié)束時(shí)改變了透明度)憔晒,因此實(shí)現(xiàn)起來(lái)很簡(jiǎn)單藻肄。
2> 實(shí)現(xiàn)A、B拒担、C層
實(shí)現(xiàn)代碼如下所示:
/*
* A層
*/
float radius = DisplayUtils.dip2px(getContext(), 38f);
GradientDrawable bigBgCircle = new GradientDrawable();
float[] radii = new float[]{radius, radius, radius, radius,
radius, radius, radius, radius};
bigBgCircle.setColor(0xAAFFFFFF);
bigBgCircle.setCornerRadii(radii);
/*
* B層
*/
int padding = DisplayUtils.dip2px(getContext(), 19f);
InsetDrawable clock = new InsetDrawable(getResources().getDrawable(R.drawable.ic_clock), padding);
/*
* C層
*/
float radius2 = DisplayUtils.dip2px(getContext(), 19f);
GradientDrawable smallBgCircleCenter = new GradientDrawable();
radii = new float[]{radius2, radius2, radius2, radius2,
radius2, radius2, radius2, radius2};
smallBgCircleCenter.setColor(0xDDFFFFFF);
smallBgCircleCenter.setCornerRadii(radii);
int padding2 = DisplayUtils.dip2px(getContext(), 19f);
InsetDrawable smallBgCircle = new InsetDrawable(smallBgCircleCenter, padding2);
LayerDrawable layerDrawable = new LayerDrawable(new Drawable[]{bigBgCircle, clock, smallBgCircle});
animatorDrawableView.setBackground(layerDrawable);
運(yùn)行截圖如下:
怎么只有A層和B層嘹屯,而沒(méi)有C層,其實(shí)這是LayerDrawable的設(shè)計(jì)缺陷从撼,padding在LayerDrawable中是累積的州弟,也就是說(shuō),在某一層的padding影響所有較高層的bounds,可以通過(guò)LayerDrawable的onBoundsChange方法的源碼看出來(lái):
@Override
protected void onBoundsChange(Rect bounds) {
final ChildDrawable[] array = mLayerState.mChildren;
final int N = mLayerState.mNum;
int padL=0, padT=0, padR=0, padB=0;
for (int i=0; i<N; i++) {
final ChildDrawable r = array[i];
r.mDrawable.setBounds(bounds.left + r.mInsetL + padL,
bounds.top + r.mInsetT + padT,
bounds.right - r.mInsetR - padR,
bounds.bottom - r.mInsetB - padB);
padL += mPaddingL[i];
padR += mPaddingR[i];
padT += mPaddingT[i];
padB += mPaddingB[i];
}
}
因?yàn)镮nsetDrawable用inset作為padding婆翔,因此導(dǎo)致在B層上面的C層中的圓的半徑被縮小了19dp拯杠,因此C層中的圓就不見(jiàn)了。
在API 21的時(shí)候啃奴,Google意識(shí)到了這個(gè)設(shè)計(jì)缺陷潭陪,因此在LayerDrawable中添加了setPaddingMode方法,通過(guò)此方法可以設(shè)置LayerDrawable的PaddingMode為如下兩種:
/**
* 默認(rèn)的PaddingMode纺腊,將每一層嵌入上一層的padding內(nèi)畔咧。
*
* @see #setPaddingMode(int)
*/
public static final int PADDING_MODE_NEST = 0;
/**
* 將每一層直接堆疊在上一層之上。
*
* @see #setPaddingMode(int)
*/
public static final int PADDING_MODE_STACK = 1;
上面的注釋非常清楚揖膜,將LayerDrawable的PaddingMode的PaddingMode設(shè)置為PADDING_MODE_STACK就好了誓沸;雖然這種方法可以解決問(wèn)題,但是遺憾的是壹粟,為了兼容API 21以下的版本拜隧,這個(gè)方法是不可行的,因此為了避免這個(gè)問(wèn)題趁仙,我自定義了一個(gè)PaddingDrawable來(lái)取代InsetDrawable洪添,源碼如下:
public class PaddingDrawable extends Drawable {
/**
* 對(duì)應(yīng)要被設(shè)置padding的drawable
*/
private Drawable drawable = null;
private Paint paint = null;
private Rect padding = null;
public PaddingDrawable(Drawable drawable) {
this.paint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
this.drawable = drawable;
}
public Paint getPaint() {
return paint;
}
public void setPadding(Rect padding) {
if (padding == null) {
return;
}
if (this.padding == null) {
this.padding = new Rect();
}
this.padding.set(padding);
invalidateSelf();
}
@Override
protected void onBoundsChange(Rect bounds) {
super.onBoundsChange(bounds);
if (null == drawable) {
return;
}
if (null == padding) {
drawable.setBounds(bounds);
} else {
drawable.setBounds(bounds.left, bounds.top,
bounds.right - padding.left - padding.right, bounds.bottom - padding.top - padding.bottom);
}
}
@Override
public void draw(@NonNull Canvas canvas) {
canvas.drawColor(Color.TRANSPARENT);
drawDrawable(canvas);
}
private void drawDrawable(@NonNull Canvas canvas) {
if (null == drawable) {
return;
}
if (null != padding) {
canvas.save();
canvas.translate(padding.left, padding.top);
drawable.draw(canvas);
canvas.restore();
} else {
drawable.draw(canvas);
}
}
@Override
public void setAlpha(@IntRange(from = 0, to = 255) int alpha) {
this.paint.setAlpha(alpha);
}
@Override
public void setColorFilter(@Nullable ColorFilter colorFilter) {
this.paint.setColorFilter(colorFilter);
}
@Override
public int getOpacity() {
return PixelFormat.TRANSLUCENT;
}
}
PaddingDrawable是通過(guò)自定義Drawable實(shí)現(xiàn)的,下面會(huì)詳細(xì)介紹自定義Drawable的雀费,因此大家只關(guān)注onBoundsChange和draw方法的實(shí)現(xiàn)就行了干奢,在2中Drawable繪制過(guò)程源碼分析已經(jīng)詳細(xì)介紹了onBoundsChange和draw方法的作用,相信大家應(yīng)該可以理解上面代碼的實(shí)現(xiàn)盏袄;接下來(lái)就是對(duì)上圖的實(shí)現(xiàn)代碼中的InsetDrawable替換成PaddingDrawable忿峻,代碼如下:
/*
* A層和上面相同,這里不再重復(fù)辕羽。
*/
/*
* B層
*/
PaddingDrawable clock = new PaddingDrawable(getResources().getDrawable(R.drawable.ic_clock));
int padding = DisplayUtils.dip2px(getContext(), 19f);
clock.setPadding(new Rect(padding, padding, padding, padding));
/*
* C層
*/
float radius2 = DisplayUtils.dip2px(getContext(), 19f);
GradientDrawable smallBgCircleCenter = new GradientDrawable();
radii = new float[]{radius2, radius2, radius2, radius2,
radius2, radius2, radius2, radius2};
smallBgCircleCenter.setColor(0xDDFFFFFF);
smallBgCircleCenter.setCornerRadii(radii);
PaddingDrawable smallBgCircle = new PaddingDrawable(smallBgCircleCenter);
int padding2 = DisplayUtils.dip2px(getContext(), 19f);
smallBgCircle.setPadding(new Rect(padding2, padding2, padding2, padding2));
LayerDrawable layerDrawable = new LayerDrawable(new Drawable[] {bigBgCircle, clock, smallBgCircle});
animatorDrawableView.setBackground(layerDrawable);
運(yùn)行截圖如下:
C層出現(xiàn)了逛尚,是我們想要的效果。
3> 實(shí)現(xiàn)D刁愿、E绰寞、F層
實(shí)現(xiàn)有動(dòng)畫(huà)效果的D、E铣口、F層就要用到自定義Drawable了滤钱,首先我們應(yīng)該繪制與D、E脑题、F層對(duì)應(yīng)的圓環(huán)和對(duì)勾菩暗,接著就是通過(guò)屬性動(dòng)畫(huà)讓D掏熬、E、F層中的圓環(huán)和對(duì)勾動(dòng)起來(lái)幔嫂。
繪制與D、E片吊、F層對(duì)應(yīng)的圓環(huán)和對(duì)勾
對(duì)于繪制圓環(huán)和對(duì)勾我借鑒了ShapeDrawable做法爷贫,那就是繼承Shape類(lèi)實(shí)現(xiàn)對(duì)于圓環(huán)和對(duì)勾的繪制,代碼如下:
/**
* TrackShape 基本的軌跡圖形類(lèi)
* 通過(guò)軌跡(即Path)來(lái)定義Shape,因此任何Shape的繪制過(guò)程就是繪制一條Path橱赠,
* 即draw方法是一樣的并且在基類(lèi)TrackShape中實(shí)現(xiàn)宰啦。
*/
public class TrackShape extends Shape {
protected Path track = null;
/**
* 代表縮放或者選擇后的track
*/
private Path transformTrack = null;
/**
* 軌跡的厚度
*/
protected float thickness = 0;
/**
* 設(shè)置軌跡為虛線
*/
private PathEffect pathEffect = null;
public TrackShape(Path track, float thickness) {
this(track, thickness, null);
}
public TrackShape(Path track, float thickness, PathEffect pathEffect) {
this.track = track;
this.thickness = thickness;
this.pathEffect = pathEffect;
}
public void scale(float widthScale, float heightScale) {
scale(widthScale, heightScale, getWidth() / 2, getHeight() / 2);
}
/**
* 當(dāng)發(fā)生縮放時(shí)被調(diào)用
*
* @param widthScale 橫向縮放的比例
* @param heightScale 縱向縮放的比例
* @param px 縮放中心點(diǎn)的x軸坐標(biāo)
* @param py 縮放中心點(diǎn)的y軸坐標(biāo)
*/
public void scale(float widthScale, float heightScale, float px, float py) {
if (widthScale < 0) {
return;
}
if (heightScale < 0) {
return;
}
Matrix matrix = new Matrix();
matrix.setScale(widthScale, heightScale, px, py);
if (null == this.transformTrack) {
this.transformTrack = new Path();
}
this.track.transform(matrix, this.transformTrack);
}
public void rotate(float degrees) {
rotate(degrees, getWidth() / 2, getHeight() / 2);
}
/**
* 當(dāng)發(fā)生旋轉(zhuǎn)時(shí)被調(diào)用
*
* @param degrees 旋轉(zhuǎn)的角度
* @param px 旋轉(zhuǎn)中心點(diǎn)的x軸坐標(biāo)
* @param py 旋轉(zhuǎn)中心點(diǎn)的y軸坐標(biāo)
*/
public void rotate(float degrees, float px, float py) {
Matrix matrix = new Matrix();
matrix.setRotate(degrees, px, py);
if (null == this.transformTrack) {
this.transformTrack = new Path();
}
this.track.transform(matrix, this.transformTrack);
}
/**
* 當(dāng)發(fā)生平移時(shí)被調(diào)用
*
* @param dx 橫向平移的距離
* @param dy 縱向平移的距離
*/
public void translate(float dx, float dy) {
Matrix matrix = new Matrix();
matrix.setTranslate(dx, dy);
if (null == this.transformTrack) {
this.transformTrack = new Path();
}
this.track.transform(matrix, this.transformTrack);
}
@Override
public void draw(Canvas canvas, Paint paint) {
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(thickness);
if (null != pathEffect) {
paint.setPathEffect(pathEffect);
}
if (null == transformTrack) {
canvas.drawPath(track, paint);
} else {
canvas.drawPath(transformTrack, paint);
}
}
public Path getTrack() {
return this.track;
}
public void setTrack(Path track) {
this.track = track;
}
}
/**
* 用來(lái)繪制圓環(huán)的軌跡圖形
*/
public class RingTrackShape extends TrackShape {
private float centerX = 0;
private float centerY = 0;
/**
* 內(nèi)圓的半徑
*/
private float innerRadius = 0;
/**
* 弧形起始角度
*/
private float startAngle = 0;
/**
* 弧形掃過(guò)的角度(正數(shù)代表順時(shí)針掃, 負(fù)數(shù)代表逆時(shí)針掃)
*/
private float sweepAngle = 0;
public RingTrackShape(float thickness, float startAngle, float sweepAngle) {
this(thickness, startAngle, sweepAngle, null);
}
public RingTrackShape(float thickness, float startAngle, float sweepAngle, DashPathEffect dashPathEffect) {
super(null, thickness, dashPathEffect);
this.startAngle = startAngle;
this.sweepAngle = sweepAngle;
}
@Override
protected void onResize(float width, float height) {
super.onResize(width, height);
this.centerX = width / 2;
this.centerY = height / 2;
this.innerRadius = Math.min(centerX - thickness, centerY - thickness);
this.track = new Path();
this.track.addArc(new RectF(centerX - innerRadius - thickness/ 2,
centerY - innerRadius - thickness / 2,
centerX + innerRadius + thickness / 2,
centerY + innerRadius + thickness / 2), startAngle, sweepAngle);
}
}
D围肥、E層的圓環(huán)是通過(guò)上面的RingTrackShape繪制的置尔,F(xiàn)層的對(duì)勾是通過(guò)TrackShape繪制的。
通過(guò)屬性動(dòng)畫(huà)讓D氢伟、E榜轿、F層中的圓環(huán)和對(duì)勾動(dòng)起來(lái)
上面提到的TrackShape和RingTrackShape將繪制圓環(huán)和對(duì)勾的過(guò)程進(jìn)行了封裝,然后在自定義的TrackShapeDrawable中對(duì)TrackShape和RingTrackShape進(jìn)行繪制朵锣,說(shuō)到自定義的Drawable谬盐,必須要了解一下Drawable類(lèi):
/**
* 指定drawable的alpha值。 0表示完全透明诚些,255表示完全不透明飞傀。
*/
public abstract void setAlpha(@IntRange(from=0,to=255) int alpha);
/**
* 為drawable指定可選的color filter。
*
* 如果Drawable具有ColorFilter诬烹,則Drawables繪圖內(nèi)容的每個(gè)輸出像素在被混合到
* Canvas的render目標(biāo)之前將被color filter進(jìn)行修改砸烦。
*
* 通過(guò)傳遞null參數(shù)刪除任何現(xiàn)有的color filter。
* 注意: 設(shè)置非null的color filter將使{@link #setTintList(ColorStateList)tint}無(wú)效绞吁。
*
* @param colorFilter 要應(yīng)用的color filter幢痘,或null參數(shù)刪除現(xiàn)有的彩色濾鏡
*/
public abstract void setColorFilter(@Nullable ColorFilter colorFilter);
/**
* 返回Drawable的不透明度。 返回的值是如下常量之一:
* {@link android.graphics.PixelFormat}:
* {@link android.graphics.PixelFormat#UNKNOWN},
* {@link android.graphics.PixelFormat#TRANSLUCENT},
* {@link android.graphics.PixelFormat#TRANSPARENT}, or
* {@link android.graphics.PixelFormat#OPAQUE}.
*
* OPAQUE drawable是在其bounds范圍內(nèi)繪制所有內(nèi)容家破,完全覆蓋了drawable后面的任何內(nèi)容颜说。
* TRANSPARENT drawable是在其bounds范圍內(nèi)不繪制任何內(nèi)容的购岗,可以讓其后面的任何東西顯示出來(lái)。
* TRANSLUCENT drawable是在其bounds范圍內(nèi)繪制一些但不是全部的內(nèi)容门粪,并且至少在drawable后面的一些內(nèi)容將可見(jiàn)藕畔。
* 如果無(wú)法確定可繪制內(nèi)容的可見(jiàn)性,則最安全/最佳返回值為T(mén)RANSLUCENT庄拇。
*
* 一般來(lái)說(shuō),返回值應(yīng)該盡可能保守(使用resolveOpacity方法可以做到)韭邓。
* 例如措近,如果它包含多個(gè)child drawables,并且一次僅顯示其中一個(gè)女淑,如果只有一個(gè)子項(xiàng)是TRANSLUCENT瞭郑,
* 而其他子項(xiàng)是OPAQUE,則應(yīng)返回TRANSLUCENT鸭你。
*
* 請(qǐng)注意屈张,返回的值不一定考慮通過(guò)setAlpha或setColorFilter方法應(yīng)用的自定義Alpha或color filter。
* 某些子類(lèi)(例如 BitmapDrawable袱巨,ColorDrawable和GradientDrawable)確實(shí)存在setAlpha的值阁谆,
* 但一般行為取決于子類(lèi)的實(shí)現(xiàn)。
*
* @return int Drawable的不透明度類(lèi)型愉老。
*
* @see android.graphics.PixelFormat
*/
public abstract @PixelFormat.Opacity int getOpacity();
/**
* 再其邊界內(nèi)繪制(通過(guò)setBounds設(shè)置)场绿,可以使用alpha(通過(guò)setAlpha設(shè)置)和
* color filter(通過(guò)setColorFilter設(shè)置)等可選效果。
*
* @param canvas The canvas to draw into
*/
public abstract void draw(@NonNull Canvas canvas);
接著來(lái)看一下TrackShapeDrawable是如何對(duì)TrackShape和RingTrackShape進(jìn)行繪制的:
/**
* 用于繪制TrackShape的Drawable
*/
public class TrackShapeDrawable extends Drawable {
/**
* 對(duì)應(yīng)要繪制的軌跡圖形
*/
protected TrackShape shape = null;
protected Paint paint;
public TrackShapeDrawable(TrackShape shape) {
this.shape = shape;
this.paint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
}
public Paint getPaint() {
return paint;
}
public Path getTrack() {
if (null == shape) {
return null;
}
return shape.getTrack();
}
public void setTrack(Path track) {
if (null == shape || null == track) {
return;
}
shape.setTrack(track);
invalidateSelf();
}
@Override
protected void onBoundsChange(Rect bounds) {
super.onBoundsChange(bounds);
updateShape(bounds);
}
private void updateShape(Rect bounds) {
if (null != shape) {
shape.resize(bounds.width(), bounds.height());
}
}
public void scale(float widthScale, float heightScale) {
if (null == shape) {
return;
}
shape.scale(widthScale, heightScale);
invalidateSelf();
}
/**
* 當(dāng)發(fā)生縮放時(shí)被調(diào)用
*
* @param widthScale 橫向縮放的比例
* @param heightScale 縱向縮放的比例
* @param px 縮放中心點(diǎn)的x軸坐標(biāo)
* @param py 縮放中心點(diǎn)的y軸坐標(biāo)
*/
public void scale(float widthScale, float heightScale, float px, float py) {
if (null == shape) {
return;
}
shape.scale(widthScale, heightScale, px, py);
invalidateSelf();
}
public void rotate(float degrees) {
if (null == shape) {
return;
}
shape.rotate(degrees);
invalidateSelf();
}
/**
* 當(dāng)發(fā)生旋轉(zhuǎn)時(shí)被調(diào)用
*
* @param degrees 旋轉(zhuǎn)的角度
* @param px 旋轉(zhuǎn)中心點(diǎn)的x軸坐標(biāo)
* @param py 旋轉(zhuǎn)中心點(diǎn)的y軸坐標(biāo)
*/
public void rotate(float degrees, float px, float py) {
if (null == shape) {
return;
}
shape.rotate(degrees, px, py);
invalidateSelf();
}
/**
* 當(dāng)發(fā)生平移時(shí)被調(diào)用
*
* @param dx 橫向平移的距離
* @param dy 縱向平移的距離
*/
public void translate(float dx, float dy) {
if (null == shape) {
return;
}
shape.translate(dx, dy);
invalidateSelf();
}
@Override
public void draw(@NonNull Canvas canvas) {
if (null == shape) {
return;
}
shape.draw(canvas, paint);
}
@Override
public void setAlpha(@IntRange(from = 0, to = 255) int alpha) {
this.paint.setAlpha(alpha);
}
@Override
public void setColorFilter(@Nullable ColorFilter colorFilter) {
this.paint.setColorFilter(colorFilter);
}
@Override
public int getOpacity() {
return PixelFormat.TRANSLUCENT;
}
}
上面代碼中的看出scale嫉入、rotate焰盗、translate、setTrack方法是用來(lái)實(shí)現(xiàn)對(duì)TrackShape和RingTrackShape中的軌跡進(jìn)行scale咒林、rotate熬拒、translate和替換,具體可以參考TrackShape中的scale垫竞、rotate澎粟、translate、setTrack方法件甥。講到這里捌议,大家應(yīng)該有了實(shí)現(xiàn)動(dòng)畫(huà)的思路了吧,就是通過(guò)屬性動(dòng)畫(huà)來(lái)動(dòng)態(tài)調(diào)用TrackShapeDrawable的scale引有、rotate瓣颅、translate、setTrack方法即可譬正。
接下來(lái)為了實(shí)現(xiàn)D宫补、E檬姥、F層中的圓環(huán)和對(duì)勾對(duì)應(yīng)的縮放動(dòng)畫(huà)和軌跡動(dòng)畫(huà),我實(shí)現(xiàn)了ScaleAnimatorDrawable粉怕、TrackAnimatorDrawable兩個(gè)自定義Drawable健民,這兩個(gè)自定義Drawable就是對(duì) 屬性動(dòng)畫(huà)動(dòng)態(tài)調(diào)用TrackShapeDrawable的scale、setTrack方法 的封裝贫贝,其中ScaleAnimatorDrawable不僅支持TrackShapeDrawable也支持其它類(lèi)型的Drawable秉犹,實(shí)現(xiàn)代碼如下:
/**
* 作為自定義動(dòng)畫(huà)Drawable的基類(lèi)
*/
public abstract class AnimatorDrawable extends Drawable implements Animatable, Drawable.Callback, Animator.AnimatorListener {
/**
* 在Drawable繪制區(qū)域的基礎(chǔ)上設(shè)置padding
*/
Rect padding = new Rect();
/**
* 要被動(dòng)畫(huà)的drawable
*/
protected Drawable drawable = null;
private ValueAnimator valueAnimator = null;
/**
* 表示動(dòng)畫(huà)是不是已經(jīng)被啟動(dòng)
*/
private boolean isFirst = true;
/**
* 軌跡動(dòng)畫(huà)的時(shí)長(zhǎng),默認(rèn)200毫秒
*/
protected long duration = 200;
/**
* 軌跡動(dòng)畫(huà)延遲執(zhí)行的時(shí)長(zhǎng),默認(rèn)為0,即不延遲
*/
protected long startDelay = 0;
/**
* 代表動(dòng)畫(huà)執(zhí)行完成之后是否停留在最后一幀
* true 代表停留在最后一幀
* false 代表不會(huì)顯示如何東西
*/
private boolean fillAfter = true;
public AnimatorDrawable(Drawable drawable, long duration) {
this(drawable, duration, true);
}
public AnimatorDrawable(Drawable drawable, long duration, boolean fillAfter) {
if (null == drawable) {
return;
}
this.drawable = drawable;
this.drawable.setCallback(this);// 防止drawable的invalidateSelf方法無(wú)法重繪
if (duration > 0) {
this.duration = duration;
}
this.fillAfter = fillAfter;
}
@Override
protected void onBoundsChange(Rect bounds) {
super.onBoundsChange(bounds);
updateDrawable();
}
private void updateDrawable() {
if (null == drawable) {
return;
}
Rect bounds = getBounds();
if (null == padding) {
drawable.setBounds(bounds);
} else {
drawable.setBounds(bounds.left, bounds.top,
bounds.right - padding.left - padding.right, bounds.bottom - padding.top - padding.bottom);
}
}
/**
* Sets padding for this shape, defined by a Rect object. Define the padding
* in the Rect object as: left, top, right, bottom.
*/
public void setPadding(Rect padding) {
if (padding == null) {
return;
}
this.padding = padding;
updateDrawable();
invalidateSelf();
}
@Override
public void draw(@NonNull Canvas canvas) {
if (isFirst) {
start();
isFirst = false;
} else {
if (isRunning() || null == valueAnimator) {
onDraw(canvas);
}
}
}
public void onDraw(@NonNull Canvas canvas) {
if (null != padding) {
canvas.save();
canvas.translate(padding.left, padding.top);
canvas.clipRect(0, 0, drawable.getBounds().width(), drawable.getBounds().height());
drawTrack(canvas);
canvas.restore();
} else {
canvas.save();
drawTrack(canvas);
canvas.restore();
}
}
public abstract void drawTrack(@NonNull Canvas canvas);
/**
* 獲取軌跡動(dòng)畫(huà)的時(shí)長(zhǎng)
*/
public long getDuration() {
return duration;
}
public void setStartDelay(long startDelay) {
if (startDelay < 0) {
return;
}
this.startDelay = startDelay;
}
public long getStartDelay() {
return startDelay;
}
void setValueAnimator(ValueAnimator valueAnimator) {
if (null == valueAnimator) {
this.valueAnimator.cancel();
this.valueAnimator = null;
} else {
this.valueAnimator = valueAnimator;
}
}
@Override
public void start() {
if (null != valueAnimator && (valueAnimator.isRunning() || valueAnimator.isStarted())) {
valueAnimator.cancel();
valueAnimator = null;
}
setupAnimators();
if (null != valueAnimator) {
valueAnimator.start();
}
}
public abstract void setupAnimators();
@Override
public void stop() {
if (null != valueAnimator && (valueAnimator.isRunning() || valueAnimator.isStarted())) {
valueAnimator.cancel();
}
}
@Override
public boolean isRunning() {
return null != valueAnimator && valueAnimator.isRunning();
}
@Override
public void invalidateDrawable(@NonNull Drawable who) {
invalidateSelf();
}
@Override
public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) {
scheduleSelf(what, when);
}
@Override
public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) {
unscheduleSelf(what);
}
@Override
public void onAnimationStart(Animator animation) {
if (null != this.onAnimatorListener) {
this.onAnimatorListener.onAnimationStart(this);
}
}
@Override
public void onAnimationEnd(Animator animation) {
valueAnimator.removeAllUpdateListeners();
valueAnimator.removeAllListeners();
if (fillAfter) {
setValueAnimator(null);
}
if (null != this.onAnimatorListener) {
this.onAnimatorListener.onAnimationEnd(this);
}
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
@Override
public void setAlpha(@IntRange(from = 0, to = 255) int alpha) {}
@Override
public void setColorFilter(@Nullable ColorFilter colorFilter) {}
@Override
public int getOpacity() {
return PixelFormat.TRANSLUCENT;
}
private OnAnimatorListener onAnimatorListener = null;
public void setOnAnimatorListener(OnAnimatorListener onAnimatorListener) {
this.onAnimatorListener = onAnimatorListener;
}
public interface OnAnimatorListener {
void onAnimationStart(AnimatorDrawable animatorDrawable);
void onAnimationEnd(AnimatorDrawable animatorDrawable);
}
}
/**
* 對(duì)Drawable進(jìn)行Scale動(dòng)畫(huà)
*/
public class ScaleAnimatorDrawable extends AnimatorDrawable {
/**
* 代表Drawable縮放的比例
*/
private float scale = 0;
private Rect initPadding = new Rect();
public ScaleAnimatorDrawable(Drawable drawable, long duration) {
super(drawable, duration);
}
@Override
public void setupAnimators() {
// 在啟動(dòng)動(dòng)畫(huà)之前記錄最開(kāi)始的padding
initPadding = padding;
final ValueAnimator valueAnimator = ValueAnimator.ofFloat(0f, 1f);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
scale = (float) animation.getAnimatedValue();
preDraw();
}
});
valueAnimator.addListener(this);
valueAnimator.setDuration(duration).setStartDelay(startDelay);
setValueAnimator(valueAnimator);
}
private void preDraw() {
if (drawable instanceof TrackShapeDrawable) {
((TrackShapeDrawable) drawable).scale(scale, scale);
} else {
Rect bounds = getBounds();
setPadding(new Rect((int) (initPadding.left + (bounds.width() - initPadding.left - initPadding.right) * 0.5 * (1 - scale)),
(int) (initPadding.top + (bounds.height() - initPadding.top - initPadding.bottom) * 0.5 * (1 - scale)),
(int) (initPadding.right + (bounds.width() - initPadding.left - initPadding.right) * 0.5 * (1 - scale)),
(int) (initPadding.bottom + (bounds.height() - initPadding.top - initPadding.bottom) * 0.5 * (1 - scale))));
}
}
@Override
public void drawTrack(@NonNull Canvas canvas) {
drawable.draw(canvas);
}
}
/**
* 對(duì)TrackShapeDrawable進(jìn)行軌跡動(dòng)畫(huà)
*/
public class TrackAnimatorDrawable extends AnimatorDrawable {
/**
* 代表將要被繪制的軌跡片段
*/
private Path trackSegment = null;
public TrackAnimatorDrawable(TrackShapeDrawable drawable, long duration) {
super(drawable, duration);
}
@Override
public void setupAnimators() {
final ValueAnimator valueAnimator = ValueAnimator.ofObject(new PathEvaluator(), new Path(), ((TrackShapeDrawable) drawable).getTrack());
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
trackSegment = (Path) animation.getAnimatedValue();
preDraw();
}
});
valueAnimator.addListener(this);
valueAnimator.setDuration(duration).setStartDelay(startDelay);
setValueAnimator(valueAnimator);
trackSegment = new Path();
}
private void preDraw() {
((TrackShapeDrawable) drawable).setTrack(trackSegment);
}
@Override
public void drawTrack(@NonNull Canvas canvas) {
drawable.draw(canvas);
}
}
接著就是使用上面自定義的ScaleAnimatorDrawable稚晚、TrackAnimatorDrawable實(shí)現(xiàn)動(dòng)畫(huà)效果了:
// A層除了顏色值變?yōu)?xAAFFFFFF其它和2>中相同崇堵,B層也和2>中相同,這里不再贅敘客燕。
// C層
float radius2 = DisplayUtils.dip2px(this, 19f);
GradientDrawable smallBgCircleCenter = new GradientDrawable();
radii = new float[]{radius2, radius2, radius2, radius2,
radius2, radius2, radius2, radius2};
smallBgCircleCenter.setColor(0xDDFFFFFF);
smallBgCircleCenter.setCornerRadii(radii);
PaddingDrawable smallBgCircle = new PaddingDrawable(smallBgCircleCenter);
int padding3 = DisplayUtils.dip2px(this, 19f);
smallBgCircle.setPadding(new Rect(padding3, padding3, padding3, padding3));
// D層
ScaleAnimatorDrawable scaledCircle = (ScaleAnimatorDrawable) AnimatorDrawableFactory.createAnimatorDrawable(this, getResources().getDrawable(R.drawable.ic_drawable_ring), AnimatorDrawableFactory.AnimatorDrawableType.SCALE);
int padding4 = DisplayUtils.dip2px(this, 19f);
scaledCircle.setPadding(new Rect(padding4, padding4, padding4, padding4));
// E層
TrackShapeDrawable scaleRingDrawable = new TrackShapeDrawable(new RingTrackShape(DisplayUtils.dip2px(this, 2), 0, 360));
scaleRingDrawable.getPaint().setColor(Color.GREEN);
TrackAnimatorDrawable trackAnimatorDrawable = (TrackAnimatorDrawable) AnimatorDrawableFactory.createAnimatorDrawable(this, scaleRingDrawable, AnimatorDrawableFactory.AnimatorDrawableType.TRACK);
int padding5 = DisplayUtils.dip2px(this, 20f);
trackAnimatorDrawable.setPadding(new Rect(padding5, padding5, padding5, padding5));
// F層
Path track = new Path();
track.moveTo(0f, DisplayUtils.dip2px(this, 10f));
track.lineTo(DisplayUtils.dip2px(this, 10f), DisplayUtils.dip2px(this, 20f));
track.lineTo(DisplayUtils.dip2px(this, 20f), 0f);
TrackShape trackShape = new TrackShape(track, DisplayUtils.dip2px(this, 2));
TrackShapeDrawable trackShapeDrawable3 = new TrackShapeDrawable(trackShape);
trackShapeDrawable3.getPaint().setColor(Color.GREEN);
ScaleAnimatorDrawable scaleAnimatorDrawable = (ScaleAnimatorDrawable) AnimatorDrawableFactory.createAnimatorDrawable(this, trackShapeDrawable3, AnimatorDrawableFactory.AnimatorDrawableType.SCALE);
int padding6 = DisplayUtils.dip2px(this, 28f);
scaleAnimatorDrawable.setPadding(new Rect(padding6, padding6, padding6, padding6));
TrackAnimatorManager trackAnimatorManager = new TrackAnimatorManager();
trackAnimatorManager.setOnAnimatorListener(this);
LayerDrawable layerDrawable = trackAnimatorManager.createLayerDrawable(TrackAnimatorManager.Order.SEQUENTIALLY,
bigBgCircle, clock, smallBgCircle, scaledCircle, trackAnimatorDrawable, scaleAnimatorDrawable);
animatorDrawableView.setBackground(layerDrawable);
上面代碼中的trackAnimatorManager.createLayerDrawable是將多個(gè)Drawable放到一個(gè)LayerDrawable中鸳劳,如果其中有AnimatorDrawable的子類(lèi)就會(huì)將其按照其在數(shù)組中順序依次進(jìn)行動(dòng)畫(huà),實(shí)現(xiàn)代碼如下:
public LayerDrawable createLayerDrawable(Order order, Drawable... drawables) {
if (null == drawables || drawables.length <= 0) {
return null;
}
this.drawables = drawables;
LayerDrawable layerDrawable = new LayerDrawable(this.drawables);
int startDelay = 0;
for (Drawable drawable : this.drawables) {
if (drawable instanceof AnimatorDrawable) {
startDelay += ((AnimatorDrawable) drawable).getStartDelay();
((AnimatorDrawable) drawable).setOnAnimatorListener(this);
switch (order) {
case SEQUENTIALLY: {
((AnimatorDrawable) drawable).setStartDelay(startDelay);
break;
}
}
startDelay += ((AnimatorDrawable) drawable).getDuration();
}
}
return layerDrawable;
}