自定義控制元件——弄個甜甜圈吧(2): 搭建

羽翼君發表於2017-11-20

【注:】本文首發於簡書,掘金會同步傳送,其餘網站皆無授權。

歡迎瀏覽掘金主頁和簡書主頁,我只是一枚普通的工程師-V-

喜歡自定義控制元件,也喜歡分享我的思路,希望能得到你的批評和建議,也希望能幫到你

【簡書:羽翼君】
github:github.com/razerdp/Ani…

上一篇:《自定義控制元件——弄個甜甜圈吧(1): 起源》


從哪開始?

上一篇,我們初步選定了方案,從這一篇文章開始,我們將會從0開始寫我們的控制元件

在上篇中我提到了我們會經歷一個迷茫,原因就是方向太多,但我們終歸是走過了那個迷茫,只是在大的方向上我們確定了,但是在實施的開始,小方向上仍然好多選擇,比如我是先寫View呢還是先寫介面,還是先寫Bean,還是先寫什麼。。。

所以,從哪開始就是一個問題

如果看過我的朋友圈文集,看過我分享我寫控制元件的思路,應該會看得出,我一般先去寫attrs.xml,也就是先寫屬性,再慢慢的去確定其他的東西。

但是在甜甜圈工程,我並沒有打算寫attrs,所以我會直接從View開始


準備階段

自定義控制元件說白了其實就是讓我們在系統給出的畫布裡(View.onDraw()是空實現)畫出我們所希望的東西,所以如果說自定義控制元件,總是不會忘掉onDraw()這個方法的

在正式畫出來之前,我們需要去考慮我們的畫布尺寸,看看需不需要我們去做測量

在本工程裡,我並不打算去要求大小,因為我只會根據畫布的大小來決定我繪製的半徑,所以onMeasure()/onLayout()這兩個我們直接忽略,不再考慮

因此,我們可以看看我們需要什麼工具(引數):

  1. 畫筆
  2. 資料
  3. 沒了。。。。哈哈

所以,在一開始的階段,我們不妨直搗黃龍,先把甜甜圈畫出來再說。

初次嘗試

畫一個甜甜圈非常簡單,確定好角度,和多個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

下一節,我們將會開始我們的第一個難點:

甜甜圈動畫

下一篇:自定義控制元件——弄個甜甜圈吧(3): 動畫篇【生長動畫】

相關文章