一. 前言

这个控件是为了做而做,中途还是遇到了很鬼畜的问题的。是因为我在开发我的软件工程课程设计-连连看的时候,需要用到这个控件。

Github地址:NumberOfItem

二. 自定义View

1. 自定义用到的属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//存储物品的图片描述
private ImageView show_img = null;
//物品的图片
private int srcResource;
//物品图片的背景样式
private int imgResource;

//存储物品的数量描述
private TextView show_count = null;
//物品数量的背景样式
private int countResource;
//存储物品的数量
private int count = 0;
//绘制物品数量的画笔
private Paint paint;

2. 在values文件夹下自定义属性

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="NumberOfItem">
<!--物品图片视图的图片资源-->
<attr name="srcResource" format="reference"/>
<!--物品图片视图的背景样式-->
<attr name="imgResource" format="reference"/>
<!--物品数量视图的背景样式-->
<attr name="countResource" format="reference"/>
</declare-styleable>
</resources>

3. 创建构造方法

构造方法还是Xml代码创建的时候进入的构造方法重要,在其中解析属性,设置属性。

在此之前,我们需要加载我们的布局文件。

在这里,我遇到了我第一个bug,解不开的。我本来是使用Java代码在onLayout手动创建视图,第一个bug’是onLayout多次测量,我解决了。但是第二个bug很无语,单独使用一个控件是没有问题的,但是使用多个,除了第一个以外,其他的都没有效果。

所以,我在这里使用了xml代码创建布局。本来想在onLayout中修改子View位置,结果发现出现上面同样的问题。最终放弃了在onLayout中的操作。

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
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content">

<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="5dp"
>
<ImageView
android:id="@+id/show_img"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/fight"
android:scaleType="fitXY"
android:background="@drawable/img_shape"
/>
</RelativeLayout>

<TextView
android:id="@+id/show_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/count_shape"
android:layout_alignParentEnd="true"
android:layout_alignParentBottom="true"
/>

</RelativeLayout>
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
/**
* 构造方法 Java代码创建的时候进入
* @param context
*/
public NumberOfItem(Context context,int srcResource,int imgResource,int countResource) {
super(context);

init();

this.srcResource = srcResource;
this.imgResource = imgResource;
this.countResource = countResource;

show_img.setImageResource(srcResource);
show_img.setBackgroundResource(imgResource);
show_count.setBackgroundResource(countResource);
}

/**
* 构造方法 Xml代码创建的时候进入
* @param context
* @param attrs
*/
public NumberOfItem(Context context, AttributeSet attrs) {
super(context, attrs);

init();

//解析自定义属性
if (attrs != null) {
//获取自定义属性值的集合
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.NumberOfItem);

//解析单个资源
srcResource = typedArray.getResourceId(
R.styleable.NumberOfItem_srcResource,
R.drawable.fight
);
show_img.setImageResource(srcResource);
imgResource = typedArray.getResourceId(
R.styleable.NumberOfItem_imgResource,
R.drawable.img_shape
);
show_img.setBackgroundResource(imgResource);
countResource = typedArray.getResourceId(
R.styleable.NumberOfItem_countResource,
R.drawable.count_shape
);
show_count.setBackgroundResource(countResource);

//释放资源
typedArray.recycle();
}
}

/**
* 初始化
*/
private void init() {
//画笔操作
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setTextSize(PxUtil.spToPx(14,getContext()));
paint.setTypeface(Typeface.DEFAULT_BOLD);
paint.setColor(Color.WHITE);

//加载视图
View inflate = View.inflate(getContext(), R.layout.number_item, this);
show_img = inflate.findViewById(R.id.show_img);
show_count = inflate.findViewById(R.id.show_count);
}

4. 在onMeasure中支持wrap_content属性

设置一个默认的宽高。

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
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);

//获取宽-测量规则的模式和大小
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);

//获取高-测量规则的模式和大小
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);

//设置wrap_content的默认宽 / 高值
//默认宽/高的设定并无固定依据,根据需要灵活设置
int mWidth = PxUtil.dpToPx(45,getContext());
int mHeight = PxUtil.dpToPx(45,getContext());

//当布局参数设置为wrap_content时,设置默认值
//宽,高任意一个布局参数为= wrap_content时,都设置默认值
if (getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT && getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT) {

//宽度,高度都设置wrap_content
setMeasuredDimension(mWidth, mHeight);
} else if (getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT) {

//宽度设置wrap_content
setMeasuredDimension(mWidth, heightSize);
} else if (getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT) {

//高度设置wrap_content
setMeasuredDimension(widthSize, mHeight);
}
}
5. 绘制物品数量

