【注:】本文首發於簡書,掘金會同步傳送,其餘網站皆無授權。
歡迎瀏覽掘金主頁和簡書主頁,我只是一枚普通的工程師-V-
喜歡自定義控制元件,也喜歡分享我的思路,希望能得到你的批評和建議,也希望能幫到你
從哪開始?
上一篇,我們初步選定了方案,從這一篇文章開始,我們將會從0開始寫我們的控制元件
在上篇中我提到了我們會經歷一個迷茫,原因就是方向太多,但我們終歸是走過了那個迷茫,只是在大的方向上我們確定了,但是在實施的開始,小方向上仍然好多選擇,比如我是先寫View呢還是先寫介面,還是先寫Bean,還是先寫什麼。。。
所以,從哪開始就是一個問題
如果看過我的朋友圈文集,看過我分享我寫控制元件的思路,應該會看得出,我一般先去寫attrs.xml
,也就是先寫屬性,再慢慢的去確定其他的東西。
但是在甜甜圈工程,我並沒有打算寫attrs,所以我會直接從View
開始
準備階段
自定義控制元件說白了其實就是讓我們在系統給出的畫布裡(View.onDraw()是空實現)畫出我們所希望的東西,所以如果說自定義控制元件,總是不會忘掉onDraw()
這個方法的
在正式畫出來之前,我們需要去考慮我們的畫布尺寸,看看需不需要我們去做測量
在本工程裡,我並不打算去要求大小,因為我只會根據畫布的大小來決定我繪製的半徑,所以onMeasure()
/onLayout()
這兩個我們直接忽略,不再考慮
因此,我們可以看看我們需要什麼工具(引數):
- 畫筆
- 資料
- 沒了。。。。哈哈
所以,在一開始的階段,我們不妨直搗黃龍,先把甜甜圈畫出來再說。
初次嘗試
畫一個甜甜圈非常簡單,確定好角度,和多個Paint,通過canvas.drawArc()
就可以完成:
public class AnimatedPieView extends View {
protected final String TAG = this.getClass().getSimpleName();
Paint paint1;
Paint paint2;
Paint paint3;
RectF mDrawRectf=new RectF();
...構造器(略)
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);
}
}複製程式碼
非常簡單,對吧,三支筆,三個角度,完事~
這時候我們就可以叼著煙,架著二郎腿,打個王者,漠視產品:“哥搞定了”
產品:搞定個屁!!!!
再次嘗試
被產品暴打一頓之後,就開始學乖了,同時心裡那股追求完美的那把火也熊熊燃燒
丫的,既然這個不能讓你閉嘴,就寫出一個牛逼點的,乾脆開源
於是,接下來我們陷入了深深的思考中
從上面簡單的幾十行程式碼中,我們不難看出,整個View的核心其實就在於幾個點:
- 畫筆
- 角度
- 半徑
其他的我們也許可以替換,但這三個點是無論如何都無法動搖其三個大哥的根基的
所以考慮到我們要做一個庫而不是去完成什麼簡單的需求,因此就需要考慮擴充套件性的問題了,下面根據這三個核心點去思考
1.1 畫筆
對於一個庫的使用者來說,我最希望的是允許我儘可能多的配置引數,但我又很不喜歡一個View
包含著一大堆的getter/setter
,因為太多的get/set帶來的只會是→選擇困難症,同時,我們使用這個庫也希望侷限性不大,給我們一個比較好的擴充套件性和自由發揮空間。
但是對於庫的創造者來說,我們很明確的知道我們要實現一個效果,需要的什麼引數,但我們又不能去限定開發者們,必須使用我這樣的實體,否則那樣侷限性也太大了。
綜上所述,其實我們設計的時候就需要考慮兩點:
避免太多getter/setter集中在一個View中,如果可以,儘量剝離,這樣View的程式碼不會很多引數,其次也給需要看原始碼的人一個方便,更多的是。。。。為了簡潔清晰
我們無法知道使用者的類裡面的具體引數,但我們知道我們需要什麼引數,所以採取介面約束的形式,是一個很不錯的方法
對於我們的這個甜甜圈工程,我們需要的畫筆,其實從開發者那裡獲取的也就是兩個引數:
- 顏色
- 大小(線寬)
所以,我們不妨定義一個介面,介面裡麵包含著獲取顏色的方法,其他的我們就不管了(線寬等引數不必在這裡限定,因為我們還有config配置類)
public interface IPieInfo {
int getColor();
}複製程式碼
至於開發者怎麼使用他們的類,我們不管,我們只需要保證他們的類有我們需要的顏色引數就好。
其二,針對避免過多的getter/setter,我們其實可以結合builder模式來寫出我們的option(本工程裡稱為config)統一管理
在這裡引用我在github上README寫的使用方法:
AnimatedPieView mAnimatedPieView = findViewById(R.id.animatedPieView);
AnimatedPieViewConfig config = new AnimatedPieViewConfig();
config.setStartAngle(-90)//起始角度偏移
.addData(new SimplePieInfo(30, getColor("FFC5FF8C"), "這是第一段"))//資料(實現IPieInfo介面的bean)
.addData(new SimplePieInfo(18.0f, getColor("FFFFD28C"), "這是第二段"))
...(儘管addData吧)
.setDuration(2000)//持續時間
.setInterpolator(new DecelerateInterpolator(2.5f));//插值器
mAnimatedPieView.applyConfig(config);
mAnimatedPieView.start();複製程式碼
總的來說,我們的庫具體分為兩個部分:
- 渲染的主體(View)
- 渲染的引數配置(config)
1.2 角度
對於一個餅圖,我們當然不會希望我們寫出來的庫像上面例子那樣都限定死每塊120度,否則都不用跳樓gg了,口水都能淹沒你。。。
同時我們也不關心使用者資料結構,所以在1.1的基礎上,我們在介面裡再約束一條:想哥渲染的漂亮不?想就給我一個值~
因此,現在我們的介面變成了這樣:
public interface IPieInfo {
float getValue();
int getColor();
}複製程式碼
有了值,我們就可以計算出這個資料所佔的比例,那麼也就相當於知道了這個資料在甜甜圈中掃描的角度了
在config中,我們用一個list來儲存開發者傳入的資料,並修飾
因此我們的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);
//計算角度
return this;
}
}複製程式碼
然而這裡有個問題,還記得我們傳入的是啥嗎,是一個介面,這個介面我們只管取值
當然,我們可以約束開發者一個setAngle
,只不過這個setAngle
只提供給我們用來把計算的值傳入而已。
如果這樣做。。。你看看開發者會不會給你寄刀片←_←?
所以,我們當然不可以這麼蛋疼啦,但我們又希望有個地方儲存我們計算出來的資料,那該咋辦?
神說:要有光,從此世界有了光
程式設計師說:要有物件,從此,我們習慣了new(kotlin等語言除外哈)
既然我們需要一個地方儲存,那我們就弄個類儲存起來就好啦~
而且這個類只能我們知道,對於外部是不知道的-V-(許可權修飾)
因此,我們再定義一個類:PieInfoImpl
,這個類不可繼承且對外隱藏,這個類對於我們來說相當於包裝,使用者資料被包在裡面,同時新增上我們需要的各種方法,既能保證開發者拿到自己的資料也能保證我們可以懟入我們的資料
因此,我們的類長這樣:
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和其他構造器暫時忽略,以後的文章會描述
}複製程式碼
所以,對開發者可見的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;
}
/**
* 為了區分引數配置和引數計算,這裡用一個內部類來管理
*/
protected final class AnimatedPieViewHelper {
private double sumValue;
private void prepare() {
//計算角度
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 半徑
請讓我喝口水。。。。
然後
輕輕告訴你
往config塞一個半徑吧-V- hhhh
下一節,我們將會開始我們的第一個難點:
甜甜圈動畫