自定义控件——弄个甜甜圈吧(2): 搭建

【注:】本文首发于简书,掘金会同步发送,其余网站皆无授权。

欢迎浏览掘金主页和简书主页,我只是一枚普通的工程师-V-

喜欢自定义控件,也喜欢分享我的思路,希望能得到你的批评和建议,也希望能帮到你

github:https://github.com/razerdp/AnimatedPieView

上一篇:《自定义控件——弄个甜甜圈吧(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): 动画篇【生长动画】

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 139,653评论 20 593
  • 【注:】本文首发于简书,掘金会同步发送,其余网站皆无授权。 欢迎浏览掘金主页和简书主页,我只是一枚普通的工程师-V...
    羽翼君阅读 888评论 0 2
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 88,856评论 13 123
  • ¥开启¥ 【iAPP实现进入界面执行逐一显】 〖2017-08-25 15:22:14〗 《//首先开一个线程,因...
    小菜c阅读 3,942评论 0 15
  • 恒山派见岳不群推三阻四,不顾义气,都心头有气。仪琳道:“令狐师兄,你且在福州养伤,我们去救了师父、师伯回来,再来探...
    littlestupid阅读 28评论 0 1