胡周八扯

Hello World


  • 首页

  • 关于

  • 归档

  • 标签

Android 贴纸

发表于 2016-04-07   |  

刚到老东家FitTime的时候正好赶上了team刚组建,大家每天都在对着IOS版本看需求,到开发启动那天就是谁看到哪就做哪,于是我就被分到了做图像处理的模块。其中最让人抓狂的是贴纸部分,每个贴纸要独立具备拖动、放大缩小、旋转翻转、删除等功能,当然这也是所有贴纸模块需要具备的基本要素,但是当时真是一穷二白,大家都不懂,网上也找不到好的解决方案,就只能硬着头皮去试验各种方法,最后发现只有自定义View可以拯救我。准备把我的思路分享给各位,希望能有所帮助。因为我的解决方案是使用自定义View,所有对于View的生命周期、Canvas、Matrix等相关知识都需要有一定的了解,如果有对这些并不清楚的童鞋最好先去了解一下,这样能方便你更好的阅读接下来的内容,关于这些背景知识我之前的博客中也有介绍,感兴趣的各位可以看看。

概述

了解过自定义View的童鞋 对Canvas.drawBitmap(Bitmap, Matrix, Paint)这个函数应该不会陌生,Bitmap的位置、大小、旋转角度、扭曲程度等都由Matrix来管理,而实现贴纸效果的就需要借助这个神奇的函数。我们可以通过很多种方法获取到贴纸的Bitmap,也可以很容易的定义绘制Bitmap所使用的Paint,那么剩下我们只需要关心怎样可以借助Matrix来让贴纸随着我们的指尖翩翩起舞。
为了更好的管理每个贴纸的Bitmap和Matrix信息,我简单的将二者进行了封装,大家不要在意名字,知道这个类是干嘛的就好了,毕竟如何起一个优雅准确的名字是一个世界性的难题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static class ImageGroup {
public Bitmap bitmap;
public Matrix matrix = new Matrix();

//删除贴纸时释放资源时使用
public void release() {
if (bitmap != null) {
bitmap.recycle();
bitmap = null;
}

if (matrix != null) {
matrix.reset();
matrix = null;
}
}
}

说到随着指尖,我们就会想到Android丰富的手势操作,因为是自定义View,所有对Bitmap的操作都需要用到手势触点坐标,因此我使用了View的onTouchEvent(MotionEvent event)方法直接对手势触点就行操作,onTouchEvent也是整个贴纸模块的核心。

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
public boolean onTouchEvent(MotionEvent event) {
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
anchorX = event.getX();
anchorY = event.getY();
moveTag = decalCheck(anchorX, anchorY);
deleteTag = deleteCheck(anchorX, anchorY);
if (moveTag != -1 && deleteTag == -1) {
downMatrix.set(mDecalImageGroupList.get(moveTag).matrix);
mode = DRAG;
}
break;

case MotionEvent.ACTION_POINTER_DOWN:
moveTag = decalCheck(event.getX(0), event.getY(0));
transformTag = decalCheck(event.getX(1), event.getY(1));
if (moveTag != -1 && transformTag == moveTag && deleteTag == -1) {
downMatrix.set(mDecalImageGroupList.get(moveTag).matrix);
mode = ZOOM;
}
oldDistance = getDistance(event);
oldRotation = getRotation(event);
midPoint = midPoint(event);
break;

case MotionEvent.ACTION_MOVE:
if (mode == ZOOM) {
moveMatrix.set(downMatrix);
float newRotation = getRotation(event) - oldRotation;
float newDistance = getDistance(event);
float scale = newDistance / oldDistance;
moveMatrix.postScale(scale, scale, midPoint.x, midPoint.y);// 缩放
moveMatrix.postRotate(newRotation, midPoint.x, midPoint.y);// 旋转
if (moveTag != -1) {
mDecalImageGroupList.get(moveTag).matrix.set(moveMatrix);
}
invalidate();
} else if (mode == DRAG) {
moveMatrix.set(downMatrix);
moveMatrix.postTranslate(event.getX() - anchorX, event.getY() - anchorY);// 平移
if (moveTag != -1) {
mDecalImageGroupList.get(moveTag).matrix.set(moveMatrix);
}
invalidate();
}
break;

case MotionEvent.ACTION_UP:
if (deleteTag != -1) {
mDecalImageGroupList.remove(deleteTag).release();
invalidate();
}
mode = NONE;
break;

case MotionEvent.ACTION_POINTER_UP:
mode = NONE;
break;
}
return true;
}

手势操作

