如何裁剪图片(圆角图片)

概览:

  1. 采用Porter-Duff 图片合成方法
  2. 采用Shader着色器重新绘制图片
  3. 不规则图片裁剪
  4. 心形图片裁剪
  5. 参考链接

先看下效果:

1. 采用Porter-Duff 图片合成方法

先说说Porter-Duff是什么意思:Porter-Duff是Thomas Porter 和 Tom Duff 的简称,就是两个人名字的合成。

Porter-Duff 操作是 1 组 12 项用于描述数字图像合成的基本手法,包括
Clear、Source Only、Destination Only、Source Over、Source In、Source
Out、Source Atop、Destination Over、Destination In、Destination
Out、Destination Atop、XOR。通过组合使用 Porter-Duff 操作,可完成任意 2D
图像的合成。

合成图片,顾名思义就是拿两张图片取需要的部分放到第三张图片上,合成一张新的图片。

看看我们采用的两张图片吧:

绿色的mask并不会把小狗整成绿色,因为合成的时候只取了mask的形状,alpha值为0。代码如下:

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
public class Part1Fragment extends Fragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_layout, container, false);
ImageView image = (ImageView) view.findViewById(R.id.image);
Bitmap dog = BitmapFactory.decodeResource(container.getResources(), R.drawable.betty);
Bitmap mask = BitmapFactory.decodeResource(container.getResources(), R.drawable.mask);
image.setImageBitmap(combineImages(mask, dog));
dog.recycle();
mask.recycle();
return view;
}
public Bitmap combineImages(Bitmap mask, Bitmap dog) {
Bitmap bmp;
int width = mask.getWidth() > dog.getWidth() ? mask.getWidth() : dog.getWidth();
int height = mask.getHeight() > dog.getHeight() ? mask.getHeight() : dog.getHeight();
bmp = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Paint paint = new Paint();
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_ATOP));
Canvas canvas = new Canvas(bmp);
canvas.drawBitmap(mask, 0, 0, null);
canvas.drawBitmap(dog, 0, 0, paint);
return bmp;
}
}

合成之后就变成圆角图片了,如下:

原理: 我们先是用两张图片的最大尺寸创建了一个mutableBitmap,用来作为Canvas的画东西的地方。然后先画了mask,即Dst,接着把画笔PaintXFerMode设置成SRC_ATOP,然后把dog画上去,这样就实现了裁剪效果。

估计你不太理解我说的东西,看下面一张图你就明白了(蓝色正方形是Src,就是你将要画上去的东西,黄色圆圈是Dst,即原来画布上有的东西):

上面是一种合成方式,看另外一种合成方式:

好了,效果是达到了,但这样做有没有问题呢?问题如下:

  • mask的尺寸必须和原图一致,我们当然可以缩放mask,但如果缩放的宽高比和原图不一致会出现失真。
  • 最大的问题还是效率!为了实现裁剪,我们加载了两个图,如果图片很大就会OutOfMemoryError

2. 采用Shader着色器重新绘制图片

Shaders着色器让我们可以在画东西的时候定义填充风格,Shaders是设置在画笔上的。BitmapShader是用一张Bitmap着色,而且还支持三种瓦片铺盖方式。所谓瓦片铺盖方式就是当我们画的区域比采用的Bitmap还大时,超出部分该怎么画。如下图(小方块就代表Bitmap):

代码如下:

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
public class Part2Fragment extends Fragment {
private static final float RADIUS_FACTOR = 8.0f;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_layout, container, false);
ImageView image = (ImageView)view.findViewById(R.id.image);
Bitmap bitmap = BitmapFactory.decodeResource(container.getResources(), R.drawable.betty);
image.setImageBitmap(processImage(bitmap));
bitmap.recycle();
return view;
}
public Bitmap processImage(Bitmap bitmap) {
Bitmap bmp;
bmp = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888);
BitmapShader shader = new BitmapShader(bitmap, BitmapShader.TileMode.CLAMP, BitmapShader.TileMode.CLAMP);
float radius = Math.min(bitmap.getWidth(), bitmap.getHeight()) / RADIUS_FACTOR;
Canvas canvas = new Canvas(bmp);
Paint paint = new Paint();
paint.setAntiAlias(true);
paint.setShader(shader);
RectF rect = new RectF(0, 0, bitmap.getWidth(), bitmap.getHeight());
canvas.drawRoundRect(rect, radius, radius, paint);
return bmp;
}
}

效果如下:

3. 不规则图片裁剪

