一直对于自定义view没有进行深入研究过,最近抽空开始整理整理自定义view相关的知识,以便可以有个系统的深入的认识,也希望能够给初学者一点启示。
本篇主要介绍自定义view中一些相关API以及相关知识点。
初识Canvas和Paint
- Paint,画笔类。
常用API如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14//设置画笔的颜色
mPaint.setColor(int color);
//设置填充风格,Paint.Style.FILL(仅填充),Paint.Style.STROKE(仅描边),Paint.Style.FILL_AND_STROKE(同时描边和填充)
mPaint.setStyle();
//设置描边宽度
mPaint.setStrokeWidth()
//设置字体大小
mPaint.setTextSize();
//重置画笔
mPaint.reset();
//设置滤镜
mPaint.setColorFilter();
//设置混合模式
mPaint.setXfermode();
- Canvas ,画布类。
常用API如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33//绘制矩形
canvas.drawRect();
//绘制圆
canvas.drawCircle();
//给画布填充颜色
canvas.drawColor();
//绘制扇形,弧
canvas.drawArc();
//绘制椭圆
canvas.drawOval();
//绘制圆角矩形
canvas.drawRoundRect();
//绘制线条
canvas.drawLine();
//绘制bitmap
canvas.drawBitmap();
//绘制点
canvas.drawPoint();
//绘制路径
canvas.drawPath();
//画文字
canvas.drawText();
//保存画布状态
canvas.save();
//旋转画布
canvas.rotate();
//平移画布
canvas.translate();
//裁切画布
canvas.clipRect()
//恢复画布状态
canvas.restore();
API还是挺多的,但也比较清晰。
自定义View流程
流程一般如下,但由于实际需求不同,并不是每个步骤都需要重写。
- 编写自定义属性
- 在构造函数中获取自定义属性
- 在
onMeasure
中测量宽高,如有需要务必考虑支持padding属性和wrap_content,margin不用管,它是由父布局控制的。 - 重写
onDraw
来进行绘制,以达到我们所需要的效果。
自定义ViewGroup流程
流程一般如下,但由于实际需求不同,并不是每个步骤都需要重写。
- 编写自定义属性
- 在构造函数中获取自定义属性
- 在
onMeasure
中测量宽高,并测量子view宽高。 - 重写
onLayout
来对子View进行布局。 - 如有需要重写
onDraw
来进行添加效果,绝大多数并不需要。
invalidate,postInvalidate和requestLayout之间的区别
invalidate和postInvalidate的区别在于,invalidate只能在UI线程中调用,而postInvalidate可以在线程中调用。
invalidate不能直接在线程中调用,因为他是违背了单线程模型:Android UI操作并不是线程安全的,并且这些操作必须在UI线程中调用。鉴于此,如果在子线程中想刷新界面,需要配合handler来调用invalidate的刷新,这样就比较繁琐。所以,在子线程中调用postInvalidate比较方便。postInvalidate里面会封装一个Message后利用Handler提交到MessageQueue中,由UI线程的Looper从MessageQueue中一一取出进行处理,处理完之后才会进行下一次处理。1
2
3
4
5
6
7
8
9
10
11
12
13
14public void postInvalidate() {
postInvalidateDelayed(0);
}
public void postInvalidateDelayed(long delayMilliseconds) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
attachInfo.mViewRootImpl.dispatchInvalidateDelayed(this, delayMilliseconds);
}
}
public void dispatchInvalidateDelayed(View view, long delayMilliseconds) {
Message msg = mHandler.obtainMessage(MSG_INVALIDATE, view);
mHandler.sendMessageDelayed(msg, delayMilliseconds);//这里使用了Handler进行处理
}invalidate和requestLayout的区别在于,invalidate只刷新draw相关的部分(dispatchDraw,draw,onDraw),requestLayout会刷新measure(measure,onMeasure),layout(layout,onLayout)相关部分。
当一个view调用requestLayout的时候,会给当前的View设置一个FORCE_LAYOUT标记。由此向ViewParent请求布局。这样从这个view开始向上,最终到达ViewRootImpl。然后ViewRootImpl由上往下根据标记位重新布局。
当view确定自身已经不再适合现有的区域时,该view本身调用这个方法要求parent(父类的视图)重新调用他的onMeasure onLayout来重新设置自己位置。特别是当view的layoutparameter发生改变,并且它的值还没能应用到view上时,这时候适合调用这个方法。
ViewConfiguration
1 | //根据设备dpi获取一个最小滑动距离,即大于这个值才判定为滑动 |
VelocityTracker (速度追踪器)
1 | /** |
GestureDetector
初始化监听手势1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53GestureDetector mGestureDetector=new GestureDetector(MainActivity.this, new GestureDetector.OnGestureListener() {
//滑动标准:MotionEvent.ACTION_MOVE的滑动距离大于设备的最小滑动距离就会判定为滑动,参照ViewConfiguration
public boolean onDown(MotionEvent e) {
//触发条件为MotionEvent执行 MotionEvent.ACTION_DOWN,任何时候都会执行
Log.d(TAG,"--onDown"+e.getAction());
return false;
}
public void onShowPress(MotionEvent e) {
//触发条件为MotionEvent执行MotionEvent.ACTION_DOWN,但未达到滑动标准或MotionEvent.ACTION_UP
Log.d(TAG,"--onShowPress"+e.getAction());
}
public boolean onSingleTapUp(MotionEvent e) {
//触发条件为MotionEvent执行MotionEvent.ACTION_UP,,且按下的时间小于长按时间标准且未达到滑动标准,则执行这个,注意onSingleTapUp和onLongPress只执行一个
Log.d(TAG,"--onSingleTapUp"+e.getAction());
return false;
}
public void onLongPress(MotionEvent e) {
//触发条件为MotionEvent执行MotionEvent.ACTION_DOWN,且按下的时间达到了长按时间标准,参照ViewConfiguration,注意onSingleTapUp和onLongPress只执行一个
Log.d(TAG,"--onLongPress"+e.getAction());
}
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
//触发条件为MotionEvent执行 MotionEvent.ACTION_DOWN,且在达到长按标准前达到滑动标准,如果达到长按标准只执行onLongPress,如果达到滑动标准则不会在执行onLongPress
//e1表示MotionEvent.ACTION_DOWN时的事件
//e2表示MotionEvent.ACTION_MOVE时的事件
//distanceX表示两次X轴滑动的距离差
//distanceY表示两次Y轴滑动的距离差
Log.d(TAG,"--onScroll"+e1.getAction()+"/"+e2.getAction()+" "+distanceX+"/"+distanceY);
return false;
}
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
//触发条件为MotionEvent执行 MotionEvent.ACTION_DOWN,且在达到长按时间标准前达到滑动标准后又达到了惯性滑动标准,参照ViewConfiguration
//执行onFling之前一定会执行onScroll,因为只有达到滑动标准后才会进行计算速度
//e1表示MotionEvent.ACTION_DOWN时的事件
//e2表示MotionEvent.ACTION_UP时的事件
//velocityX表示X轴滑动的速度(1000ms)
//velocityX表示Y轴滑动的速度(1000ms)
Log.d(TAG,"--onFling"+e1.getAction()+"/"+e2.getAction()+" "+velocityX+"/"+velocityY);
return false;
}
});
设置监听1
2
3
4
5
6
7
8
9
10
11
12
public boolean onTouchEvent(MotionEvent event) {
return mGestureDetector.onTouchEvent(event);
}
view.setOnTouchListener(new OnTouchListener() {
public boolean onTouch(View view, MotionEvent event) {
return mGestureDetector.onTouchEvent(event);
}
});