onTouchEvent中我们使用了ACTION_DOWN、ACTION_POINTER_DOWN、ACTION_MOVE、ACTION_UP、ACTION_POINTER_UP五种事件,其中ACTION_DOWN和ACTION_UP、ACTION_POINTER_DOWN和ACTION_POINTER_UP分别对应。

  • ACTION_DOWN和ACTION_UP:当View从无到有手指触摸,ACTION_DOWN会被触发,对应的ACTION_UP则为从有到无,也就是说只有当没有任何一根手指在触摸View时ACTION_UP才会被触发,ACTION_DOWN和ACTION_UP是整个手势操作生命周期的起点和终点。
  • ACTION_POINTER_DOWN和ACTION_POINTER_UP:当View有多点触摸时ACTION_POINTER_DOWN会被触发,而当其中某个触点消失后ACTION_POINTER_UP会被触发。这里我们只考虑有两根手指触摸的情况。
  • ACTION_MOVE:当View被触摸且该触摸点在移动时ACTION_MOVE会被触发,多点触摸时无论哪个点移动都会触发。

Matrix的Translate(平移)外,Scale(缩放)、Rotate(旋转)、Skew(扭曲)四大操作除了Skew外我们都需要使用,对应到手势上我们通过单点触摸来控制Bitmap平移,通过多点触摸来控制Bitmap缩放和旋转,因此,在ACTION_MOVE阶段我们需要根据两种不同情况做区分。

1
2
3
int NONE = 0;//无
int DRAG = 1;//平移模式
int ZOOM = 2;//缩放、旋转模式

我们定义三种mode,mode的初始值为NONE,当ACTION_DOWN被触发时mode置为DRAG,当ACTION_POINTER_DOWN被触发时mode置为ZOOM。当ACTION_MOVE被触发时,我们对mode进行判断,针对DRAG和ZOOM两种情况分别进行处理。

  1. mode == DRAG
1
2
3
4
5
6
7
8
anchorX = event.getX();
anchorY = event.getY();
moveTag = decalCheck(anchorX, anchorY);
deleteTag = deleteCheck(anchorX, anchorY);
if (moveTag != -1 && deleteTag == -1) {
downMatrix.set(mDecalImageGroupList.get(moveTag).matrix);
mode = DRAG;
}

ACTION_DOWN被触发时我们首先将当前触摸点的坐标保存下来以备使用。之后要判断当前触摸点是否在某一个贴纸的Bitmap范围内以及当前触摸点是否在某一个贴纸的删除按钮范围内,我们分别用moveTag和deleteTag来保存结果,当结果为-1时表示没有在任何相关范围内,结果为0~贴纸数量-1时表示在某个贴纸的相关范围内。只有当moveTag != -1 && deleteTag == -1(触摸点某一个贴纸范围内且不在任何删除按钮范围内)时,我们将当前贴纸的Matrix保存到downMatrix中并将mode置成DRAG,若deleteTag不等于-1时,我们在ACTION_UP就将对应的贴纸从贴纸列表中移除。

1
2
3
4
5
6
moveMatrix.set(downMatrix);
moveMatrix.postTranslate(event.getX() - anchorX, event.getY() - anchorY);// 平移
if (moveTag != -1) {
mDecalImageGroupList.get(moveTag).matrix.set(moveMatrix);
}
invalidate();

进入到ACTION_MOVE阶段,我们首先将downMatrix赋值给moveMatrix,downMatrix是这次手势操作的起始Matrix,之后的变换都是基于downMatrix进行的,所以我们不能直接对downMatrix进行操作,moveMatrix承担了这个责任。DRAG模式下表示当前要进行的是平移操作,而平移的横纵距离是在ACTION_DOWN阶段保存下来的触摸点横纵坐标值与当前移动到的触摸点横纵坐标值的差值。最后将处理好的moveMatrix赋值回该贴纸的Matrix,并调用重绘函数,完成此次贴纸平移操作。因为ACTION_MOVE会在ACTION_UP触发之前一直保持,所以整个平移操作会一直持续,贴纸随着手指移动而移动的效果就出现了。

  1. mode == ZOOM
1
2
3
4
5
6
7
8
9
moveTag = decalCheck(event.getX(0), event.getY(0));
transformTag = decalCheck(event.getX(1), event.getY(1));
if (moveTag != -1 && transformTag == moveTag && deleteTag == -1) {
downMatrix.set(mDecalImageGroupList.get(moveTag).matrix);
mode = ZOOM;
}
oldDistance = getDistance(event);
oldRotation = getRotation(event);
midPoint = midPoint(event);