对话框气泡,原理是一样的,看代码如下:

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
public class Part3Fragment extends Fragment {
private static final float RADIUS_FACTOR = 8.0f;
private static final int TRIANGLE_WIDTH = 120;
private static final int TRIANGLE_HEIGHT = 100;
private static final int TRIANGLE_OFFSET = 300;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_layout, container, false);
ImageView image = (ImageView)view.findViewById(R.id.image);
Bitmap bitmap = BitmapFactory.decodeResource(container.getResources(), R.drawable.betty);
image.setImageBitmap(processImage(bitmap));
bitmap.recycle();
return view;
}
public Bitmap processImage(Bitmap bitmap) {
Bitmap bmp;
bmp = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888);
BitmapShader shader = new BitmapShader(bitmap, BitmapShader.TileMode.CLAMP, BitmapShader.TileMode.CLAMP);
float radius = Math.min(bitmap.getWidth(), bitmap.getHeight()) / RADIUS_FACTOR;
Canvas canvas = new Canvas(bmp);
Paint paint = new Paint();
paint.setAntiAlias(true);
paint.setShader(shader);
RectF rect = new RectF(TRIANGLE_WIDTH, 0, bitmap.getWidth(), bitmap.getHeight());
canvas.drawRoundRect(rect, radius, radius, paint);
Path triangle = new Path();
triangle.moveTo(0, TRIANGLE_OFFSET);
triangle.lineTo(TRIANGLE_WIDTH, TRIANGLE_OFFSET - (TRIANGLE_HEIGHT / 2));
triangle.lineTo(TRIANGLE_WIDTH, TRIANGLE_OFFSET + (TRIANGLE_HEIGHT / 2));
triangle.close();
canvas.drawPath(triangle, paint);
return bmp;
}
}

效果如下:

4. 心形图片裁剪

先设置BitmapShader, Canvas, 和 Paint 对象:

1
2
3
4
5
6
7
8
9
10
11
12
Bitmap bmp;
bmp = Bitmap.createBitmap(bitmap.getWidth(),
bitmap.getHeight(), Bitmap.Config.ARGB_8888);
BitmapShader shader = new BitmapShader(bitmap,
BitmapShader.TileMode.CLAMP,
BitmapShader.TileMode.CLAMP);
Canvas canvas = new Canvas(bmp);
Paint paint = new Paint();
paint.setAntiAlias(true);
paint.setShader(shader);

再初始化一些后面需要用到的东西:

1
2
3
4
5
6
float width = bitmap.getWidth();
float height = bitmap.getHeight();
Path oval = new Path();
Matrix matrix = new Matrix();
Region region = new Region();

把长方形变成椭圆:

1
2
3
4
RectF ovalRect = new RectF(width / 8, 0,
width - (width / 8), height);
oval.addOval(ovalRect, Path.Direction.CW);

得到如下图形:

旋转30度:

1
2
matrix.postRotate(30, width / 2, height / 2);
oval.transform(matrix, oval);

得到如下图形:

再用Region裁剪:

1
2
3
region.setPath(oval, new Region((int)width / 2, 0,
(int)width, (int)height));
canvas.drawPath(region.getBoundaryPath(), paint);

得到如下图形:

同理再画另一边,画之前先复位:

1
2
matrix.reset();
oval.reset();
1
2
3
4
5
6
oval.addOval(ovalRect, Path.Direction.CW);
matrix.postRotate(-30, width / 2, height / 2);
oval.transform(matrix, oval);
region.setPath(oval,
new Region(0, 0, (int)width / 2, (int)height));
canvas.drawPath(region.getBoundaryPath(), paint);

得到如下图形:

全部代码放一起如下:

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
public class Part4Fragment extends Fragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_layout, container, false);
ImageView image = (ImageView)view.findViewById(R.id.image);
Bitmap bitmap = BitmapFactory.decodeResource(container.getResources(), R.drawable.betty);
image.setImageBitmap(processImage(bitmap));
bitmap.recycle();
return view;
}
public Bitmap processImage(Bitmap bitmap) {
Bitmap bmp;
bmp = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888);
BitmapShader shader = new BitmapShader(bitmap, BitmapShader.TileMode.CLAMP, BitmapShader.TileMode.CLAMP);
Canvas canvas = new Canvas(bmp);
Paint paint = new Paint();
paint.setAntiAlias(true);
paint.setShader(shader);
float width = bitmap.getWidth();
float height = bitmap.getHeight();
Path oval = new Path();
Matrix matrix = new Matrix();
Region region = new Region();
RectF ovalRect = new RectF(width / 8, 0, width - (width / 8), height);
oval.addOval(ovalRect, Path.Direction.CW);
matrix.postRotate(30, width / 2, height / 2);
oval.transform(matrix, oval);
region.setPath(oval, new Region((int)width / 2, 0, (int)width, (int)height));
canvas.drawPath(region.getBoundaryPath(), paint);
matrix.reset();
oval.reset();
oval.addOval(ovalRect, Path.Direction.CW);
matrix.postRotate(-30, width / 2, height / 2);
oval.transform(matrix, oval);
region.setPath(oval, new Region(0, 0, (int)width / 2, (int)height));
canvas.drawPath(region.getBoundaryPath(), paint);
return bmp;
}
}

