【注:】本文首發(fā)于簡(jiǎn)書,掘金會(huì)同步發(fā)送揩魂,其余網(wǎng)站皆無(wú)授權(quán)幽邓。
歡迎瀏覽掘金主頁(yè)和簡(jiǎn)書主頁(yè),我只是一枚普通的工程師-V-
喜歡自定義控件火脉,也喜歡分享我的思路牵舵,希望能得到你的批評(píng)和建議,也希望能幫到你
上一篇:《自定義控件——弄個(gè)甜甜圈吧(1): 起源》
從哪開(kāi)始倦挂?
上一篇畸颅,我們初步選定了方案,從這一篇文章開(kāi)始方援,我們將會(huì)從0開(kāi)始寫我們的控件
在上篇中我提到了我們會(huì)經(jīng)歷一個(gè)迷茫没炒,原因就是方向太多,但我們終歸是走過(guò)了那個(gè)迷茫犯戏,只是在大的方向上我們確定了送火,但是在實(shí)施的開(kāi)始,小方向上仍然好多選擇先匪,比如我是先寫View呢還是先寫接口种吸,還是先寫B(tài)ean,還是先寫什么呀非。坚俗。。
所以岸裙,從哪開(kāi)始就是一個(gè)問(wèn)題
如果看過(guò)我的朋友圈文集猖败,看過(guò)我分享我寫控件的思路,應(yīng)該會(huì)看得出降允,我一般先去寫attrs.xml
辙浑,也就是先寫屬性,再慢慢的去確定其他的東西拟糕。
但是在甜甜圈工程判呕,我并沒(méi)有打算寫attrs,所以我會(huì)直接從View
開(kāi)始
準(zhǔn)備階段
自定義控件說(shuō)白了其實(shí)就是讓我們?cè)谙到y(tǒng)給出的畫布里(View.onDraw()是空實(shí)現(xiàn))畫出我們所希望的東西送滞,所以如果說(shuō)自定義控件侠草,總是不會(huì)忘掉onDraw()
這個(gè)方法的
在正式畫出來(lái)之前,我們需要去考慮我們的畫布尺寸犁嗅,看看需不需要我們?nèi)プ鰷y(cè)量
在本工程里边涕,我并不打算去要求大小,因?yàn)槲抑粫?huì)根據(jù)畫布的大小來(lái)決定我繪制的半徑,所以onMeasure()
/onLayout()
這兩個(gè)我們直接忽略功蜓,不再考慮
因此园爷,我們可以看看我們需要什么工具(參數(shù)):
- 畫筆
- 數(shù)據(jù)
- 沒(méi)了。式撼。童社。。哈哈
所以著隆,在一開(kāi)始的階段扰楼,我們不妨直搗黃龍,先把甜甜圈畫出來(lái)再說(shuō)美浦。
初次嘗試
畫一個(gè)甜甜圈非常簡(jiǎn)單弦赖,確定好角度,和多個(gè)Paint浦辨,通過(guò)canvas.drawArc()
就可以完成:
public class AnimatedPieView extends View {
protected final String TAG = this.getClass().getSimpleName();
Paint paint1;
Paint paint2;
Paint paint3;
RectF mDrawRectf=new RectF();
...構(gòu)造器(略)
public AnimatedPieView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView(context, attrs);
}
private void initView(Context context, AttributeSet attrs) {
if (paint1 == null) paint1 = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
paint1.setStyle(Paint.Style.STROKE);
paint1.setStrokeWidth(80);
paint1.setColor(Color.RED);
if (paint2 == null) paint2 = new Paint(paint1);
paint2.setColor(Color.GREEN);
if (paint3 == null) paint3 = new Paint(paint1);
paint3.setColor(Color.BLUE);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
final float width = getWidth() - getPaddingLeft() - getPaddingRight();
final float height = getHeight() - getPaddingTop() - getPaddingBottom();
canvas.translate(width / 2, height / 2);
//半徑
final float radius = (float) (Math.min(width, height) / 2 * 0.85);
mDrawRectf.set(-radius, -radius, radius, radius);
canvas.drawArc(mDrawRectf,0,120,false,paint1);
canvas.drawArc(mDrawRectf,120,120,false,paint2);
canvas.drawArc(mDrawRectf,240,120,false,paint3);
}
}
非常簡(jiǎn)單蹬竖,對(duì)吧,三支筆流酬,三個(gè)角度币厕,完事~
這時(shí)候我們就可以叼著煙,架著二郎腿康吵,打個(gè)王者劈榨,漠視產(chǎn)品:“哥搞定了”
產(chǎn)品:搞定個(gè)屁7玫荨;耷丁!拷姿!
再次嘗試
被產(chǎn)品暴打一頓之后惭载,就開(kāi)始學(xué)乖了,同時(shí)心里那股追求完美的那把火也熊熊燃燒
丫的响巢,既然這個(gè)不能讓你閉嘴描滔,就寫出一個(gè)牛逼點(diǎn)的,干脆開(kāi)源
于是踪古,接下來(lái)我們陷入了深深的思考中
從上面簡(jiǎn)單的幾十行代碼中含长,我們不難看出,整個(gè)View的核心其實(shí)就在于幾個(gè)點(diǎn):
- 畫筆
- 角度
- 半徑
其他的我們也許可以替換伏穆,但這三個(gè)點(diǎn)是無(wú)論如何都無(wú)法動(dòng)搖其三個(gè)大哥的根基的
所以考慮到我們要做一個(gè)庫(kù)而不是去完成什么簡(jiǎn)單的需求拘泞,因此就需要考慮擴(kuò)展性的問(wèn)題了,下面根據(jù)這三個(gè)核心點(diǎn)去思考
1.1 畫筆
對(duì)于一個(gè)庫(kù)的使用者來(lái)說(shuō)枕扫,我最希望的是允許我盡可能多的配置參數(shù)陪腌,但我又很不喜歡一個(gè)View
包含著一大堆的getter/setter
,因?yàn)樘嗟膅et/set帶來(lái)的只會(huì)是→選擇困難癥,同時(shí)诗鸭,我們使用這個(gè)庫(kù)也希望局限性不大染簇,給我們一個(gè)比較好的擴(kuò)展性和自由發(fā)揮空間。
但是對(duì)于庫(kù)的創(chuàng)造者來(lái)說(shuō)强岸,我們很明確的知道我們要實(shí)現(xiàn)一個(gè)效果锻弓,需要的什么參數(shù),但我們又不能去限定開(kāi)發(fā)者們请唱,必須使用我這樣的實(shí)體弥咪,否則那樣局限性也太大了。
綜上所述十绑,其實(shí)我們?cè)O(shè)計(jì)的時(shí)候就需要考慮兩點(diǎn):
避免太多getter/setter集中在一個(gè)View中聚至,如果可以,盡量剝離本橙,這樣View的代碼不會(huì)很多參數(shù),其次也給需要看源碼的人一個(gè)方便甚亭,更多的是贷币。。役纹。。為了簡(jiǎn)潔清晰
我們無(wú)法知道用戶的類里面的具體參數(shù)暇唾,但我們知道我們需要什么參數(shù),所以采取接口約束的形式策州,是一個(gè)很不錯(cuò)的方法
對(duì)于我們的這個(gè)甜甜圈工程瘸味,我們需要的畫筆,其實(shí)從開(kāi)發(fā)者那里獲取的也就是兩個(gè)參數(shù):
- 顏色
- 大信苑隆(線寬)
所以,我們不妨定義一個(gè)接口孽糖,接口里面包含著獲取顏色的方法枯冈,其他的我們就不管了(線寬等參數(shù)不必在這里限定办悟,因?yàn)槲覀冞€有config配置類)
public interface IPieInfo {
int getColor();
}
至于開(kāi)發(fā)者怎么使用他們的類,我們不管誉尖,我們只需要保證他們的類有我們需要的顏色參數(shù)就好罪既。
其二,針對(duì)避免過(guò)多的getter/setter琢感,我們其實(shí)可以結(jié)合builder模式來(lái)寫出我們的option(本工程里稱為config)統(tǒng)一管理
在這里引用我在github上README寫的使用方法:
AnimatedPieView mAnimatedPieView = findViewById(R.id.animatedPieView);
AnimatedPieViewConfig config = new AnimatedPieViewConfig();
config.setStartAngle(-90)//起始角度偏移
.addData(new SimplePieInfo(30, getColor("FFC5FF8C"), "這是第一段"))//數(shù)據(jù)(實(shí)現(xiàn)IPieInfo接口的bean)
.addData(new SimplePieInfo(18.0f, getColor("FFFFD28C"), "這是第二段"))
...(盡管addData吧)
.setDuration(2000)//持續(xù)時(shí)間
.setInterpolator(new DecelerateInterpolator(2.5f));//插值器
mAnimatedPieView.applyConfig(config);
mAnimatedPieView.start();
總的來(lái)說(shuō)丢间,我們的庫(kù)具體分為兩個(gè)部分:
- 渲染的主體(View)
- 渲染的參數(shù)配置(config)
1.2 角度
對(duì)于一個(gè)餅圖,我們當(dāng)然不會(huì)希望我們寫出來(lái)的庫(kù)像上面例子那樣都限定死每塊120度驹针,否則都不用跳樓gg了烘挫,口水都能淹沒(méi)你。柬甥。饮六。
同時(shí)我們也不關(guān)心用戶數(shù)據(jù)結(jié)構(gòu),所以在1.1的基礎(chǔ)上苛蒲,我們?cè)诮涌诶镌偌s束一條:想哥渲染的漂亮不卤橄?想就給我一個(gè)值~
因此,現(xiàn)在我們的接口變成了這樣:
public interface IPieInfo {
float getValue();
int getColor();
}
有了值臂外,我們就可以計(jì)算出這個(gè)數(shù)據(jù)所占的比例窟扑,那么也就相當(dāng)于知道了這個(gè)數(shù)據(jù)在甜甜圈中掃描的角度了
在config中,我們用一個(gè)list來(lái)保存開(kāi)發(fā)者傳入的數(shù)據(jù)漏健,并修飾
因此我們的config就可以這樣子寫了:
public class AnimatedPieViewConfig implements Serializable {
private List<IPieInfo> mIPieInfos;
public AnimatedPieViewConfig() {
mIPieInfos=new ArrayList<>();
}
public AnimatedPieViewConfig addData(IPieInfo info){
if (mIPieInfos==null)mIPieInfos=new ArrayList<>();
mIPieInfos.add(info);
//計(jì)算角度
return this;
}
}
然而這里有個(gè)問(wèn)題嚎货,還記得我們傳入的是啥嗎,是一個(gè)接口蔫浆,這個(gè)接口我們只管取值
當(dāng)然殖属,我們可以約束開(kāi)發(fā)者一個(gè)setAngle
,只不過(guò)這個(gè)setAngle
只提供給我們用來(lái)把計(jì)算的值傳入而已瓦盛。
如果這樣做洗显。。谭溉。你看看開(kāi)發(fā)者會(huì)不會(huì)給你寄刀片←_←墙懂?
所以橡卤,我們當(dāng)然不可以這么蛋疼啦扮念,但我們又希望有個(gè)地方保存我們計(jì)算出來(lái)的數(shù)據(jù),那該咋辦碧库?
神說(shuō):要有光柜与,從此世界有了光
程序員說(shuō):要有對(duì)象,從此嵌灰,我們習(xí)慣了new(kotlin等語(yǔ)言除外哈)
既然我們需要一個(gè)地方保存弄匕,那我們就弄個(gè)類保存起來(lái)就好啦~
而且這個(gè)類只能我們知道,對(duì)于外部是不知道的-V-(權(quán)限修飾)
因此沽瞭,我們?cè)俣x一個(gè)類:PieInfoImpl
迁匠,這個(gè)類不可繼承且對(duì)外隱藏,這個(gè)類對(duì)于我們來(lái)說(shuō)相當(dāng)于包裝,用戶數(shù)據(jù)被包在里面城丧,同時(shí)添加上我們需要的各種方法延曙,既能保證開(kāi)發(fā)者拿到自己的數(shù)據(jù)也能保證我們可以懟入我們的數(shù)據(jù)
因此,我們的類長(zhǎng)這樣:
final class PieInfoImpl {
private final String id;
private final IPieInfo mPieInfo;
private float startAngle;
private float endAngle;
public static PieInfoImpl create(IPieInfo info) {
return new PieInfoImpl(info);
}
//getter/setter和其他構(gòu)造器暫時(shí)忽略亡哄,以后的文章會(huì)描述
}
所以枝缔,對(duì)開(kāi)發(fā)者可見(jiàn)的config我們就可以修改了:
public class AnimatedPieViewConfig implements Serializable {
private List<PieInfoImpl> mIPieInfos;
private AnimatedPieViewHelper mPieViewHelper;
public AnimatedPieViewConfig() {
mIPieInfos=new ArrayList<>();
mPieViewHelper=new AnimatedPieViewHelper();
}
public AnimatedPieViewConfig addData(IPieInfo info){
if (mIPieInfos==null)mIPieInfos=new ArrayList<>();
mIPieInfos.add(PieInfoImpl.create(info));
mPieViewHelper.prepare();
return this;
}
/**
* 為了區(qū)分參數(shù)配置和參數(shù)計(jì)算,這里用一個(gè)內(nèi)部類來(lái)管理
*/
protected final class AnimatedPieViewHelper {
private double sumValue;
private void prepare() {
//計(jì)算角度
if (ToolUtil.isListEmpty(mIPieInfos)) return;
sumValue = 0;
//算總和
for (PieInfoImpl dataImpl : mIPieInfos) {
IPieInfo info = dataImpl.getPieInfo();
sumValue += info.getValue();
}
//算每部分的角度
float start = 0;
for (PieInfoImpl data : mIPieInfos) {
data.setStartAngle(start);
float angle = (float) (360.0 * (data.getPieInfo().getValue() / sumValue));
angle = Math.max(1.0f, angle);
float endAngle = start + angle;
data.setEndAngle(endAngle);
start = endAngle;
}
}
public double getSumValue() {
return sumValue;
}
}
}
1.3 半徑
請(qǐng)讓我喝口水蚊惯。愿卸。。截型。
然后
輕輕告訴你
往config塞一個(gè)半徑吧-V- hhhh
下一節(jié)趴荸,我們將會(huì)開(kāi)始我們的第一個(gè)難點(diǎn):
甜甜圈動(dòng)畫