ACTION_POINTER_DOWN被触发时,我们首先对两个触摸点是否在某个贴纸范围内进行判断,结果分别用moveTag和transformTag进行保存。当moveTag != -1 && transformTag == moveTag && deleteTag == -1(两个触摸点在同一个贴纸范围内且不在任何删除按钮范围内)条件满足时,我们将当前贴纸的Matrix保存到downMatrix中并将mode置成ZOOM。同时我们需要将当前两个触摸点之间的距离、中点、角度用oldDistance、midPoint、oldRotation保存起来以备使用。

1
2
3
4
5
6
7
8
9
10
moveMatrix.set(downMatrix);
float newRotation = getRotation(event) - oldRotation;
float newDistance = getDistance(event);
float scale = newDistance / oldDistance;
moveMatrix.postScale(scale, scale, midPoint.x, midPoint.y);// 缩放
moveMatrix.postRotate(newRotation, midPoint.x, midPoint.y);// 旋转
if (moveTag != -1) {
mDecalImageGroupList.get(moveTag).matrix.set(moveMatrix);
}
invalidate();

进入到ACTION_MOVE阶段,我们首先将downMatrix赋值给moveMatrix。用当前的两个触摸点算出新的角度,同之前保存的值算出差值newRotation,算出新的距离newDistance并和oldDistance做商,算出缩放比例。分别对moveMatrix进行缩放和旋转操作,处理好后将moveMatrix赋值回该贴纸的Matrix,并调用重绘函数,完成此次贴纸平移操作。因为ACTION_MOVE会在ACTION_UP触发之前一直保持且在ACTION_POINTER_UP触发之前ZOOM模式会一直保持,所以缩放、旋转操作会一直持续,贴纸随着两根手指之间距离变化而变化,角度变化而变化的效果就出现了。

触摸点判断

1
2
3
4
5
6
7
8
9
10
11
protected float[] getBitmapPoints(Bitmap bitmap, Matrix matrix) {
float[] dst = new float[8];
float[] src = new float[]{
0, 0,
bitmap.getWidth(), 0,
0, bitmap.getHeight(),
bitmap.getWidth(), bitmap.getHeight()
};
matrix.mapPoints(dst, src);
return dst;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private boolean pointCheck(ImageGroup imageGroup, float x, float y) {
float[] points = getBitmapPoints(imageGroup);
float x1 = points[0];
float y1 = points[1];
float x2 = points[2];
float y2 = points[3];
float x3 = points[4];
float y3 = points[5];
float x4 = points[6];
float y4 = points[7];
float edge = (float) Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2));
if ((2 + Math.sqrt(2)) * edge >= Math.sqrt(Math.pow(x - x1, 2) + Math.pow(y - y1, 2))
+ Math.sqrt(Math.pow(x - x2, 2) + Math.pow(y - y2, 2))
+ Math.sqrt(Math.pow(x - x3, 2) + Math.pow(y - y3, 2))
+ Math.sqrt(Math.pow(x - x4, 2) + Math.pow(y - y4, 2))) {
return true;
}
return false;
}
1
2
3
4
5
6
7
8
9
10
private boolean circleCheck(ImageGroup imageGroup, float x, float y) {
float[] points = getBitmapPoints(imageGroup);
float x2 = points[2];
float y2 = points[3];
int checkDis = (int) Math.sqrt(Math.pow(x - x2, 2) + Math.pow(y - y2, 2));
if (checkDis < 40) {
return true;
}
return false;
}

在整个手势操作流程中我们需要多次使用触摸点判断,不论是判断是否在贴纸范围还是删除按钮范围。Matrix提供了map开头的映射方法,其中的mapPoints(float[] dst, float[] src)可以将src坐标数组根据Matrix映射到dst。使用Matrix来存储Bitmap的绘制信息,Bitmap默认的绘制起点(左上角点)为(0,0),因此默认情况下Bitmap四个点的坐标为(0,0)、(bitmap.getWidth(), 0)、(0, bitmap.getHeight())、(bitmap.getWidth(), bitmap.getHeight()),依此我们可以映射出当前Bitmap四个点的坐标。我们的贴纸在整个流程任何操作下都是正方形,因此我们可以使用已知正方形四个顶点来判断第五点是否在正方形范围内,算法是第五点到四顶点的距离是否小于等于2√2倍的边长。这个算法对长方形适不适用我没有验证,如果要添加非正方形Bitmap的话需要自行优化此处。判断点是否在一个圆的范围内很简单,只要将该点到圆心的距离和半径进行比较即可。

总结

整个Android贴纸的简单实现思路就行这样,完整代码链接如下,有需要的童鞋可以搞下来看看,有什么问题或者好点子欢迎交流。
代码地址:https://github.com/JunyiZhou/AndroidExercises/tree/master/ImageHandleDemo

Android图像处理之Matrix

发表于 2016-03-16   |  