5. 参考链接

http://www.douban.com/note/143111853/

http://blog.stylingandroid.com/category/canvas/

http://chiuki.github.io/android-shaders-filters/#/

android touch事件传递机制

1.先看3张图吧

child不消费touch事件


child消费touch事件


ViewGroup截获touch事件

2.Android是怎么处理touch事件的?




3.自定义touch事件处理



4.系统提供的touch事件监听处理函数


5.更多关于多点触控和事件委托

文档见 http://vdisk.weibo.com/s/dyk_6CfP2FIz_

或者
http://files.cnblogs.com/sunzn/PRE_andevcon_mastering-the-android-touch-system.pdf

视频
https://www.youtube.com/watch?v=EZAoJU-nUyI

android 自定义ViewGroup

概论:

  1. 三种自定义的比较
  2. 继承自ViewGroup
  3. onMeasure()
  4. onLayout()
  5. 自定义LayoutParams
  6. 稍微说下draw吧
  7. 参考链接

1.三种自定义的比较

Custom Views

  • Extending the View
  • Overriding onMeasure()
  • Overriding onDraw()
  • Saving state using the BaseSavedState pattern
  • Working with custom attributes
  • Understanding and applying requestLayout and invalidate

In contrast, you can ignore the following details when creating custom views:

  • Overriding onLayout()
  • Implementating and using LayoutParams

Compound Controls

  • Extend an existing layout
  • Saving state using the BaseSavedState pattern
  • Taking control of saving state for its child views
  • Working with custom attributes

While you can ignore:

  • Overriding onMeasure()
  • Overriding onDraw()
  • Overriding onLayout()
  • Worrying about requestLayout() and invalidate()
  • Implementing and using LayoutParams

Custom Layouts

  1. Inherit from ViewGroup
  2. Override onMeasure()
  3. Override onLayout()
  4. Implement custom LayoutParams with any additional layout attributes
  5. Override layout parameters construction methods in the custom layout class

2.继承自ViewGroup

流水线布局的样子:

这里的代码和自定义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
public class FlowLayout extends ViewGroup
{
private static final String tag="FlowLayout";
private int hspace=10;
private int vspace=10;
public FlowLayout(Context context) {
super(context);
initialize(context);
}
public FlowLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
TypedArray t = context.obtainStyledAttributes(attrs,
R.styleable.FlowLayout, 0, 0);
hspace = t.getDimensionPixelSize(R.styleable.FlowLayout_hspace,
hspace);
vspace = t.getDimensionPixelSize(R.styleable.FlowLayout_vspace,
vspace);
Log.d(tag,"hspace:" + hspace);
Log.d(tag,"vspace:" + vspace);
t.recycle();
initialize(context);
}
public FlowLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
private void initialize(Context context) {
}
...
}

就是自定义属性多了个布局属性,如下:

1
2
3
4
5
6
7
8
9
<resources>
<declare-styleable name="FlowLayout">
<attr name="hspace" format="dimension"/>
<attr name="vspace" format="dimension" />
</declare-styleable>
<declare-styleable name="FlowLayout_Layout">
<attr name="layout_space" format="dimension"/>
</declare-styleable>
</resources>

3.onMeasure()

这里需要注意的地方比较多,也有点难理解。
由于是ViewGroup,你需要先measure children,但是比不能直接用child.measure(),因为这个方法完全没考虑ViewGroup的MeasureSpec,你应该用的是ViewGroup.measureChild()。到最后你setMeasuredDimension(),还需要用resolveSize(w, widthMeasureSpec)来计算ViewGroup的实际尺寸,因为你所有的children的尺寸有可能超出ViewGroup的尺寸,所以框架提供了这个方便的方法让我们使用。

看下这个图有助于了解接下来的代码:

