【注:】本文首发于简书,掘金会同步发送,其余网站皆无授权。
欢迎浏览掘金主页和简书主页,我只是一枚普通的工程师-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
下一节,我们将会开始我们的第一个难点:
甜甜圈动画