Canvas类中drawBitmap(Bitmap bitmap, Matrix matrix, Paint paint)方法中有个参数类型是Matrix,从字面上理解是矩阵的意思,而实际上它也确实是个3x3的矩阵。Matrix在Android中的主要作用是图像变换,如平移、旋转、缩放、扭曲等。

Matrix内部通过维护一个float[9]的数组来构成3x3矩阵的形式,而实际上所有的变换方法说到底就是通过更改数组中某个或某几个位置的数值。Matrix提供了setValues()和getValues()方法来操作数组。

显然这两个方法使用起来很不方便,如果只有这样,那Matrix估计就不会有人使用了。Google轻易不会辜负我们的信任,Matrix提供了若干简单易用的变换方法和映射方法供开发者使用。

Matrix变换方法

Matrix提供了Translate(平移)、Scale(缩放)、Rotate(旋转)、Skew(扭曲)四中变换操作,这四种操作实质上是调用了setValues()方法来设置矩阵数组来达到变换效果。除Translate(平移)外,Scale(缩放)、Rotate(旋转)、Skew(扭曲)都可以围绕一个中心点来进行,如果不指定,在默认情况下是围绕(0, 0)来进行相应的变换的。
Matrix提供的四种操作,每一种都有pre、set、post三种形式。原因是矩阵乘法不满足乘法交换律,因此左乘还是右乘最终的效果都不一样。我们可以把Matrix变换想象成一个队列,队列里面包含了若干个变换操作,队列中每个操作按照先后顺序操作变换目标完成变换,pre相当于向队首增加一个操作,post相当于向队尾增加一个操作,set相当于清空当前队列重新设置。

  • 例1

    1
    2
    3
    4
    //这段代码只有translate(100, 100)生效,因为第二个set会把之前队列中的操作清除。
    Matrix m = new Matrix();
    m.setRotate(100);
    m.setTranslate(100, 100);
  • 例2

    1
    2
    3
    4
    //这段代码先执行translate(100, 100),后执行rotate(100)
    Matrix m = new Matrix();
    m.setTranslate(100, 100);
    m.postRotate(100);
  • 例3

    1
    2
    3
    4
    //这段代码先执行rotate(100),后执行translate(100, 100)
    Matrix m = new Matrix();
    m.setTranslate(100, 100);
    m.preRotate(100);
  • 例4

    1
    2
    3
    4
    5
    6
    //这段代码的执行顺序:translate(100f, 100f) -> scale(2f, 2f) -> scale(0.5f, 0.5f) -> translate(50f, 50f)
    Matrix m = new Matrix();
    m.preScale(2f, 2f);
    m.preTranslate(100f, 100f);
    m.postScale(0.5f, 0.5f);
    m.postTranslate(50f, 50f);
  • 例5

    1
    2
    3
    4
    5
    6
    7
    //这段代码的执行顺序:translate(50f, 50f) -> scale(0.8f, 0.8f) -> scale(3f, 3f)
    Matrix m = new Matrix();
    m.postTranslate(100, 100);
    m.preScale(0.5f, 0.5f);
    m.setScale(0.8f, 0.8f);
    m.postScale(3f, 3f);
    m.preTranslate(50f, 50f);

Matrix 映射方法

Matrix提供了mapPoints(),mapRects(),mapVectors()等映射方法,用来获取经Matrix映射后的值。

  • 例
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    //这段代码的作用是获取经过平移后该bitmap四个点的坐标
    Matrix m = new Matrix();
    m.postTranslate(100f, 100f);

    float[] src = {
    0, 0,
    0, bitmap.getHeight(),
    bitmap.getWidth(), 0,
    bitmap.getWidth(), bitmap.getHeight()
    };
    float[] dst = new float[8];

    m.mapPoints(dst, src);

Android图像处理之Canvas

发表于 2016-03-09   |  

上一篇我们了解了View生命周期的相关方法和调用流程,其中最重要的onDraw方法是自定义View的重要基石。而其中的Canvas参数则至关重要。

Android官方源码中对Canvas的描述是:“Canvas类容纳所有和Draw(绘制)相关方法。为了去Draw些东西,你需要具备4个基础要素:1个Bitmap用来承载像素信息,1个Canvas用来管理Draw相关方法(写入Bitmap中),1个绘图基元(例如,Rect,Path,text,Bitmap),1个画笔(用于描绘图像的颜色和风格)。

这和我们日常理解的绘画异曲同工,Bitmap作为画布,Canvas管理着绘画的手法,绘图基元代表着要绘制的目标,Paint就是你手里的画笔和颜料。

