Android 自定义View的探索

注意

参考内容
http://www.gcssloop.com/
http://hencoder.com/
《Android开发艺术探索》

invalidate(); //重新绘制

绘图

Paint 画笔

paint.setAntiAlias(true);//抗锯齿
paint.setColor(Color.RED);//颜色
paint.setStyle(Paint.Style.STROKE); // Style 修改为画线模式 FILL 是填充模式,STROKE 是画线模式(即勾边模式),FILL_AND_STROKE 是两种模式一并使用:既画线又填充。它的默认值是 FILL,填充模式。
paint.setStrokeWidth(20); //STROKE 和 FILL_AND_STROKE 下,还可以使用 paint.setStrokeWidth(float width) 来设置线条的宽度:
paint.setTextSize(84);//设置字体大小
paint.setTypeface();//设置字体样式
paint.setStrokeCap(cap) //设置线冒样式 Cap.BUTT(无线帽)、Cap.SQUARE(方形线帽)、Cap.ROUND(圆形线冒)
setShader(Shader shader)

Shader 注意:在设置了 Shader 的情况下, Paint.setColor/ARGB() 所设置的颜色就不再起作用。
LinearGradient 线性渐变
Shader shader = new LinearGradient(float x0, float y0, float x1, float y1, int colors[], float positions[], TileMode tile)
第一个参数为线性起点的x坐标
第二个参数为线性起点的y坐标
第三个参数为线性终点的x坐标
第四个参数为线性终点的y坐标
第五个参数为实现渐变效果的颜色的组合
第六个参数为前面的颜色组合中的各颜色在渐变中占据的位置(比重),如果为空,则表示上述颜色的集合在渐变中均匀出现
第七个参数为渲染器平铺的模式,一共有三种
-CLAMP
边缘拉伸
-REPEAT
在水平和垂直两个方向上重复,相邻图像没有间隙
-MIRROR
以镜像的方式在水平和垂直两个方向上重复,相邻图像有间隙
LinearGradient(float x0, float y0, float x1, float y1, int color0, int color1, TileMode tile)
int color0表示渐变起始颜色
int color1表示渐变终止颜色

Canvas 画布

改变坐标
//移动坐标原点
canvas.translate(x, y);//移动坐标原点,位移是基于当前位置移动
//缩放和翻转 可叠加
canvas.scale(float sx, float sy)
canvas.scale(float sx, float sy, float px, float py);//(缩放x轴比例,缩放y轴比例,缩放中心点x,缩放中心点y) 缩放x轴比例,缩放y轴比例为负数会图形根据中心点翻转, 缩放中心点x,y,缩放中心点代表图形向哪个点缩或者放。缩放是可以叠加的
//旋转 可叠加
rotate(float degrees);//旋转角度
rotate (float degrees, float px, float py)//旋转角度degrees 旋转中心点px,py

保存快照和回退
http://www.gcssloop.com/customview/Canvas_Convert
save();
save (int saveFlags);
回退
restore();
restoreToCount();//如果调用restoreToCount(2) 则会弹出 2 3 4 5 的状态,并根据第2次保存的状态进行恢复。

//错切 可叠加
skew (float sx, float sy)
float sx:将画布在x方向上倾斜相应的角度,sx倾斜角度的tan值,
float sy:将画布在y轴方向上倾斜相应的角度,sy为倾斜角度的tan值.