示例代码如下:

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
65
66
67
68
69
70
71
72
73
74
75
//This is very basic
//doesn't take into account padding
//You can easily modify it to account for padding
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
//********************
//Initialize
//********************
int rw = MeasureSpec.getSize(widthMeasureSpec);
int rh = MeasureSpec.getSize(heightMeasureSpec);
int h = 0; //current height
int w = 0; //current width
int h1 = 0, w1=0; //Current point to hook the child to
//********************
//Loop through children
//********************
int numOfChildren = this.getChildCount();
for (int i=0; i < numOfChildren; i++ )
{
//********************
//Front of the loop
//********************
View child = this.getChildAt(i);
this.measureChild(child,widthMeasureSpec, heightMeasureSpec);
int vw = child.getMeasuredWidth();
int vh = child.getMeasuredHeight();
if (w1 + vw > rw)
{
//new line: max of current width and current width position
//when multiple lines are in play w could be maxed out
//or in uneven sizes is the max of the right side lines
//all lines don't have to have the same width
//some may be larger than others
w = Math.max(w,w1);
//reposition the point on the next line
w1 = 0; //start of the line
h1 = h1 + vh; //add view height to the current heigh
}
//********************
//Middle of the loop
//********************
int w2 = 0, h2 = 0; //new point for the next view
w2 = w1 + vw;
h2 = h1;
//latest height: current point + height of the view
//however if the previous height is larger use that one
h = Math.max(h,h1 + vh);
//********************
//Save the current coords for the view
//in its layout
//********************
LayoutParams lp = (LayoutParams)child.getLayoutParams();
lp.x = w1;
lp.y = h1;
//********************
//Restart the loop
//********************
w1=w2;
h1=h2;
}
//********************
//End of for
//********************
w = Math.max(w1,w);
//h = h;
setMeasuredDimension(
resolveSize(w, widthMeasureSpec),
resolveSize(h,heightMeasureSpec));
};

这里还需要说明的是当我们onLayout()的时候我们需要知道children的坐标和尺寸,尺寸不用担心,已经有了,但原点(左上角)坐标呢?这当然就是layout parameters的功用了,LayoutParams就是ViewGroup让children保存的数据,定义LayoutParams还是在ViewGroup中定义的。

1
2
3
LayoutParams lp = (LayoutParams)child.getLayoutParams();
int spacing = lp.spacing;
//Adjust your widths based on this spacing.

具体的LayoutParams的定义还有ViewGroup如何调用它稍后再讲。

4.onLayout()

这里没太多可说的,直接依次调用child.layout()方法就行了,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
protected void onLayout(boolean arg0, int arg1, int arg2, int arg3, int arg4)
{
//Call layout() on children
int numOfChildren = this.getChildCount();
for (int i=0; i < numOfChildren; i++ )
{
View child = this.getChildAt(i);
LayoutParams lp = (LayoutParams)child.getLayoutParams();
child.layout(lp.x,
lp.y,
lp.x + child.getMeasuredWidth(),
lp.y + child.getMeasuredHeight());
}
}

5.自定义LayoutParams

LayoutParams 是由child views生成和持有的,但是是在ViewGroup中定义的,原因很简单,ViewGroup需要child的这些信息,当然得由他来定义需要哪些信息。看代码吧:

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
//*********************************************************
//Custom Layout Definition
//*********************************************************
public static class LayoutParams extends ViewGroup.MarginLayoutParams {
public int spacing = -1;
public int x =0;
public int y =0;
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
TypedArray a =
c.obtainStyledAttributes(attrs, R.styleable.FlowLayout_Layout);
spacing = a.getDimensionPixelSize(R.styleable.FlowLayout_Layout_layout_space, 0);
Log.d(tag,"child spacing:" + spacing);
a.recycle();
}
public LayoutParams(int width, int height) {
super(width, height);
spacing = 0;
}
public LayoutParams(ViewGroup.LayoutParams p) {
super(p);
}
public LayoutParams(MarginLayoutParams source) {
super(source);
}
}//eof-layout-params

还有个问题,child的ViewGroup可以是LinearLayout 或者FlowLayout,child view是怎么知道该构造哪个布局的LayoutParams呢?这当然要Android SDK来解决了。当一个view被放到布局中时,框架会调用如下四个方法来构造view的LayoutParams

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
//*********************************************************
//Layout Param Support
//*********************************************************
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new FlowLayout.LayoutParams(getContext(), attrs);
}
@Override
protected LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}
@Override
protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
return new LayoutParams(p);
}
// Override to allow type-checking of LayoutParams.
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return p instanceof FlowLayout.LayoutParams;
}