如何得到1个Canvas对象

  1. 之前提到的onDraw方法的入口参数就是Canvas,我们用变量承载它,就可以使用,而我们操作这个Canvas最终的效果会直接反应在这个View上。
  2. SurfaceView是View的继承了,其内部有专门的线程来完成画图的工作,而不用像View一样需要等待刷新,主要用在游戏和高品质动画方面。既然是View的继承类,SurfaceView自然可以获取到Canvas的对象,方式是通过调用SurfaceView的好基友SurfaceHolder的lockCanvas()方法。
  3. 当然,除了获取View或SurfaceView自带的现成的Canvas对象,我们还可以自己创建。从4大基本要素我们就可以知道,1个Canvas对象一定要结合1个Bitmap对象。所以一定要为新建的Canvas对象设置1个Bitmap对象。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    /**
    * 得到一个Bitmap对象,当然也可以使用别的方式得到。但是要注意,改bitmap一定要是mutable(异变的)
    * mutable : 易变的,不定的
    * mutable 作用 : 控制bitmap的setPixel方法能否使用,也就是外界能否修改bitmap的像素。
    * Bitmap.createBitmap(mWidth, mHeight, Config.ARGB_8888) 为 mutable 为true
    * BimapFactory.decodeResource() 得到的mutable 为false, 要想其为true
    * 一般会BimtapFactory.decodeResource().copy(configu_argb_8888, true);
    * 先new一个Canvas对象,在调用setBitmap方法,一样的效果
    * Canvas c = new Canvas();
    * c.setBitmap(b);
    */

    Bitmap b = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);
    Canvas canvas2 = new Canvas(b);

Canvas能画些什么

Canvas类提供了若干draw……的方法,从字面上我们就可以知道用这些方法我们可以绘制哪些东西。

填充

Canvas内部维持了一个mutable Bitmap,所以我们可以用颜色来填充整个Bitmap。而填充的范围受限于clip的范围。

  • drawRGB(int r, int g, int b):r-红色要素(0~255),g-绿色要素(0~255),b-蓝色要素(0~255)。
  • drawARGB(int a, int r, int g, int b):a-透明度(0~255),r-红色要素(0~255),g-绿色要素(0~255),b-蓝色要素(0~255)。
  • drawColor(int color):这里的color要为16进制的颜色号,例如:0xFF000000,共8位,两位一组从左到右分别为ARGB,具体含义同上。若要使用“#FF000000”这样的色号,可以调用Color.parseColor(String colorString)方法来进行转换。
  • drawColor(int color, PorterDuff.Mode mode):color同上,PorterDuff.Mode比较有意思,有很多种模式,每一种模式都会有特定的效果,感兴趣的朋友可以自行了解一下。
  • drawPaint(Paint paint):Canvas同样可以用画笔来填充Bitmap,当然也受限于clip的范围。

    绘制图形

  • canvas.drawArc (扇形)
  • canvas.drawCircle(圆)
  • canvas.drawOval(椭圆)
  • canvas.drawLine(线)
  • canvas.drawPoint(点)
  • canvas.drawRect(矩形)
  • canvas.drawRoundRect(圆角矩形)
  • canvas.drawVertices(顶点)
  • cnavas.drawPath(路径)

    绘制图片

  • canvas.drawBitmap(位图)
  • canvas.drawPicture(图片)

    文本

  • canvas.drawText

    Canvas的变换

    Canvas不仅仅可以draw一些图形、图片,其本身也提供了可操作的方法:rorate(旋转)、scale(压缩)、translate(平移)、skew(扭曲)等。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    @Override
    protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    canvas.drawRect(new Rect(0, 0, 200, 200), new Paint());
    canvas.scale(0.5f, 0.5f);//缩放了
    canvas.drawRect(new Rect(400, 400, 600, 600), new Paint());

    canvas.translate(600, 600);//平移了
    canvas.rotate(45);//旋转了
    canvas.drawRect(new Rect(0, 0, 200, 200), new Paint());

    canvas.translate(200, 200);
    canvas.skew(.5f, .5f);//扭曲了
    canvas.drawRect(new Rect(0, 0, 200, 200), new Paint());
    }

Canvas的保存和回滚