//绘制画布颜色
canvas.drawColor(Color.parseColor(“#88880000”));(半透明颜色会加遮罩)另外有 drawRGB(int r, int g, int b) 和 drawARGB(int a, int r, int g, int b)
这类颜色填充方法一般用于在绘制之前设置底色,或者在绘制之后为界面设置半透明蒙版。

//画点
drawPoint(float x, float y, Paint paint)//(点x坐标,点y坐标, Paint paint) //点的大小可以通过 paint.setStrokeWidth(width) 来设置;点的形状可以通过 paint.setStrokeCap(cap) 来设置:ROUND 画出来是圆形的点,SQUARE 或 BUTT 画出来是方形的点。
// 和FILL 模式下的 drawCircle() 和 drawRect()一样

//批量画点

float[] points = {0, 0, 50, 50, 50, 100, 100, 50, 100, 100, 150, 50, 150, 100};
drawPoints(float[] pts, int offset, int count, Paint paint) / drawPoints(float[] pts, Paint paint) //offset偏移量 点数量
//canvas.drawPoints(points, 2 / 跳过两个数,即前两个 0 /, 8 / 一共绘制 8 个数(4 个点)/, paint);

//画线
drawLine(200, 200, 800, 500, paint); //drawLine(左边x轴起点, 上边y轴起点 右边x轴终点, 下边y轴终点, Paint paint)

//批量划线
float[] points = {20, 20, 120, 20, 70, 20, 70, 120, 20, 120, 120, 120, 150, 20, 250, 20, 150, 20, 150, 120, 250, 20, 250, 120, 150, 120, 250, 120};
drawLines(points, paint);

//画矩形 支持RectF
drawRect(float left, float top, float right, float bottom, Paint paint) ; ( 左边x轴起点, 上边y轴起点 右边x轴终点, 下边y轴终点, Paint paint)

//圆角矩形 支持RectF
drawRoundRect(左边x轴起点, 上边y轴起点 右边x轴终点, 下边y轴终点, float rx, float ry, Paint paint) //rx 和 ry 是圆角的横向半径和纵向半径。

//画圆
drawCircle(float centerX, float centerY, float radius, Paint paint) 画圆 (圆心x坐标,圆心y坐标,半径,画笔)

//画椭圆
drawOval(float left, float top, float right, float bottom, Paint paint) //( 左边x轴起点, 上边y轴起点 右边x轴终点, 下边y轴终点, Paint paint)
//绘制弧形或扇形
drawArc(float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean useCenter, Paint paint)
//drawArc() 是使用一个椭圆来描述弧形的 startAngle 是弧形的起始角度(x 轴的正向,即正右的方向,是 0 度的位置;顺时针为正角度,逆时针为负角度),sweepAngle 是弧形划过的角度;
//useCenter 表示是否连接到圆心,如果不连接到圆心,就是弧形,如果连接到圆心,就是扇形。

画 Bitmap
drawBitmap(Bitmap bitmap, float left, float top, Paint paint) 画 Bitmap
drawBitmap(Bitmap bitmap, Rect src, RectF dst, Paint paint) /
drawBitmap(Bitmap bitmap, Rect src, Rect dst, Paint paint) / //Rect src 指定绘制图片的区域, Rect dst 指定图片在屏幕上显示的区域
drawBitmap(Bitmap bitmap, Matrix matrix, Paint paint)
绘制文字
drawText(String text, float x, float y, Paint paint) 绘制文字
drawText(str,1,3,200,500,textPaint);// (字符串 开始截取位置 结束截取位置 基线x 基线y 画笔)
指定字符位置
drawPosText(str,new float[]{
100,100, // 第一个字符位置
200,200, // 第二个字符位置
300,300, // …
400,400,
500,500
},textPaint);
drawPosText (char[] text, int index, int count, float[] pos, Paint paint)

//画自定义图形 通过描述路径的方式来绘制图形
canvas.drawPath(path, paint);

描述路径Path

Path path = new Path();

图形交叉的填充方式

//setFillType(Path.FillType ft) 设置填充方式
//Path.FillType.WINDING 默认值(全填充)
//Path.FillType.EVEN_ODD 交叉填充
//带有 INVERSE 前缀是反色


路径方向(Direction dir)

Path.Direction.CW //顺时针
Path.Direction.CCW //逆时针

添加图形

addCircle(float centerX, float centerY, float radius, Direction dir) 添加圆形
addOval(float left, float top, float right, float bottom, Direction dir) / addOval(RectF oval, Direction dir) 添加椭圆
addRect(float left, float top, float right, float bottom, Direction dir) / addRect(RectF rect, Direction dir) 添加矩形
addRoundRect(RectF rect, float rx, float ry, Direction dir) / addRoundRect(float left, float top, float right, float bottom, float rx, float ry, Direction dir)
/ addRoundRect(RectF rect, float[] radii, Direction dir) / addRoundRect(float left, float top, float right, float bottom, float[] radii, Direction dir) 添加圆角矩形

添加线条

lineTo(float x, float y) (绝对坐标 开始坐标为 0,0) / rLineTo(float x, float y) (相对坐标,指最后 调用画 Path 的方法的终点位置)

quadTo(float x1, float y1, float x2, float y2)
/ rQuadTo(float dx1, float dy1, float dx2, float dy2) 画二次贝塞尔曲线
这条二次贝塞尔曲线的起点就是当前位置,而参数中的 x1, y1 和 x2, y2 则分别是控制点和终点的坐标。和 rLineTo(x, y) 同理,rQuadTo(dx1, dy1, dx2, dy2) 的参数也是相对坐标

cubicTo(float x1, float y1, float x2, float y2, float x3, float y3)
/ rCubicTo(float x1, float y1, float x2, float y2, float x3, float y3) 画三次贝塞尔曲线

移动坐标

moveTo(float x, float y) / rMoveTo(float x, float y) 移动到目标位置

setLastPoint(x,y); //重置上一次操作的最后一个点

画弧形

arcTo(RectF oval, float startAngle, float sweepAngle, boolean forceMoveTo) /
arcTo(float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean forceMoveTo) /
arcTo(RectF oval, float startAngle, float sweepAngle) 画弧形 forceMoveTo为true表示不拖笔 false为拖笔

//默认不拖笔
addArc(float left, float top, float right, float bottom, float startAngle, float sweepAngle) /
addArc(RectF oval, float startAngle, float sweepAngle)

平移

offset (float dx, float dy)
offset (float dx, float dy, Path dst) Path dst 把平移的Path保存给dst 会覆盖dst原有的 同时最开始调用 path.offset 不会修改path
//重置
path.reset();

添加另一个path

addPath(Path path)
封闭图形 (将起始点和结束点连接)
path.close();
不是所有的子图形都需要使用 close() 来封闭。当需要填充图形时(即 Paint.Style 为 FILL 或 FILL_AND_STROKE),Path 会自动封闭子图形。

PathMeasure 测量path

关联path
PathMeasure ()
用这个构造函数可创建一个空的 PathMeasure,但是使用之前需要先调用 setPath 方法来与 Path 进行关联。被关联的 Path 必须是已经创建好的,如果关联之后 Path 内容进行了更改,则需要使用 setPath 方法重新关联。

PathMeasure (Path path, boolean forceClosed)
用这个构造函数是创建一个 PathMeasure 并关联一个 Path, 其实和创建一个空的 PathMeasure 后调用 setPath 进行关联效果是一样的,同样,被关联的 Path 也必须是已经创建好的,如果关联之后 Path 内容进行了更改,则需要使用 setPath 方法重新关联。不论 forceClosed 设置为何种状态(true 或者 false), 都不会影响原有Path显示的状态,设置闭合后如果是非闭合path则测量的长度为会加上闭合的长度。

该方法有两个参数,第一个参数自然就是被关联的 Path 了,第二个参数是用来确保 Path 闭合,如果设置为 true, 则不论之前Path是否闭合,都会自动闭合该 Path(如果Path可以闭合的话)。
setPath 是 PathMeasure 与 Path 关联的重要方法,效果和 构造函数 中两个参数的作用是一样的。
isClosed 用于判断 Path 是否闭合,但是如果你在关联 Path 的时候设置 forceClosed 为 true 的话,这个方法的返回值则一定为true。
测量
测量 默认是选取path最外层的曲线
float getLength(); 用于获取 Path 的总长度。
boolean getSegment (float startD, float stopD, Path dst, boolean startWithMoveTo) 截取Path的一个片段 返回true为截取成功 startD为开始长度 stopD为结束长度 要注意path的方向 startWithMoveTo 是否保留截取的起点的位置,false就会把截取的path的起点放到dst的终点 有可能导致截取的path形变。
boolean nextContour(); 跳转到path的下一条曲线 成功返回true

boolean getPosTan (float distance, float[] pos, float[] tan)得到路径上某一长度的位置以及该位置的正切值 成功返回true distance距离 Path 起点的长度 pos[0]为x坐标 pos[1]为y坐标 tan[0]是邻边边长,tan[1]是对边边长 邻边对边是由pos的坐标与坐标轴原点连接构成一个直角三角形得到的
Math.atan2(tan[1], tan[0]) 获取到正切角的弧度值
(float) (Math.atan2(tan[1], tan[0]) * 180.0 / Math.PI) 弧度转角度

录制Canvas

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
// 1.创建Picture
private Picture mPicture = new Picture();
---------------------------------------------------------------
// 2.录制内容方法
private void recording() {
// 开始录制 (接收返回值Canvas)
Canvas canvas = mPicture.beginRecording(500, 500);
// 创建一个画笔
Paint paint = new Paint();
paint.setColor(Color.BLUE);
paint.setStyle(Paint.Style.FILL);
// 在Canvas中具体操作
// 位移
canvas.translate(250,250);
// 绘制一个圆
canvas.drawCircle(0,0,100,paint);
mPicture.endRecording();
}
---------------------------------------------------------------
// 3.在使用前调用(我在构造函数中调用了)
public Canvas3(Context context, AttributeSet attrs) {
super(context, attrs);
recording(); // 调用录制
}
回放
canvas.drawPicture(mPicture,new RectF(0,0,mPicture.getWidth(),200));//RectF Rect 小于录制大小会缩放
// 包装成为Drawable
PictureDrawable drawable = new PictureDrawable(mPicture);
// 设置绘制区域 -- 注意此处所绘制的实际内容不会缩放
drawable.setBounds(0,0,250,mPicture.getHeight());
// 绘制
drawable.draw(canvas);
setBounds是设置在画布上的绘制区域,并非根据该区域进行缩放,也不是剪裁Picture,每次都从Picture的左上角开始绘制。

绘制顺序

第 1 步——背景,它的绘制发生在一个叫 drawBackground() 的方法里,但这个方法是 private 的,不能重写,你如果要设置背景,只能用自带的 API 去设置(xml 布局文件的 android:background 属性以及 Java 代码的 View.setBackgroundXxx() 方法,第2步主体onDraw() 第3步子view dispatchDraw()第4步onDrawForeground() 滑动边缘渐变和滑动条
onDraw()
不是继承view的情况下 view的onDraw()原本就是空实现
把绘制代码写在 super.onDraw() 的下面,由于绘制代码会在原有内容绘制结束之后才执行,所以绘制内容就会盖住控件原来的内容。如果把绘制代码写在 super.onDraw() 的上面,由于绘制代码会执行在原有内容的绘制之前,所以绘制的内容会被控件的原内容盖住。

有子view
http://hencoder.com/ui-1-5/
dispatchDraw()
ViewGroup 会先调用自己的 onDraw() 来绘制完自己的主体之后再去调用dispatchDraw()绘制它的子 View 写在 super.dispatchDraw() 后能让绘制内容盖住子 View , 写在super.dispatchDraw()前,也就是绘制内容会出现在主体内容和子 View 之间

onDrawForeground()(API 23安卓6.0才能用)
写在 super.onDrawForeground() 的下面绘制代码会在滑动边缘渐变、滑动条和前景之后被执行,那么绘制内容将会盖住滑动边缘渐变、滑动条和前景。
如果你把绘制代码写在了 super.onDrawForeground() 的上面,绘制内容就会在 dispatchDraw() 和 super.onDrawForeground() 之间执行,那么绘制内容会盖住子 View,但被滑动边缘渐变、滑动条以及前景盖住:

动画

属性动画


translationX() 和 translationXBy() 这两个方法的区别是:前者表示移动到某个坐标值,后者表示移动了多少距离。

ViewPropertyAnimator

1
view.animate().translationX(500);

http://hencoder.com/ui-1-6/

ObjectAnimator

如果是自定义控件,需要添加 setter / getter 方法;

例子

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
public class SportsView extends View {
float progress = 0;
......
// 创建 getter 方法
public float getProgress() {
return progress;
}
// 创建 setter 方法
public void setProgress(float progress) {
this.progress = progress;
invalidate();
}
@Override
public void onDraw(Canvas canvas) {
super.onDraw(canvas);
......
canvas.drawArc(arcRectF, 135, progress * 2.7f, false, paint);
......
}
}
......
// 创建 ObjectAnimator 对象
ObjectAnimator animator = ObjectAnimator.ofFloat(view, "progress", 0, 65);
// 执行动画
animator.start();

属性使用例子

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
// tvT为控件名称 若为自定义view内部则用this
//颜色渐变
ObjectAnimator animator = ObjectAnimator.ofInt(tvT, "BackgroundColor", 0xffff0000, 0xff00ff00);
animator.setEvaluator(new ArgbEvaluator());
animator.setDuration(3000); // 设置动画持续时间
//平移
ObjectAnimator animator1 = ObjectAnimator.ofFloat(tvT, "translationX", 0,500,0,200);
animator1.setInterpolator(new OvershootInterpolator());//时间插值器 回弹
animator1.setDuration(1000);
//同时改变多个属性
PropertyValuesHolder holder1 = PropertyValuesHolder.ofFloat("scaleX", 2);//改变尺寸
PropertyValuesHolder holder2 = PropertyValuesHolder.ofFloat("scaleY", 5);
PropertyValuesHolder holder3 = PropertyValuesHolder.ofFloat("alpha", 0.5f);//改变透明度
PropertyValuesHolder holder4 = PropertyValuesHolder.ofFloat("rotation", 0,90,180);//z轴旋转
PropertyValuesHolder holder5 = PropertyValuesHolder.ofFloat("rotationX", 0,90,180);//x轴旋转
PropertyValuesHolder holder6 = PropertyValuesHolder.ofFloat("rotationY", 0,90,180);//y轴旋转
ObjectAnimator animator2 = ObjectAnimator.ofPropertyValuesHolder(tvT, holder1, holder2, holder3,holder4,holder5,holder6);
animator2.setDuration(1000);
//平移
ObjectAnimator animator3 = ObjectAnimator.ofFloat(tvT, "translationY", 500);
animator3.setDuration(2000);
//animator.pause();//暂停
//多个动画依次执行
/* animatorSet.playSequentially(animator, animator1,animator2);
animatorSet.start();*/
//多个动画一起执行
/*animatorSet.playTogether(animator, animator1,animator2);
animatorSet.start()*/
// 属性动画合集
AnimatorSet animatorSet = new AnimatorSet();
/*顺序为animator>>animator1&&animator2>>animator3*/
animatorSet.play(animator1).with(animator2);//同时进行
animatorSet.play(animator1).before(animator3);//animator1在animator3之前
animatorSet.play(animator1).after(animator);//animator1在animator之后
animatorSet.start();

插值器类型
AccelerateInterpolator()加速动画
DecelerateInterpolator()减速动画
AccelerateDecelerateInterpolator()先加速在减速
AnticipateInterpolator()先反向压缩一小段,然后在加速弹出
AnticipateOvershootInterpolator()先反向一段,然后加速反向回来,执行完毕自带回弹效果
BounceInterpolator()执行完毕之后会回弹跳跃几段
CycleInterpolator(2)循环,动画循环一定次数
LinearInterpolator()线性均匀改变
OvershootInterpolator加速执行,结束之后回弹

事件分发机制

view的事件分发机制

https://www.jianshu.com/p/0b821660b195
http://www.gcssloop.com/customview/dispatch-touchevent-theory

Activity -> PhoneWindow -> DecorView -> ViewGroup -> … -> View
Activity <- PhoneWindow <- DecorView <- ViewGroup <- … <- View

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//原理伪代码
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean result = false; // 默认状态为没有消费过
//ViewGroup才有这个步骤
if (!onInterceptTouchEvent(ev)) { // 如果没有拦截交给子View
result = child.dispatchTouchEvent(ev);
}
if (!result) { // 如果事件没有被消费,询问自身onTouchEvent
result = onTouchEvent(ev);
}
return result;
}

dispatchTouchEvent 事件分发(Activity ViewGroup View)

只要事件能传到当前view一定调用 主要控制最终控制是否回传父view 不特指的话只要onTouchEvent()方法或者OnTouchListener()返回了true它就返回true
为false时回传子view dispatchTouchEvent

onInterceptTouchEvent 事件拦截(ViewGroup) ViewGroup默认不拦截

表示是否拦截此事件 true为拦截 拦截后执行 onTouchEvent false则传给他的子view dispatchTouchEvent

onTouchEvent 事件消费(Activity ViewGroup View) 主要处理点击事件

如果返回false dispatchTouchEvent也返回false
在同一个View或者ViewGroup的事件处理中,OnTouchListener优先级最高,OnTouchEvent其次,OnClickListener最低。OnTouchListener返回true则OnTouchEvent以下皆不执行
OnTouchEvent 返回true 且 super.onTouchEvent(event)才执行OnClickListener方法.

view的分发

  1. 不论 View 自身是否注册点击事件,只要 View 是可点击的就会消费事件。
  2. 事件是否被消费由返回值决定,true 表示消费,false 表示不消费,与是否使用了事件无关。
    事件的调度顺序应该是 onTouchListener > onTouchEvent > onLongClickListener > onClickListener。

VIewGroup的分发

1.判断自身是否需要(询问 onInterceptTouchEvent 是否拦截),如果需要,调用自己的 onTouchEvent。
2.自身不需要或者不确定,则询问 ChildView ,一般来说是调用手指触摸位置的 ChildView。
3.如果子 ChildView 不需要则调用自身的 onTouchEvent。
4.ViewGroup 中可能有多个 ChildView,会把所有的 ChildView 遍历一遍,如果手指触摸的点在 ChildView 区域内就分发给这个View。
5.当 ChildView 重叠时,一般会分配给显示在最上面的 ChildView。
6.只有 View1 可点击时,事件将会分配给 View1,即使被 View2 遮挡,这一部分仍是 View1 的可点击区域。只有 View2 可点击时,事件将会分配给 View2。
View1 和 View2 均可点击时,事件会分配给后加载的 View2,View2 将事件消费掉,View1接收不到事件。
7.ViewGroup 和 ChildView 同时注册了事件监听器(onClick等),事件优先给 ChildView,会被 ChildView消费掉,ViewGroup 不会响应。

MotionEvent

详细内容左转
http://www.gcssloop.com/customview/motionevent

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
TextView tvT = (TextView) findViewById(R.id.tv_t);
tvT.setClickable(true);
tvT.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
// motionEvent.getAction() 单点触控
switch (motionEvent.getActionMasked()){//多点触控
case MotionEvent.ACTION_DOWN:
Log.e("MotionEvent", "ACTION_DOWN ");
// 手指按下
break;
case MotionEvent.ACTION_MOVE:
Log.e("MotionEvent", "ACTION_MOVE");
// 手指移动
break;
case MotionEvent.ACTION_UP:
Log.e("MotionEvent", "ACTION_UP");
// 手指抬起
break;
case MotionEvent.ACTION_CANCEL:
Log.e("MotionEvent", "ACTION_CANCEL");
// 事件被拦截
break;
case MotionEvent.ACTION_OUTSIDE:
Log.e("MotionEvent", "ACTION_OUTSIDE");
// 超出区域
break;
case MotionEvent.ACTION_POINTER_DOWN://getActionMasked()才有
Log.e("MotionEvent", "ACTION_POINTER_DOWN");
// 有非主要的手指按下(即按下之前已经有手指在屏幕上)。
break;
case MotionEvent.ACTION_POINTER_UP://getActionMasked()才有
Log.e("MotionEvent", "ACTION_POINTER_UP");
// 有非主要的手指按下(即按下之前已经有手指在屏幕上)。
break;
}
return true;
}
});