6.稍微说下draw吧

你当然可以在viewGroup上画点东西,当你想在所有child都画好后再话东西的话,就用dispatchDraw(),当然你得先call super.dispatchDraw()。现在画的东西就是在最上层。如果你想在child之前画点东西的话,当然就要用onDraw()了,注意,你先得配置setWillNotDraw(false),因为默认ViewGroup是不会画东西的,就不会调用onDraw()

7.参考链接

http://reacoder.github.io/2014/11/23/android-good-video/

《ExpertAndroid》

android 自定义组合控件

概论:

  1. 说在前面的话
  2. 继承框架提供的布局
  3. 布局文件
  4. 构造方法
  5. 状态保存

1.说在前面的话

自定义组合控件还是蛮简单的,你不用measure,layout,draw,当然你想的话也可以在组合控件上做这些操作的,不过大多数时候是没有必要的。组合控件的自定义属性和自定义view完全一样,这里比较难的是状态保存,因为一个页面可以有很多个你自定义的组合控件,但是系统的状态保存是依赖viewID的,所以你不能让ViewGroup来管理你的控件状态,只能自己管理了。当然,你也可能不需要状态保存,那就很简单了。

下面说说需要经历的步骤吧:

  • 让自定义的组合控件继承框架提供的布局,如LinearLayout,RelativeLayout等。
  • 把你组合控件里面的子控件放到一个布局文件中,然后在组合控件的构造方法中加载进来。
  • 采用merge作为你布局文件的根,这样可以减少嵌套层次。
  • 接下来是比较难的状态保存。
  • 自定义属性,这个和自定义view一样。

2.继承框架提供的布局

