Android 底部导航控件实例代码

时间:2022-06-21 04:53:13

一、先给大家展示下最终效果

Android 底部导航控件实例代码Android 底部导航控件实例代码Android 底部导航控件实例代码

通过以上可以看到,图一是简单的使用,图二、图三中为结合viewpager共同使用,而且都可以随viewpager的滑动渐变色,不同点是图二为选中非选中两张图片,图三的选中非选中是一张图片只是做了颜色变化。

二、 需求

我们希望做可以做成这样的,可以在xml布局中引入控件并绑定数据,在代码中设置监听回调,并且配置使用要非常简单!

三、需求分析

根据我们多年做不明确需求项目的经验,以上需求还算明确。那么我们可以采用在linearlayout添加子view控件,这个子view控件就是我们自定义的每个tab条目,当然对linearlayout要设置权重。

需求大致明确之后就先设计每个条目的子view控件,这个子view控件是一个可以切换状态变化的,一张、两张都可以切换状态(参考图一、图三)。那么这个view要可以设置底部显示的文字,设置选中时颜色、未选中时颜色、选中时图片、未选中时图片、文字大小、设置是否有指示点、设置指示点大小、设置指示点图片等等。

四、tab条目接口

通过需求分析,我们可以定义如下的tab子view操作接口:

Android 底部导航控件实例代码

仔细的朋友会发现,为什么在接口中没有设置选中图片以及设置非选中时图片,那是因为这个属性不是通用的,在不同的实现中再去定义。

五、tab条目实现

Android 底部导航控件实例代码

类的继承关系及说明:

类图如下所示:

Android 底部导航控件实例代码

在tabviewbase中主要的方法就是测量,其他的都是对接口的简单实现。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@override
protected void onmeasure(int widthmeasurespec, int heightmeasurespec) {
super.onmeasure(widthmeasurespec, heightmeasurespec);
// 得到绘制icon的宽
int bitmapwidth = math.min(getmeasuredwidth() - getpaddingleft()
- getpaddingright(), getmeasuredheight() - getpaddingtop()
- getpaddingbottom() - mtextbound.height());
int left = getmeasuredwidth() / 2 - bitmapwidth / 2;
int top = (getmeasuredheight() - mtextbound.height()) / 2 - bitmapwidth / 2;
// 设置icon的绘制范围
miconrect = new rect(left, top, left + bitmapwidth, top + bitmapwidth);
// 设置指示点的范围
int indicatorradius = mindicatorsize / 2;
int tabrealheight = bitmapwidth + mtextbound.height();
mindicatorrect = new rect(left + tabrealheight* 4/5 - indicatorradius, top, left+tabrealheight* 4/5 + indicatorradius, top + mindicatorsize);
}

在以上代码中可以看到,测量文字的高度,用控件的高度减去文字的高度和控件的宽度对比,取较小的为图片的大小,也就是设置的图片要为正方形,否则会产生变形。

看下普通两张图片切换的tabview的绘制:

Android 底部导航控件实例代码

?
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
@override
protected void ondraw(canvas canvas) {
super.ondraw(canvas);
setuptargetbitmap(canvas);
drawindicator(canvas);
if(null != mtext) {
drawtargettext(canvas);
}
}
/**
* 绘制图标图片
* @param canvas
*/
private void setuptargetbitmap(canvas canvas) {
canvas.drawbitmap(isselected ? mselectediconbitmap : munselectediconbitmap, null, miconrect, null);
}
/**
* 绘制指示点
* @param canvas
*/
protected void drawindicator(canvas canvas) {
if(isindicatedisplay) {
canvas.drawbitmap(mindicatorbitmap, null, mindicatorrect, null);
}
}
/**
* 绘制文字
* @param canvas
*/
protected void drawtargettext(canvas canvas) {
mtextpaint.setcolor(isselected ? mselectedcolor : munselectedcolor);
canvas.drawtext(mtext, miconrect.left + miconrect.width() / 2
- mtextbound.width() / 2,
miconrect.bottom + mtextbound.height(), mtextpaint);
}

可以看到非常的简单,就是绘制图标图片以及绘制指示点,在绘制图标图片时判断当前条目是否在选中状态,根据是否选中来绘制不同的图片,在绘制指示点的时候首先判断下是否设置了显示指示点。如果有底部文字,那么久绘制底部文字。
在viewpager两张图片图片的时,我们再把效果图拿过来观察下:

Android 底部导航控件实例代码

这里有两种模式,即随着viewpager的滚动图标渐变及普通变化。ok,了解之后我们就能很轻松的来编写它的绘制了,可以通过绘制两张图片,但是在绘制的时候控制它的透明度就可以啦,是不是也很简单。