为了配合Canvas本身的变换操作,Canvas提供了保持当前状态和回滚的方法。来个小例子。
我们准备画一个表盘,那么我们就有两种实现的思路:

  1. 表盘上有60个刻度,每个刻度之间间隔6°,这是有规律的,那么我们就可以利用三角函数的知识来把刻度的两个坐标求出来,再利用drawLine画到Canvas上。考验你逻辑思维和数学功底的时候到了,不用仔细想就能知道这个方法有点麻烦。
  2. 如果不喜欢第一种方法,那么我们可以尝试一下这一种。Canvas提供了旋转操作的方法,也提供了保存和回复状态的方法,那么我们就用这些搭配起来。首先我在(100,0)和(100,10)两个坐标之间画一条竖线,很简单,整点的刻度就出来了。之后先将Canvas的状态保存起来,在(100,0)和(100,10)两个坐标之间画一条竖线,回复Canvas的状态,一分钟的刻度就出来了。以此类推,思路是不是很简单。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @Override
    protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    for (int i = 0; i < 360; i = i + 6) {
    canvas.save();
    canvas.rotate(i, 100, 100);
    canvas.drawLine(100, 0, 100, 10, new Paint());
    canvas.restore();
    }
    }

总结

Canvas的大体情况就是这样,如果只是需要用Canvas来实现自定义View的话,那么这些内容应该可以帮到你。当然,还有相当一部分的方法和属性我没有提到,感兴趣的同学可以深入研究一下Canvas。
下一篇我们一起研究一下自定义控件中比较重要的Matrix(矩阵)类。

Android图像处理之View的生命周期

发表于 2016-03-07   |  

去年三月份来帝都,荣幸的加入了老东家FitTime睿健时代的Android团队,阴差阳错的分到了图像处理的相关模块。
此后的很长一段时间都在和各种View啊,Canvas啊,Bitmap啊,Metrix啊厮混,相爱相杀。
踩过好多的坑,换过好多种实现方法,最后确定了现在的方案,希望能给需要的你提供一个思路。

View生命周期相关方法

View是什么?官方源码注释中的定义:这个类是用户接口的基础构件。View表示屏幕上的一块矩形区域,负责绘制这个区域和事件处理。
View是所有widget类的基类,Widget类用于创建交互式UI构件(按钮,输入框等)。
View类的ViewGroup子类是layout的基类,Layout是一个不可见的容器,它保存着View(或ViewGroup)并定义这些View的layout 属性。

简单点说,View就是屏幕上的一块矩形区域,我们可以在这块区域绘制我们想让用户看到的图形。

关于View的生命周期,官方源码注释中有详细的描述,作为英语六级勉强飘过的选手,我顶着巨大的压力翻译了一下。

  1. Creation(创建)
    • Constructors(构造函数):有一种形式的构造函数会在View在代码中被创建时调用,另一种形式的构造函数会在View从layout加载出来时被调用。
      第二种形式的构造函数会解析和应用layout文件中定义的任何属性。
    • onFinishInflate():该方法当View及其子View从XML文件中加载完成后会被调用。
  2. Layout(布局)
    • onMeasure(int, int):该方法在计算当前View及其所有子View尺寸大小需求时会被调用。
    • onLayout(boolean, int, int, int, int):该方法在当前View需要为其子View分配尺寸和位置时会被调用。
    • onSizeChanged(int, int, int, int):该方法在当前View尺寸变化时被调用。
  3. Drawing(绘制)
    • onDraw(android.graphics.Canvas):该方法在当前View需要呈现其内容时被调用。
  4. Event processing(事件处理)
    • onKeyDown(int, KeyEvent):该方法在一个物理按键事件发生时被调用。
    • onKeyUp(int, KeyEvent):该方法在一个物理按键弹起事件发生时被调用。
    • onTrackballEvent(MotionEvent):该方法在一个轨迹球运动事件发生时被调用。
    • onTouchEvent(MotionEvent):该方法在一个触摸屏幕运动事件发生时被调用。
  5. Focus(聚焦)
    • onFocusChanged(boolean, int, android.graphics.Rect):该方法在当前View获得或失去焦点时被调用。
    • onWindowFocusChanged(boolean):该方法在包含当前View的window获得或失去焦点时被调用。
  6. Attaching(附上)
    • onAttachedToWindow():该方法在当前View被附到一个window上时被调用。
    • onDetachedFromWindow():该方法在当前View从一个window上分离时被调用。
    • onVisibilityChanged(View, int):该方法在当前View或其祖先的可见性改变时被调用。
    • onWindowVisibilityChanged(int):该方法在包含当前View的window可见性改变时被调用。

上述方法是View生命周期中涉及到的比较重要的一部分,View类中包含了很多的方法和属性,有兴趣的话各位可以自己研究一下。

View生命周期相关方法调用顺序