1
2
3
4
5
public class CompoundControl
extends LinearLayout
implements android.view.View.OnClickListener
{
...

3.布局文件

样例如下:

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
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<LinearLayout
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
>
<TextView
android:id="@+id/fromDate"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="Enter From Date"
android:layout_weight="70"
/>
<Button
android:id="@+id/fromButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="Go"
android:layout_weight="30"
/>
</LinearLayout>
<LinearLayout
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
>
<TextView
android:id="@+id/toDate"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="Enter To Date"
android:layout_weight="70"
/>
<Button
android:id="@+id/toButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="Go"
android:layout_weight="30"
/>
</LinearLayout>
</merge>

4.构造方法

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
public DurationControl(Context context) {
super(context);
initialize(context);
}
public DurationControl(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
TypedArray t = context.obtainStyledAttributes(attrs,
R.styleable.DurationComponent, 0, 0);
durationUnits = t.getInt(R.styleable.DurationComponent_durationUnits,
durationUnits);
t.recycle();
initialize(context);
}
public DurationControl(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
private void initialize(Context context) {
LayoutInflater lif = (LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
lif.inflate(R.layout.duration_view_layout, this);
Button b = (Button) this.findViewById(R.id.fromButton);
b.setOnClickListener(this);
b = (Button) this.findViewById(R.id.toButton);
b.setOnClickListener(this);
this.setSaveEnabled(true);
}

5.状态保存

前面说过了,由于你控件可能会在一个页面出现多次,所以viewID会重复,这样系统就没办法帮你管理状态了。所以如果你要管理children的状态,就必须亲自动手了,而不是让children自己管理自己的状态。

要想管理children的状态,你必须要知道ViewGroup的四个状态管理的方法,他们是:

1
2
3
4
dispatchSaveInstanceState
dispatchFreezeSelfOnly
dispatchRestoreInstanceState
dispatchThawSelfOnly

A ViewGroup uses dispatchSaveInstanceState to first save its own state by calling super (view’s) dispatchSaveInstanceState, which in turn triggers onSaveInstanceState on itself and then calls the dispatchSaveInstanceState for each of its children. If the children are plain views and not ViewGroups, this will result in having their onSaveInstanceState called. Listing 2-12 presents the pseudo code for how these key methods are meshed together.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ViewGroup.dispatchSaveInstanceState() {
View.dispatchSaveInstanceState()
...ends up calling its own ViewGroup.onSaveInstanceState()
Children.dispatchSaveInstanceState()
...ends up calling children's onSaveInstanceState()
}
View.dispatchSaveInstanceState() {
onSaveInstanceState()
}
ViewGroup.dispatchFreezeSelfOnly() {
View.dispatchSaveInstanceState()
...ends up calling ViewGroup.onSaveInstanceState()
}

技巧在于dispatchFreezeSelfOnly,这个ViewGroup方法只是简单的保存了ViewGroup的状态,这同样也发生在对应的恢复dispatchThawSelfOnly上。所以你可以重写ViewGroup的方法来阻止children自己管理状态。如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Override
protected void dispatchSaveInstanceState(SparseArray<Parcelable> container)
{
//Don't call this so that children won't be explicitly saved
//super.dispatchSaveInstanceState(container);
//Call your self onsavedinstancestate
super.dispatchFreezeSelfOnly(container);
}
@Override
protected void dispatchRestoreInstanceState(
SparseArray<Parcelable> container)
{
//Don't call this so that children won't be explicitly saved
//super.dispatchRestoreInstanceState(container);
super.dispatchThawSelfOnly(container);
}

接下来就需要ViewGroup在onSaveInstanceStateonRestoreInstanceState中管理children的状态了,实例代码如下:

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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
@Override
protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
// Don't call this so that children won't be explicitly saved
// super.dispatchSaveInstanceState(container);
// Call your self onsavedinstancestate
super.dispatchFreezeSelfOnly(container);
Log.d(tag, "in dispatchSaveInstanceState");
}
@Override
protected void dispatchRestoreInstanceState(
SparseArray<Parcelable> container) {
// Don't call this so that children won't be explicitly saved
// .super.dispatchRestoreInstanceState(container);
super.dispatchThawSelfOnly(container);
Log.d(tag, "in dispatchRestoreInstanceState");
}
@Override
protected void onRestoreInstanceState(Parcelable state) {
Log.d(tag, "in onRestoreInstanceState");
if (!(state instanceof SavedState)) {
super.onRestoreInstanceState(state);
return;
}
// it is our state
SavedState ss = (SavedState) state;
// Peel it and give the child to the super class
super.onRestoreInstanceState(ss.getSuperState());
// this.fromDate = ss.fromDate;
// this.toDate= ss.toDate;
this.setFromDate(ss.fromDate);
this.setToDate(ss.toDate);
}
@Override
protected Parcelable onSaveInstanceState() {
Log.d(tag, "in onSaveInstanceState");
Parcelable superState = super.onSaveInstanceState();
SavedState ss = new SavedState(superState);
ss.fromDate = this.fromDate;
ss.toDate = this.toDate;
return ss;
}
/*
* ***************************************************************
* Saved State inner static class
* ***************************************************************
*/
public static class SavedState extends BaseSavedState {
// null values are allowed
private Calendar fromDate;
private Calendar toDate;
SavedState(Parcelable superState) {
super(superState);
}
SavedState(Parcelable superState, Calendar inFromDate, Calendar inToDate) {
super(superState);
fromDate = inFromDate;
toDate = inToDate;
}
@Override
public void writeToParcel(Parcel out, int flags) {
super.writeToParcel(out, flags);
if (fromDate != null) {
out.writeLong(fromDate.getTimeInMillis());
} else {
out.writeLong(-1L);
}
if (fromDate != null) {
out.writeLong(toDate.getTimeInMillis());
} else {
out.writeLong(-1L);
}
}
@Override
public String toString() {
StringBuffer sb = new StringBuffer("fromDate:"
+ DurationControl.getDateString(fromDate));
sb.append("fromDate:" + DurationControl.getDateString(toDate));
return sb.toString();
}
@SuppressWarnings("hiding")
public static final Parcelable.Creator<SavedState> CREATOR
= new Parcelable.Creator<SavedState>() {
public SavedState createFromParcel(Parcel in) {
return new SavedState(in);
}
public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
// Read back the values
private SavedState(Parcel in) {
super(in);
// Read the from date
long lFromDate = in.readLong();
if (lFromDate == -1) {
fromDate = null;
} else {
fromDate = Calendar.getInstance();
fromDate.setTimeInMillis(lFromDate);
}
// Read the from date
long lToDate = in.readLong();
if (lFromDate == -1) {
toDate = null;
} else {
toDate = Calendar.getInstance();
toDate.setTimeInMillis(lToDate);
}
}
}// eof-state-class

其实这就相当于把整个组合控件当成一个View来管理

android 自定义View

概览:

  1. View 的生命周期
  2. Traversals
  3. Measure
  4. Draw
  5. Custom Attributes
  6. 监听事件很简单,不讲了
  7. 状态保存
  8. 参考链接

1. View 的生命周期

  • Attachment/detachment(一般不用care)
  • Traversals(必须care)
  • State save/restore(看你的需求了)

Attachment的回调接口是onAttachedToWindow(),在这里你可以做以下操作

  • Call super.onAttachedToWindow()!
  • Perform any relevant state resets
  • Start listening for state changes

detachment的回调接口是onDetachedFromWindow(),在这里你可以做以下操作

  • Call super.onDetachedFromWindow()!
  • Remove any posted Runnables
  • Stop listening for data changes
  • Clean up resources
    • Bitmaps
    • Threads

看看官方的SwipeRefreshLayout的运用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
removeCallbacks(mCancel);
removeCallbacks(mReturnToStartPosition);
}
@Override
public void onDetachedFromWindow() {
super.onDetachedFromWindow();
removeCallbacks(mReturnToStartPosition);
removeCallbacks(mCancel);
}

打个简单地比喻就是,如果你的View和网络操作相关的话,最好在这里做取消操作。它这里的操作其实就是取消或重置动画。好了,这里就不多说了,一般用不到。下面说说遍历Traversals。

2. Traversals

Traversals 分四个阶段

Animate—>Measure—>Layout—>Draw

我们现在关心的是Measure和Draw,分别对应onMeasure(),onDraw(),先看看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
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
protected void onDraw(Canvas canvas) {
}

现在来看看具体的Measure

3. Measure

如果你自定义的View没有特殊尺寸要求的话,也可以不重写onMeasure()。现在来说说比较难理解的MeasureSpec 模式,三张图搞定它

MeasureSpec.EXACTLY

MeasureSpec.AT_MOST

MeasureSpec.UNSPECIFIED

onMeasure()里由于没有回调,框架依赖你最后设置尺寸,所以你一定要调用setMeasuredDimension(),否则运行时会crash。

看看大牛Dave Smith 给的一个模板吧:

1
2
3
4
5
6
7
8
9
10
11
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//Get the width measurement
int widthSize = MeasureUtils.getMeasurement(widthMeasureSpec, getDesiredWidth());
//Get the height measurement
int heightSize = MeasureUtils.getMeasurement(heightMeasureSpec, getDesiredHeight());
//MUST call this to store the measurements
setMeasuredDimension(widthSize, heightSize);
}

工具方法

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 MeasureUtils {
/**
* Utility to return a view's standard measurement. Uses the
* supplied size when constraints are given. Attempts to
* hold to the desired size unless it conflicts with provided
* constraints.
*
* @param measureSpec Constraints imposed by the parent
* @param contentSize Desired size for the view
* @return The size the view should be.
*/
public static int getMeasurement(int measureSpec, int contentSize) {
int specMode = View.MeasureSpec.getMode(measureSpec);
int specSize = View.MeasureSpec.getSize(measureSpec);
int resultSize = 0;
switch (specMode) {
case View.MeasureSpec.UNSPECIFIED:
//Big as we want to be
resultSize = contentSize;
break;
case View.MeasureSpec.AT_MOST:
//Big as we want to be, up to the spec
resultSize = Math.min(contentSize, specSize);
break;
case View.MeasureSpec.EXACTLY:
//Must be the spec size
resultSize = specSize;
break;
}
return resultSize;
}
}

4. Draw

这其实没有太多要说的,onDraw()回调已经给你了一个Canvas,你要做的就是画各种你需要的东西了。看看Canvas的官方定义吧:

The Canvas class holds the “draw” calls. To draw something, you need 4 basic components: A Bitmap to hold the pixels, a Canvas to host the draw calls (writing into the bitmap), a drawing primitive (e.g. Rect, Path, text, Bitmap), and a paint (to describe the colors and styles for the drawing).

这里的Canvas已经包含一个mutable 的Bitmap了。需要注意的是onDraw()调用的很频繁,实例化对象的操作应该都放到外面。随便看个例子吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
public void onDraw(Canvas canvas) {
super.onDraw(canvas);
Log.d(tag,"onDraw called");
int w = this.getWidth();
int h = this.getHeight();
int t = this.getTop();
int l = this.getLeft();
int ox = w/2;
int oy = h/2;
int rad = Math.min(ox,oy)/2;
canvas.drawCircle(ox, oy, rad, getBrush());
}

其实这里的获取宽高的操作应该放到onSizeChanged()里面,这个是在onLayout()里调用的,而且一般只遍历一次。

5. Custom Attributes

这个没什么要讲的,看例子就理解了。

1
2
3
4
5
6
7
Listing 1-18. Defining Custom Attributes in attrs.xml
<resources>
<declare-styleable name="CircleView">
<attr name="strokeWidth" format="integer"/>
<attr name="strokeColor" format="color|reference" />
</declare-styleable>
</resources>

下面这段好好理解
Given the attrs.xml in Listing 1-18, Android generates the following IDs:

1
2
3
4
5
R.attr.strokeWidth (int)
R.attr.srokeColor (int)
R.styleable.CircelView (an array of ints)
R.styleable.CircleView_strokeWidth (offset into the array)
R.styleable.CircelView_strokeColor (offset into the array)

在初始化时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public CircleView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
//Use the array constant to read the bag once
TypedArray t = context.obtainStyledAttributes(attrs,
R.styleable.CircleView,
defStyle, //if any values are in the theme
0); //Do you have your own style group
//Use the offset in the bag to get your value
strokeColor = t.getColor(R.styleable.CircleView_strokeColor, mDefaultStrokeColor);
strokeWidth = t.getInt(R.styleable.CircleView_strokeWidth, mDefaultStrokeWidth);
//Recycle the typed array
t.recycle();
//Go ahead and initialize your class.
initCircleView();
}