?
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
@override
protected void ondraw(canvas canvas) {
super.ondraw(canvas);
int alpha = (int) math.ceil((255 * malpha));
drawsourcebitmap(canvas, alpha);
drawtargetbitmap(canvas, alpha);
if(null != mtext) {
drawsourcetext(canvas, alpha);
drawtargettext(canvas, alpha);
}
drawindicator(canvas);
}
/**
* 绘制未选中图标
* @param canvas
* @param alpha
*/
private void drawsourcebitmap(canvas canvas, int alpha) {
mpaint.setantialias(true);
mpaint.setdither(true);
mpaint.setalpha(255 - alpha);
canvas.drawbitmap(munselectediconbitmap, null, miconrect, mpaint);
}
/**
* 绘制选中图标
* @param canvas
* @param alpha
*/
private void drawtargetbitmap(canvas canvas, int alpha) {
mpaint.setantialias(true);
mpaint.setdither(true);
mpaint.setalpha(alpha);
canvas.drawbitmap(mselectediconbitmap, null, miconrect, mpaint);
}
/**
* 画未选中文字
* @param canvas
* @param alpha
*/
private void drawsourcetext(canvas canvas, int alpha) {
mtextpaint.settextsize(mtextsize);
mtextpaint.setcolor(munselectedcolor);
mtextpaint.setalpha(255 - alpha);
canvas.drawtext(mtext, miconrect.left + miconrect.width() / 2
- mtextbound.width() / 2,
miconrect.bottom + mtextbound.height(), mtextpaint);
}
/**
* 画选中文字
* @param canvas
* @param alpha
*/
private void drawtargettext(canvas canvas, int alpha) {
mtextpaint.setcolor(mselectedcolor);
mtextpaint.setalpha(alpha);
canvas.drawtext(mtext, miconrect.left + miconrect.width() / 2
- mtextbound.width() / 2,
miconrect.bottom + mtextbound.height(), mtextpaint);
}

代码中的malpha是viewpager滚动的百分比,然后分别绘制选中以及未选中的图标和文本,但是绘制的时候设置的透明度不同,这样就会有一个渐变的效果。

在viewpager单张图片图片的时,我们再把效果图拿过来观察下:

Android 底部导航控件实例代码

?
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
private void setuptargetbitmap(int alpha) {
mbitmap = bitmap.createbitmap(getmeasuredwidth(), getmeasuredheight(), config.argb_8888);
mcanvas = new canvas(mbitmap);
mpaint = new paint();
mpaint.setcolor(mselectedcolor);
mpaint.setantialias(true);
mpaint.setdither(true);
mpaint.setalpha(alpha);
mcanvas.drawrect(miconrect, mpaint);
mpaint.setxfermode(new porterduffxfermode(porterduff.mode.dst_in));
mpaint.setalpha(255);
mcanvas.drawbitmap(miconbitmap, null, miconrect, mpaint);
}
private void drawsourcetext(canvas canvas, int alpha) {
mtextpaint.settextsize(mtextsize);
mtextpaint.setcolor(munselectedcolor);
mtextpaint.setalpha(255 - alpha);
canvas.drawtext(mtext, miconrect.left + miconrect.width() / 2
- mtextbound.width() / 2,
miconrect.bottom + mtextbound.height(), mtextpaint);
}
private void drawtargettext(canvas canvas, int alpha) {
mtextpaint.setcolor(mselectedcolor);
mtextpaint.setalpha(alpha);
canvas.drawtext(mtext, miconrect.left + miconrect.width() / 2
- mtextbound.width() / 2,
miconrect.bottom + mtextbound.height(), mtextpaint);
}

绘制的过程大致与两张图片相同,不同点就是在绘制图片的时候paint设置 xfermode,来控制颜色的渐变。

ok,tab条目的自定义view搞定之后剩下的就简单多了。

六、定义属性

接下来就是封装继承自linearlayout的整体控件,先来定义下属性。

Android 底部导航控件实例代码

可以看到tabicons为单张图片渐变效果特殊的,tabselectedicons和tabunselectedicon为两张图标切换效果特殊的。

七、 控件编写

由于三中样式有公共的部分,我们进行积累抽取。类图结构如下:

Android 底部导航控件实例代码

1. 构造函数初始化自定义属性

在tabindicatorbase中初始化自定义属性