TouchSlop

通过:ViewConfiguration.get(getContext()).getScaledTouchSlop() 获取系统的滑动常量来,判断此时是否属于滑动事件,TouchSlop在各家手机系统默认值是不同的。

VelocityTracker

用于跟踪手指滑动的速度,包括x轴方向和y轴方向的速度。如快速滑动或者其他手势操作。
一般在MotionEvent.ACTION_DOWN初始化

1
VelocityTracker velocityTracker = VelocityTracker.obtain();//获取实例

在 MotionEvent.ACTION_MOVE 追踪速度

1
2
3
4
velocityTracker.addMovement(event);//当前的 移动事件传递给VelocityTracker对象
velocityTracker.computeCurrentVelocity(1000);//多少毫秒滑动的像素数
xVelocity = (int) velocityTracker.getXVelocity();//x像素
yVelocity = (int) velocityTracker.getYVelocity();//y像素

在 MotionEvent.ACTION_CANCEL 回收

1
2
velocityTracker.clear();
velocityTracker.recycle();

GestureDetector

用来处理双击、长按、快速滑动等手势
GestureDetector 使用时控件要setClickable(true)不然会不准确,具体原因不知道

OnGestureListener

比较快的点击松开会触发
onDown>>onSingleTapUp

普通速度稍微停留一点的点击松开会触发onDown>>onShowPress>>onSingleTapUp