DON’T FORGET: TypedArrays are heavyweight objects that should be recycled immediately after all the attributes you need have been extracted.

6. 监听事件很简单,不讲了

7. 状态保存

用系统的方式保存状态,万无一失,下面看模板:

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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
/*
* ***************************************************************
* Save and restore work
* ***************************************************************
*/
@Override
protected void onRestoreInstanceState(Parcelable p)
{
this.onRestoreInstanceStateStandard(p);
this.initCircleView();
}
@Override
protected Parcelable onSaveInstanceState()
{
return this.onSaveInstanceStateStandard();
}
/*
* ***************************************************************
* Use a simpler approach
* ***************************************************************
*/
private void onRestoreInstanceStateSimple(Parcelable p)
{
if (!(p instanceof Bundle))
{
throw new RuntimeException("unexpected bundle");
}
Bundle b = (Bundle)p;
defRadius = b.getInt("defRadius");
Parcelable sp = b.getParcelable("super");
//No need to call parent. It is just a base view
super.onRestoreInstanceState(sp);
}
private Parcelable onSaveInstanceStateSimple()
{
//Don't call the base class as it will return a null
Parcelable p = super.onSaveInstanceState();
Bundle b = new Bundle();
b.putInt("defRadius",defRadius);
b.putParcelable("super",p);
return b;
}
/*
* ***************************************************************
* Use a standard approach
* ***************************************************************
*/
private void onRestoreInstanceStateStandard(Parcelable state)
{
//If it is not yours doesn't mean it is BaseSavedState
//You may have a parent in your hierarchy that has their own
//state derived from BaseSavedState
//It is like peeling an onion or a Russian doll
if (!(state instanceof SavedState)) {
super.onRestoreInstanceState(state);
return;
}
//it is our state
SavedState ss = (SavedState)state;
//Peel it and give the child to the super class
super.onRestoreInstanceState(ss.getSuperState());
defRadius = ss.defRadius;
}
private Parcelable onSaveInstanceStateStandard()
{
Parcelable superState = super.onSaveInstanceState();
SavedState ss = new SavedState(superState);
ss.defRadius = this.defRadius;
return ss;
}
/*
* ***************************************************************
* Saved State inner static class
* ***************************************************************
*/
public static class SavedState extends BaseSavedState {
int defRadius;
SavedState(Parcelable superState) {
super(superState);
}
@Override
public void writeToParcel(Parcel out, int flags) {
super.writeToParcel(out, flags);
out.writeInt(defRadius);
}
//Read back the values
private SavedState(Parcel in) {
super(in);
defRadius = in.readInt();
}
@Override
public String toString() {
return "CircleView defRadius:" + defRadius;
}
@SuppressWarnings("hiding")
public static final Parcelable.Creator<SavedState> CREATOR
= new Parcelable.Creator<SavedState>() {
public SavedState createFromParcel(Parcel in) {
return new SavedState(in);
}
public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
}//eof-state-class

注意:你需要在初始化时,调用this.setSaveEnabled(true);,当然,状态保存也不是必须的,看你的需求了。

8. 参考链接

https://www.youtube.com/watch?v=NYtB6mlu7vA
https://thenewcircle.com/s/post/1663/tutorial_enhancing_android_ui_with_custom_views_dave_smith_video
https://dl.dropboxusercontent.com/u/16714463/Google%20IO%202014/Material%20Witness.pdf
http://developer.android.com/training/custom-views/index.html
http://developer.android.com/guide/topics/ui/custom-components.html

《Expert Android》