简单的了解了View生命周期相关的几个方法,接着我们看看这些方法调用的顺序是怎样的,我们针对View的可见性分三种情况来观察。

  1. android:visibility=visible

    • 创建

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      I/TestView: TestView(Context context, AttributeSet attrs)
      I/TestView: onFinishInflate()
      I/TestView: onVisibilityChanged(View changedView, int visibility) changedView = com.android.internal.policy.impl.PhoneWindow$DecorView{2192bad9 I.E..... R.....ID 0,0-0,0} visibility = 4
      I/TestView: onVisibilityChanged(View changedView, int visibility) changedView = com.android.internal.policy.impl.PhoneWindow$DecorView{2192bad9 V.E..... R.....ID 0,0-0,0} visibility = 0
      I/TestView: onAttachedToWindow()
      I/TestView: onWindowVisibilityChanged(int visibility) visibility = 0
      I/TestView: onMeasure(int widthMeasureSpec, int heightMeasureSpec) widthMeasureSpec = 1073743152 heightMeasureSpec = 1073743848
      I/TestView: onMeasure(int widthMeasureSpec, int heightMeasureSpec) widthMeasureSpec = 1073743152 heightMeasureSpec = 1073743848
      I/TestView: onMeasure(int widthMeasureSpec, int heightMeasureSpec) widthMeasureSpec = 1073743152 heightMeasureSpec = 1073744016
      I/TestView: onMeasure(int widthMeasureSpec, int heightMeasureSpec) widthMeasureSpec = 1073743152 heightMeasureSpec = 1073744016
      I/TestView: onSizeChanged(int w, int h, int oldw, int oldh) w = 1328 h = 2192 oldw = 0 oldh0
      I/TestView: onLayout(boolean changed, int left, int top, int right, int bottom) changed = true left = 56 top = 56 right = 1384 bottom = 2248
      I/TestView: onMeasure(int widthMeasureSpec, int heightMeasureSpec) widthMeasureSpec = 1073743152 heightMeasureSpec = 1073743820
      I/TestView: onSizeChanged(int w, int h, int oldw, int oldh) w = 1328 h = 1996 oldw = 1328 oldh2192
      I/TestView: onLayout(boolean changed, int left, int top, int right, int bottom) changed = true left = 56 top = 56 right = 1384 bottom = 2052
      I/TestView: onDraw(Canvas canvas)
      I/TestView: onWindowFocusChanged(boolean hasWindowFocus) hasWindowFocus = true
      I/TestView: onMeasure(int widthMeasureSpec, int heightMeasureSpec) widthMeasureSpec = 1073743152 heightMeasureSpec = 1073743820
      I/TestView: onMeasure(int widthMeasureSpec, int heightMeasureSpec) widthMeasureSpec = 1073743152 heightMeasureSpec = 1073743820
      I/TestView: onLayout(boolean changed, int left, int top, int right, int bottom) changed = false left = 56 top = 56 right = 1384 bottom = 2052
      I/TestView: onDraw(Canvas canvas)
    • 销毁

      1
      2
      3
      I/TestView: onWindowFocusChanged(boolean hasWindowFocus) hasWindowFocus = false
      I/TestView: onWindowVisibilityChanged(int visibility) visibility = 8
      I/TestView: onDetachedFromWindow()
  2. android:visibility=invisible

    • 创建

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      I/TestView: onVisibilityChanged(View changedView, int visibility) changedView = com.example.junyizhou.rxjavademo.TestView{3ead3d52 I.ED.... ........ 0,0-0,0} visibility = 4
      I/TestView: TestView(Context context, AttributeSet attrs)
      I/TestView: onFinishInflate()
      I/TestView: onVisibilityChanged(View changedView, int visibility) changedView = com.android.internal.policy.impl.PhoneWindow$DecorView{3aeb2b95 I.E..... R.....ID 0,0-0,0} visibility = 4
      I/TestView: onVisibilityChanged(View changedView, int visibility) changedView = com.android.internal.policy.impl.PhoneWindow$DecorView{3aeb2b95 V.E..... R.....ID 0,0-0,0} visibility = 0
      I/TestView: onAttachedToWindow()
      I/TestView: onWindowVisibilityChanged(int visibility) visibility = 0
      I/TestView: onMeasure(int widthMeasureSpec, int heightMeasureSpec) widthMeasureSpec = 1073743152 heightMeasureSpec = 1073743848
      I/TestView: onMeasure(int widthMeasureSpec, int heightMeasureSpec) widthMeasureSpec = 1073743152 heightMeasureSpec = 1073743848
      I/TestView: onMeasure(int widthMeasureSpec, int heightMeasureSpec) widthMeasureSpec = 1073743152 heightMeasureSpec = 1073744016
      I/TestView: onMeasure(int widthMeasureSpec, int heightMeasureSpec) widthMeasureSpec = 1073743152 heightMeasureSpec = 1073744016
      I/TestView: onSizeChanged(int w, int h, int oldw, int oldh) w = 1328 h = 2192 oldw = 0 oldh0
      I/TestView: onLayout(boolean changed, int left, int top, int right, int bottom) changed = true left = 56 top = 56 right = 1384 bottom = 2248
      I/TestView: onMeasure(int widthMeasureSpec, int heightMeasureSpec) widthMeasureSpec = 1073743152 heightMeasureSpec = 1073743820
      I/TestView: onSizeChanged(int w, int h, int oldw, int oldh) w = 1328 h = 1996 oldw = 1328 oldh2192
      I/TestView: onLayout(boolean changed, int left, int top, int right, int bottom) changed = true left = 56 top = 56 right = 1384 bottom = 2052
      I/TestView: onWindowFocusChanged(boolean hasWindowFocus) hasWindowFocus = true
      I/TestView: onMeasure(int widthMeasureSpec, int heightMeasureSpec) widthMeasureSpec = 1073743152 heightMeasureSpec = 1073743820
      I/TestView: onMeasure(int widthMeasureSpec, int heightMeasureSpec) widthMeasureSpec = 1073743152 heightMeasureSpec = 1073743820
      I/TestView: onLayout(boolean changed, int left, int top, int right, int bottom) changed = false left = 56 top = 56 right = 1384 bottom = 2052
    • 销毁

      1
      2
      3
      I/TestView: onWindowFocusChanged(boolean hasWindowFocus) hasWindowFocus = false
      I/TestView: onWindowVisibilityChanged(int visibility) visibility = 8
      I/TestView: onDetachedFromWindow()
  3. android:visibility=gone

    • 创建

      1
      2
      3
      4
      5
      6
      7
      8
      I/TestView: onVisibilityChanged(View changedView, int visibility) changedView = com.example.junyizhou.rxjavademo.TestView{3ead3d52 G.ED.... ......I. 0,0-0,0} visibility = 8
      I/TestView: TestView(Context context, AttributeSet attrs)
      I/TestView: onFinishInflate()
      I/TestView: onVisibilityChanged(View changedView, int visibility) changedView = com.android.internal.policy.impl.PhoneWindow$DecorView{3aeb2b95 I.E..... R.....ID 0,0-0,0} visibility = 4
      I/TestView: onVisibilityChanged(View changedView, int visibility) changedView = com.android.internal.policy.impl.PhoneWindow$DecorView{3aeb2b95 V.E..... R.....ID 0,0-0,0} visibility = 0
      I/TestView: onAttachedToWindow()
      I/TestView: onWindowVisibilityChanged(int visibility) visibility = 0
      I/TestView: onWindowFocusChanged(boolean hasWindowFocus) hasWindowFocus = true
    • 销毁

      1
      2
      3
      I/TestView: onWindowFocusChanged(boolean hasWindowFocus) hasWindowFocus = false
      I/TestView: onWindowVisibilityChanged(int visibility) visibility = 8
      I/TestView: onDetachedFromWindow()

