学习目标
熟悉并掌握自定义 view 实现的细节
废话连篇的我~~~~
Github 是个很强大的学习资源,在上面我们可以找到满足我们大部分需求的框架、控件等。对于新手很容易依赖“拿来主义”,但不太会注意怎么实现的,久而久之,即使工作两三年可能自身的技能并没有提高多少,如果今天的你和昨天的你一样或是比昨天还差,那简直是对生命的浪费啊!
我觉得,持续学习是一个人需要养成的一生的好习惯,这样才对得起自己。
跳过~~~~
Android 上所有的控件都是 view 或 view 的子类,它表示的是屏幕上的一块矩形区域。
Measure
过程:view 会先做一次测量,算出自己需要占用多大的面积。在这个过程中给我们暴露一个onMeasure()
方法,我们可以重新定义 view 的宽高。在其中调用 setMeasuredDimension()
方法,设置 Measure 过程的宽高。
MeasureSpec
是 view 类的一个内部静态类,它定义了三个常量 UNSPECIFIED
、AT_MOST
、EXACTLY
。我们可以这样理解,UNSPECIFIED
,父容器不对 view 有任何限制,要多大有多大,一般用于系统内部测量。EXACTLY
,对应的是 LayoutParams
的 match_parent
和具体数值(xxxdp
),表示父容器已经检测出 view 的大小。AT_MOST
,对应的是 LayoutParams
的 wrap_content
,父容器指定了可用的大小。
Layout
过程:在这个过程中给我们暴露了 onLayout()
方法。
说明,这里说的是 view,没有子 view 需要排列,所以不需要做额外的工作。而在继承ViewGroup
类中, 在 onLayout()
方法中,我们需要将子 view 的大小宽高都设置好。
Draw
过程:就是在 canvas
上画出自己需要的 view 样式。同样 view 给我们暴露了 onDraw()
方法。
requestLayout
:view 重新调用一次 Layout 过程;
invalidate
:view 重新调用一次 Draw 过程;
forceLayout
:标识 view 在下一次重新绘制,需要重新调用 Layout 过程。
除了 view 的一些基本属性,比如 layout_width
,layout_height
,background
等,我们还可以定义自己的属性,具体步骤如下:
attrs.xml
文件,存放全部自定义 view 的属性;declare-styleable
标签下添加控件名称和自定义属性;TypedArray t = context.obtainStyledAttributes(attrs,R.styleable.rainbowbar, 0, 0);
t.recycle();
View 有三个构造方法需要被重写,这里介绍下三个方法被调用的场景。
View view = new View(context)
;inflater
布局时被调用;style
属性设置,这时 inflater
布局时会被调用。而在第一个构造方法里调用第二个构造方法,第二个构造方法里调用第三个构造方法,而在第三个构造方法里读取自定义属性,是为了避免代码冗余。
这是一个类似于谷歌彩虹进度条的 view。
首先在 attrs.xml 文件下新建自定义 view 属性
<declare-styleable name="rainbowba">
<attr name="rainbowbar_rainbowbar_hspace" format="dimension"/>
<attr name="rainbowbar_rainbowbar_vspace" format="dimension"/>
<attr name="rainbowbar_rainbowbar_color" format="color"/>
declare-styleable>
然后新建自定义 view 继承 View
参数说明
int barColor = Color.parseColor("#E6962E");
int barBColor = Color.parseColor("#FF4081");
int hSpace;// 矩形长度
int vSpace;// 矩形框宽度
int space;// 矩形间隔
boolean turnRight = true;
boolean turnLeft = false;
float startX = 0; // 坐标起始点
float delta = 20f;// 每次绘制的偏移量
Paint mPaint;
重写三个构造方法,在第三个构造方法中获取自定义 view 属性,创建画笔
public RainbowBar(Context context) {
this(context,null);
}
public RainbowBar(Context context, @Nullable AttributeSet attrs) {
this(context, attrs,0);
}
public RainbowBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
hSpace = DpPxUtils.dip2px(context,80);
vSpace = DpPxUtils.dip2px(context,10);
space = DpPxUtils.dip2px(context,0);
TypedArray typedArray = context.obtainStyledAttributes(attrs,
R.styleable.rainbowba,0,0);
hSpace = typedArray.getDimensionPixelSize(
R.styleable.rainbowba_rainbowbar_rainbowbar_hspace,hSpace);
vSpace = typedArray.getDimensionPixelSize(
R.styleable.rainbowba_rainbowbar_rainbowbar_vspace,vSpace);
barColor = typedArray.getColor(
R.styleable.rainbowba_rainbowbar_rainbowbar_color,barColor);
typedArray.recycle();
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setStrokeWidth(vSpace);
}
在 onDraw()
方法中具体实现
整体思想就是,调用 canvas 的 drawLine 方法,然后每次将 draw 的起点向前推进。
可能不是很理解,那就把功能分开实现。
怎么让矩形向右推进呢?一般我们在 onDraw()
方法中调用 canvas.drawLine()
方法画出直线来,不用说肯定是不动的。可前面介绍的一个重要方法就是 invalidate()
,它可以重新调用 onDraw() 方法,在每次调用中,我们动态改变上次画的线条的第一个线条的 startX
,这样每次调用 onDraw()
方法时都会向后移,就会出现整个线条在一直向右移动。
注意,当下次调用 onDraw() 方法画线条的第一个线条的 startX 大于屏幕宽度时,我们就要重置到初始位置,不然就一直前进,回不来啦。
// 计算当前坐标起始点有没有在屏幕外面
// delta 表示每次矩形框移动量
float sw = getMeasuredWidth();
if (startX >= sw ) {
startX = 0;
} else {
startX += delta;
}
float start = startX;
// draw latter parse
// 从当前起始点位置开始绘制矩形框,一直绘制到屏幕最右边
// drawline(startX,startY,stopX,stopY,paint),过程不用管 矩形的话,startY 和 stopY 一样就是宽度
// canvas.drawLine(start, vSpace, start + hSpace, vSpace, mPaint);
while (start < sw) {
canvas.drawLine(start, vSpace, start + hSpace, vSpace, mPaint);
start += (hSpace + space); // 下一个起点矩形是上一个起始点坐标加上矩形宽度,设置每个矩形间间隔10
}
就是下面这个效果
啊线条怎么不连续呢?因为每次 onDraw()
使线条前进,可左边空出来的部分越来越大,我们需要在 start(看上面图片画上文字标注的) 位置超出屏幕宽度时,对如果左边有空白处的位置进行填补。
start =startX - (space + hSpace);
解释:画下个矩形的 start = 当前 draw 的第一个矩形的 startX - (矩形间的间隔 + 矩形的宽度)
// 第一个矩形起点左边空出来的部分画上矩形
while (start >= -hSpace) {
canvas.drawLine(start, vSpace, start + hSpace, vSpace, mPaint);
start -= (hSpace + space); // 下一个起点矩形是上一个起始点坐标加上矩形宽度
}
这样就把左边的空白处线条补上了,这个循环语句的条件是当矩形的起点坐标在屏幕左边以左的屏幕外面的一个矩形宽度以内,因为这个范围最多还可以画一个矩形就可以了,其它看不见的范围画了也没有意义。
最后调用 invalidate()
方法就会出现最上面连续线条移动的效果。
在自定义 view 的时候,懂得拆分效果,一步一步实现自己想要的内容。注意几个重要方法的使用,如invalidate()
方法等。自己也许做的不是很好看,但一定要熟悉自定义 view 的设计思路和一些细节问题,这样才能进步。
Github 源码
重要参考:
教你搞定Android自定义View