长按会触发
onDown>>onShowPress>>onLongPress

普通滚动
onDown>>onShowPress>>onScroll(多个)

快速滚动
onDown>>onShowPress>>onScroll(多个)>>onFling

OnDoubleTapListener

点击
onSingleTapConfirmed

双击
发生一次双击
onDoubleTap>>onDoubleTapEvent(两个 这里代表点击次数)

例子

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
53
54
55
56
57
58
59
60
61
62
63
64
TextView tvT = (TextView) findViewById(R.id.tv_t);
tvT.setClickable(true);
final GestureDetector gestureDetector =new GestureDetector(this, new GestureDetector.OnGestureListener() {
@Override
public boolean onDown(MotionEvent motionEvent) {
// Log.e("手势检测", "轻触屏幕");
return false;
}
@Override
public void onShowPress(MotionEvent motionEvent) {
// Log.e("手势检测", "轻触屏幕未松开");
}
@Override
public boolean onSingleTapUp(MotionEvent motionEvent) {
// Log.e("手势检测", "普通单击");
return false;
}
@Override
public boolean onScroll(MotionEvent motionEvent, MotionEvent motionEvent1, float v, float v1) {
//Log.e("手势检测", "普通滑动");
return false;
}
@Override
public void onLongPress(MotionEvent motionEvent) {
//Log.e("手势检测", "长按");
}
@Override
public boolean onFling(MotionEvent motionEvent, MotionEvent motionEvent1, float v, float v1) {
//Log.e("手势检测", "快速滑动后松开");
return false;
}
});
gestureDetector.setOnDoubleTapListener(new GestureDetector.OnDoubleTapListener() {
@Override
public boolean onSingleTapConfirmed(MotionEvent motionEvent) {
Log.e("手势检测", "严格的单击");
return false;
}
@Override
public boolean onDoubleTap(MotionEvent motionEvent) {
Log.e("手势检测", "双击");
return false;
}
@Override
public boolean onDoubleTapEvent(MotionEvent motionEvent) {
Log.e("手势检测", "发生双击行为");
return false;
}
});
tvT.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View view, MotionEvent event) {
return gestureDetector.onTouchEvent(event);
}
});