总结

从中我们可以看出:

  1. View默认为可见的,不是默认值时先调用onVisibilityChanged(),但是此时该View的尺寸、位置等信息都不知道。
  2. 可见性改变后才是调用带有两个参数的构造函数,当然,如果该View不是在layout中定义的话,会调用一个参数的构造函数。
  3. 从XMl文件中inflate完成(onFinishInflate())。
  4. 将View加到window中(View是gone的,那么View创建生命周期也就结束)。
  5. 测量view的长宽(onMeasure())。
  6. 定位View 在父View中的位置(onLayout()),若View是invisible,则View的创建生命周期结束。
  7. 绘制View的content(onDraw()),只有可见的View才在window中绘制。
  8. View的销毁流程和可见性没有关系。

综上所述:View的关键生命周期为:

1
[改变可见性] --> 构造View() --> onFinishInflate() --> onAttachedToWindow() --> onMeasure() --> onSizeChanged() --> onLayout() --> onDraw() --> onDetackedFromWindow()

自定义View时我们不可避免的要和View生命周期相关函数打交道,可能需要重新其中的某个或某几个来满足定制的需求,因此了解View的生命周期是Android程序猿进阶的必经之路。当然,我们没必要重新所有的方法,如果我们只是单纯的想把一个Bitmap画到View上,那我们只要重写View的onDraw方法就可以了,事实上自定义View的大部分情况我们也只是关注这个方法。onDraw方法的入口参数是android.graphics.Canvas,如何把文字、图像等绘制到View上就要了解这个类,下一篇我们一起了解一下。

JunyiZhou

JunyiZhou

陷阵之志,有死无生

4 日志
5 标签
GitHub 新浪微博

友情链接

个人简历 GitHub 稀土掘金 简书
© 2016 JunyiZhou
由 Hexo 强力驱动
主题 - NexT.Mist