?
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
private void init(context context, attributeset attrs) {
setorientation(linearlayout.horizontal);
setgravity(gravity.center);
//load defaults from resources
final resources res = getresources();
final int defaultselectedcolor = res.getcolor(r.color.default_tab_view_selected_color);
final int defaultunselectedcolor = res.getcolor(r.color.default_tab_view_unselected_color);
final float defaulttextsize = res.getdimension(r.dimen.default_tab_view_text_size);
final float defaulttabpadding = res.getdimension(r.dimen.default_tab_view_padding);
final float defaultindicatorsize = res.getdimension(r.dimen.default_tab_view_indicator_size);
// styleables from xml
typedarray a = context.obtainstyledattributes(attrs, r.styleable.tabindicator);
// 读取布局中,各个tab使用的文字
if (a.hasvalue(r.styleable.tabindicator_tablabels)) {
mlabels = a.gettextarray(r.styleable.tabindicator_tablabels);
}
mselectedcolor = a.getcolor(r.styleable.tabindicator_tabselectedcolor, defaultselectedcolor);
munselectedcolor = a.getcolor(r.styleable.tabindicator_tabunselectedcolor, defaultunselectedcolor);
mtextsize = (int) a.getdimension(r.styleable.tabindicator_tabtextsize, defaulttextsize);
mindicatorsize = (int) a.getdimension(r.styleable.tabindicator_tabindicatorsize, defaultindicatorsize);
mtabpadding = (int) a.getdimension(r.styleable.tabindicator_tabitempadding, defaulttabpadding);
handlestyledattributes(a);
a.recycle();
initview();
}

由于有些属性不是公共的,这里定义handlestyleattributes(a)的抽象方法,在子类中去实现。

2. 初始化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
/**
* 初始化控件
*/
private void initview() {
layoutparams params = new layoutparams(0, layoutparams.match_parent, 1);
params.gravity = gravity.center;
int size = gettabsize();
for (int i = 0; i < size; i++) {
final int index = i;
t tabitemview = createtabview();
tabitemview.setpadding(mtabpadding, mtabpadding, mtabpadding, mtabpadding);
// 图标及文字
if(null != mlabels) {
tabitemview.settext(mlabels[index]);
tabitemview.settextsize(mtextsize);
}
tabitemview.setselectedcolor(mselectedcolor);
tabitemview.setunselectedcolor(munselectedcolor);
tabitemview.setindicatorsize(mindicatorsize);
setproperties(tabitemview, i);
this.addview(tabitemview, params);
tabitemview.settag(index); // checkedtextview设置索引作为tag,以便后续更改颜色、图片等
mcheckedlist.add(tabitemview); // 将checkedtextview添加到list中,便于操作
tabitemview.setonclicklistener(new onclicklistener()
@override
public void onclick(view v) {
settabsdisplay(index); // 设置底部图片和文字的显示
if (null != mtablistener) {
mtablistener.ontabselected(index); // tab项被选中的回调事件
}
}
});
// 初始化 底部菜单选中状态,默认第一个选中
if (i == 0) {
tabitemview.setselected(true);
} else {
tabitemview.setselected(false);
}
}
}

生成tab条目以及设置特殊的属性都通过抽象方法的方式交给子类去完成。

?
1
2
3
4
5
6
7
8
9
10
/**
* 生成tabview
* @return
*/
protected abstract t createtabview();
/**
* 设置特殊属性
* @param t
*/
protected abstract void setproperties(t t, int index);

3. 子类实现

由于这里都比较简单,我们选取其中一个简单的双图标图片来说明:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@override
protected void handlestyledattributes(typedarray a) {
// 读取布局中,各个tab使用的图标
int selectediconsresid = a.getresourceid(r.styleable.tabindicator_tabselectedicons, 0);
typedarray ta = getcontext().getresources().obtaintypedarray(selectediconsresid);
int len = ta.length();
mselecteddrawableids = new int[len];
for(int i = 0; i < len; i++) {
mselecteddrawableids[i] = ta.getresourceid(i, 0);
}
int unselectediconsresid = a.getresourceid(r.styleable.tabindicator_tabunselectedicons, 0);
ta = getcontext().getresources().obtaintypedarray(unselectediconsresid);
len = ta.length();
munselecteddrawableids = new int[len];
for(int i = 0; i < len; i++) {
munselecteddrawableids[i] = ta.getresourceid(i, 0);
}
ta.recycle();
}

这里读取了xml中配置的选中及未选中图标

生成tabview

?
1
2
3
4
@override
protected tabview createtabview() {
return new tabview(getcontext());
}

设置特殊属性

?
1
2
3
4
5
@override
protected void setproperties(tabview tabview, int index) {
tabview.setselectedicon(mselecteddrawableids[index]);
tabview.setunselectedicon(munselecteddrawableids[index]);
}

获取条目个数

?
1
2
3
4
@override
protected int gettabsize() {
return mselecteddrawableids.length;
}

八、源码及示例

给大家提供一个github的地址: android-tabindicator
另外,欢迎 star or f**k me on github!

九、一行引入库

如果您的项目使用 gradle 构建, 只需要在您的build.gradle文件添加下面一行到 dependencies :
compile 'com.kevin:tabindicator:1.0.1'

关于小编给大家分享的android 底部导航控件实例代码就到此结束了,希望对大家有所帮助!