对于drawText方法,可以看这篇文章:Android Canvas的drawText()和文字居中方案,尤其是其中的文字居中方案。

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
@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);

//需要绘制的文本
String text = String.valueOf(count);

//计算文本宽度
float text_width = paint.measureText(text);

//获取字体fontMetrics
Paint.FontMetrics fontMetrics = paint.getFontMetrics();

//计算文本高度
float text_height = (fontMetrics.bottom - fontMetrics.top)/2 - fontMetrics.bottom;

//获取绘制的位置
int pivotX = show_count.getLeft() + (show_count.getRight()-show_count.getLeft()) / 2;
int pivotY = show_count.getTop() + (show_count.getBottom()-show_count.getTop()) / 2;

//绘制需要的字体
canvas.drawText(text,
pivotX-text_width/2,
pivotY+text_height,
paint);
}

//在count的set方法里面刷新
public void setCount(int count) {
this.count = count;

//刷新界面
invalidate();
}

6. 默认的资源文件

文本数量的默认背景,这里不仅控制了其背景颜色还有大小。

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">

<!--设置填充颜色-->
<solid android:color="#EB5476"/>

<!--设置大小-->
<size android:width="20dp"
android:height="20dp"
/>

</shape>

视图的总体背景,这里不仅设置了背景颜色和圆角,还设置了内间距。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">

<!--设置圆角-->
<corners android:radius="18dp"/>

<!--设置填充颜色-->
<solid android:color="#67C1D1"/>

<!--设置内间距-->
<padding android:left="5dp"
android:top="5dp"
android:right="5dp"
android:bottom="5dp"
/>

</shape>

这两个文件就是我们可以自己修改的文件,从而实现自己想要的样式。

三. 具体的使用

1. NumberOfItem

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
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
public class NumberOfItem extends RelativeLayout {
//存储物品的图片描述
private ImageView show_img = null;
//物品的图片
private int srcResource;
//物品图片的背景样式
private int imgResource;

//存储物品的数量描述
private TextView show_count = null;
//物品数量的背景样式
private int countResource;
//存储物品的数量
private int count = 0;
//绘制物品数量的画笔
private Paint paint;

/**
* 构造方法 Java代码创建的时候进入
* @param context
*/
public NumberOfItem(Context context) {
super(context);

init();
}

/**
* 构造方法 Xml代码创建的时候进入
* @param context
* @param attrs
*/
public NumberOfItem(Context context, AttributeSet attrs) {
super(context, attrs);

init();

//解析自定义属性
if (attrs != null) {
//获取自定义属性值的集合
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.NumberOfItem);

//解析单个资源
srcResource = typedArray.getResourceId(
R.styleable.NumberOfItem_srcResource,
R.drawable.fight
);
imgResource = typedArray.getResourceId(
R.styleable.NumberOfItem_imgResource,
R.drawable.img_shape
);
countResource = typedArray.getResourceId(
R.styleable.NumberOfItem_countResource,
R.drawable.count_shape
);

//释放资源
typedArray.recycle();
}
}

/**
* 初始化
*/
private void init() {
//画笔操作
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setTextSize(PxUtil.spToPx(14,getContext()));
paint.setTypeface(Typeface.DEFAULT_BOLD);
paint.setColor(Color.WHITE);

//加载视图
View inflate = View.inflate(getContext(), R.layout.number_item, this);
show_img = inflate.findViewById(R.id.show_img);
show_count = inflate.findViewById(R.id.show_count);
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);

//获取宽-测量规则的模式和大小
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);

//获取高-测量规则的模式和大小
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);

//设置wrap_content的默认宽 / 高值
//默认宽/高的设定并无固定依据,根据需要灵活设置
int mWidth = PxUtil.dpToPx(45,getContext());
int mHeight = PxUtil.dpToPx(45,getContext());

//当布局参数设置为wrap_content时,设置默认值
//宽,高任意一个布局参数为= wrap_content时,都设置默认值
if (getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT && getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT) {

//宽度,高度都设置wrap_content
setMeasuredDimension(mWidth, mHeight);
} else if (getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT) {

//宽度设置wrap_content
setMeasuredDimension(mWidth, heightSize);
} else if (getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT) {

//高度设置wrap_content
setMeasuredDimension(widthSize, mHeight);
}
}

@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);

//需要绘制的文本
String text = String.valueOf(count);

//计算文本宽度
float text_width = paint.measureText(text);

//获取字体fontMetrics
Paint.FontMetrics fontMetrics = paint.getFontMetrics();

//计算文本高度
float text_height = (fontMetrics.bottom - fontMetrics.top)/2 - fontMetrics.bottom;