Scroller

注意View中直接调用scrollBy滑动的是View的Content内容,对于Button,它的Content就是文本,ImageView就是drawable,也就是只能改变view内容的位置而不能改变view在布局的位置。

scrollTo(x,y)绝对滑动

scrollBy(x,y)根据当前位置叠加移动 可多次叠加

postDelayed view的定时器

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
TextView tvT = (TextView) findViewById(R.id.tv_t);
tvT.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
tvT.setText("变化前");
tvT.postDelayed(new Runnable() {
@Override
public void run() {
tvT.setText("变化后");//三秒后执行
}
},3*1000);
}
});

解决滑动冲突的思路

一般来说就是父容器和子容器or子view的冲突

外部拦截(父容器负责拦截)

自定义控件继承父容器重写onInterceptTouchEvent()
根据需求决定是否拦截
假如子容器or子view 和父容器都需要响应事件的情况下(例如一个要响应向左右一个要响应上下)
那么父容器的 MotionEvent.ACTION_DOWN必须返回false 不然事件无法再传给子容器or子view

内部拦截(子容器 子view负责拦截)

阻止父层的View截获点击事件
自定义控件继承 子容器 子view重写dispatchTouchEvent()
根据需求决定是否拦截

1
2
3
4
5
6
7
8
9
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
....
getParent().requestDisallowInterceptTouchEvent(true);//表示拦截
.....
return super.dispatchTouchEvent(event);
}

假如子容器or子view 和父容器都需要响应事件的情况下(例如一个要响应向左右一个要响应上下)
那么父容器的onInterceptTouchEvent()的 MotionEvent.ACTION_DOWN也必须返回false 不然事件无法再传给子容器or子view

view的工作原理

自定义view流程

getMeasuredWidth()获取的是view原始的大小,也就是这个view在XML文件中配置或者是代码中设置的大小。getWidth()获取的是这个view最终显示的大小,这个大小有可能等于原始的大小也有可能不等于原始大小。

自定义view分为两种其中每一种有两个类型。
1.完全自定义的
继承 View
继承 ViewGroup
2.继承于特定的控件或者布局的
继承某个控件
继承某个布局

自定义属性

在res 的values 新建attrs 或者attrs_xxxxxx 都可以
例子

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
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="CircleView"><!--自定义属性集合CircleView-->
<attr name="circle_color" format="color" />
<!--
属性定义时可以指定多种类型值
format= 类型有
reference:参考某一资源ID
color:颜色值
boolean:布尔值
dimension:尺寸值
float:浮点值
integer:整型值
string:字符串
fraction:百分数
-->
<!--
enum:枚举值
<attr name="orientation">
<enum name="horizontal" value="0" />
<enum name="vertical" value="1" />
</attr>-->
<!--
flag:位或运算
<attr name="windowSoftInputMode">
<flag name="stateUnspecified" value="0" />
<flag name="stateUnchanged" value="1" />
<flag name="stateHidden" value="2" />
<flag name="stateAlwaysHidden" value="3" />
<flag name="stateVisible" value="4" />
<flag name="stateAlwaysVisible" value="5" />
<flag name="adjustUnspecified" value="0x00" />
<flag name="adjustResize" value="0x10" />
<flag name="adjustPan" value="0x20" />
<flag name="adjustNothing" value="0x30" />
</attr>
使用 android:windowSoftInputMode="stateUnspecified | stateUnchanged "
-->
</declare-styleable>
</resources>