//获取绘制的位置
int pivotX = show_count.getLeft() + (show_count.getRight()-show_count.getLeft()) / 2;
int pivotY = show_count.getTop() + (show_count.getBottom()-show_count.getTop()) / 2;

//绘制需要的字体
canvas.drawText(text,
pivotX-text_width/2,
pivotY+text_height,
paint);
}

//setter,getter方法
public int getCount() {
return count;
}

public void setCount(int count) {
this.count = count;

//刷新界面
invalidate();
}

public Paint getPaint() {
return paint;
}

public void setPaint(Paint paint) {
this.paint = paint;
}

public ImageView getShow_img() {
return show_img;
}

public void setShow_img(ImageView show_img) {
this.show_img = show_img;
}

public int getSrcResource() {
return srcResource;
}

public void setSrcResource(int srcResource) {
this.srcResource = srcResource;
}

public int getImgResource() {
return imgResource;
}

public void setImgResource(int imgResource) {
this.imgResource = imgResource;
}

public TextView getShow_count() {
return show_count;
}

public void setShow_count(TextView show_count) {
this.show_count = show_count;
}

public int getCountResource() {
return countResource;
}

public void setCountResource(int countResource) {
this.countResource = countResource;
}
}
1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="NumberOfItem">
<!--物品图片视图的图片资源-->
<attr name="srcResource" format="reference"/>
<!--物品图片视图的背景样式-->
<attr name="imgResource" format="reference"/>
<!--物品数量视图的背景样式-->
<attr name="countResource" format="reference"/>
</declare-styleable>
</resources>

2. PxToUtil

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
public class PxUtil {
/**
* 得到设备的密度
*/
public static float getScreenDensity(Context context) {
return context.getResources().getDisplayMetrics().density;
}

/**
* 将传递的 整型dp 值转化为 px
* @param dp
* @param context
* @return
*/
public static int dpToPx(int dp, Context context){
return (int) (dp * getScreenDensity(context));
}

/**
* 将传递的 浮点型dp 值转化为 px
* @param dp
* @param context
* @return
*/
public static int dpToPx(float dp, Context context){
return (int) (dp * getScreenDensity(context));
}

/**
* 将传递的 整型px 值转化为 dp
* @param px
* @param context
* @return
*/
public static float pxToDp(int px, Context context){
return (px / getScreenDensity(context));
}

/**
* 将传递的 浮点型px 值转化为 dp
* @param px
* @param context
* @return
*/
public static float pxToDp(float px, Context context){
return (px / getScreenDensity(context));
}

/**
* 将传递的 sp 值转化为 px
* @param sp
* @param context
* @return
*/
public static int spToPx(int sp, Context context){
return (int) (sp * getScreenDensity(context));
}

/**
* 将传递的 px 值 转化为 sp
* @param px
* @param context
* @return
*/
public static int pxToSp(int px, Context context){
return (int) (px / getScreenDensity(context));
}
}

3. 资源文件

count_shape.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">

<!--设置填充颜色-->
<solid android:color="#EB5476"/>

<!--设置大小-->
<size android:width="20dp"
android:height="20dp"
/>

</shape>

Img_shape.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">

<!--设置圆角-->
<corners android:radius="18dp"/>

<!--设置填充颜色-->
<solid android:color="#67C1D1"/>

<!--设置内间距-->
<padding android:left="5dp"
android:top="5dp"
android:right="5dp"
android:bottom="5dp"
/>

</shape>

number_item.xml

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
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content">

<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="5dp"
>
<ImageView
android:id="@+id/show_img"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/fight"
android:scaleType="fitXY"
android:background="@drawable/img_shape"
/>
</RelativeLayout>

<TextView
android:id="@+id/show_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/count_shape"
android:layout_alignParentEnd="true"
android:layout_alignParentBottom="true"
/>

</RelativeLayout>

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
32
33
34
35
36
37
38
39
40
41
42
43
44
public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

NumberOfItem item = findViewById(R.id.number_item);
NumberOfItem item1 = findViewById(R.id.number_item1);

item.setCount(10);
}
}

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<swu.xl.numberofitem.NumberOfItem
android:id="@+id/number_item"
android:layout_width="55dp"
android:layout_height="55dp"
/>

<swu.xl.numberofitem.NumberOfItem
android:id="@+id/number_item1"
android:layout_width="55dp"
android:layout_height="55dp"
android:layout_marginStart="80dp"
app:srcResource="@drawable/fight" />

<swu.xl.numberofitem.NumberOfItem
android:id="@+id/number_item2"
android:layout_width="55dp"
android:layout_height="55dp"
app:srcResource="@drawable/fight"
android:layout_marginStart="160dp"
/>

</RelativeLayout>

5. 运行效果