使用

1
2
3
4
5
6
<work.model.com.cczhrdraw.CircleView
android:paddingRight="40dp"
app:circle_color="@color/colorPrimary"
android:background="@color/colorAccent"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />

拿数据

1
2
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
mColor = a.getColor(R.styleable.CircleView_circle_color, Color.RED);

自定义view

例子

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
53
54
55
56
57
58
59
60
61
62
63
64
public class CircleView extends View {
private int mColor = Color.RED;
private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
public CircleView(Context context) {
super(context);
init();
}
public CircleView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CircleView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
mColor = a.getColor(R.styleable.CircleView_circle_color, Color.RED);
a.recycle();//回收TypedArray
init();
}
private void init() {
mPaint.setColor(mColor);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);//得到宽度的测量模式
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);//得到宽度的数值
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);//得到长度的测量模式
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);//得到长度的数值
/*
测量模式:
UNSPECIFIED:不限制 match_parent
AT_MOST:限制上限 wrap_content
EXACTLY:限制固定值 手动自定义
*/
if (widthSpecMode == MeasureSpec.AT_MOST
&& heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(200, 200);
} else if (widthSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(200, heightSpecSize);
} else if (heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSpecSize, 200);//这里设的大小相当于Canvas大小
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
final int paddingLeft = getPaddingLeft();
final int paddingRight = getPaddingRight();
final int paddingTop = getPaddingTop();
final int paddingBottom = getPaddingBottom();
int width = getWidth() - paddingLeft - paddingRight;
int height = getHeight() - paddingTop - paddingBottom;
int radius = Math.min(width, height) / 2;
canvas.drawCircle(paddingLeft+ width / 2, paddingTop+ height / 2,radius, mPaint);
}
}