Demo地址:https://github.com/Pedestrian0209/NavigationBar
该导航栏结合fragment实现,代码结构简单,每个item通过自定义view的方式绘制出来,只需设置一些简单的参数,即可达到想要的效果,支持文字提示、圆点提示等功能,效果如下图:
代码结构:
BottomNavigationItemView
该类为底部导航栏的item,所包含的元素为:图片、标题、圆点提示或者文字提示等,每个元素均是通过canvas绘制出来,所以该类等主要代码即为onDraw的实现:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (paint == null) {
paint = new Paint();
}
paint.setAntiAlias(true);
//绘制图片
int iconWidth = isActive ? (activeIcon == null ? 0 : activeIcon.getIntrinsicWidth())
: (inActiveIcon == null ? 0 : inActiveIcon.getIntrinsicWidth());
int iconHeight = isActive ? (activeIcon == null ? 0 : activeIcon.getIntrinsicHeight())
: (inActiveIcon == null ? 0 : inActiveIcon.getIntrinsicHeight());
Drawable icon = isActive ? (activeIcon == null ? null : activeIcon)
: (inActiveIcon == null ? null : inActiveIcon);
if (icon != null) {
icon.setBounds((getWidth() - iconWidth) / 2, activeItemPaddingTop,
(getWidth() + iconWidth) / 2, activeItemPaddingTop + iconHeight);
icon.draw(canvas);
}
//绘制标题
if (!TextUtils.isEmpty(title)) {
paint.setTextSize(isActive ? activeTextSize : inActiveTextSize);
paint.setColor(isActive ? activeTextColor : inActiveTextColor);
//测量标题的宽度
int titleWidth = (int) paint.measureText(title);
Paint.FontMetrics metrics = paint.getFontMetrics();
canvas.drawText(title, (getWidth() - titleWidth) / 2,
getHeight() - (isActive ? activeItemPaddingBottom : inActiveItemPaddingBottom) - metrics.bottom, paint);
}
//绘制圆点提示
if (showTipDot) {
paint.setColor(tipBgColor);
canvas.drawCircle((getWidth() + iconWidth) / 2 - tipMarginLeft + tipDotRadius,
(isActive ? activeItemPaddingTop : inActiveItemPaddingTop) + tipMarginTop + tipDotRadius, tipDotRadius, paint);
}
//根据提示文字是否为空来判断是否绘制文字提示
if (!TextUtils.isEmpty(tipMessage)) {
paint.setColor(tipBgColor);
paint.setTextSize(tipTextSize);
int msgWidth = (int) paint.measureText(tipMessage);
Path path = new Path();
int left = (getWidth() + iconWidth) / 2 - tipMarginLeft;
int top = (isActive ? activeItemPaddingTop : inActiveItemPaddingTop) + tipMarginTop;
int right = left + msgWidth + tipBgCornerRadius;
int bottom = top + tipBgCornerRadius * 2;
float[] radius = {tipBgCornerRadius, tipBgCornerRadius, tipBgCornerRadius, tipBgCornerRadius,
tipBgCornerRadius, tipBgCornerRadius, tipBgCornerRadius, tipBgCornerRadius};
path.addRoundRect(new RectF(left, top, right, bottom), radius, Path.Direction.CW);
canvas.drawPath(path, paint);
paint.setColor(tipTextColor);
Paint.FontMetrics metrics = paint.getFontMetrics();
canvas.drawText(tipMessage, left + tipBgCornerRadius / 2,
top + tipBgCornerRadius - metrics.top / 2 - metrics.bottom / 2, paint);
}
}
从上至下一次为绘制图片、绘制标题、绘制原点提示、绘制文字提示,图片和标题的字体大小、颜色等因为该item当前的选择状态而有所不同。
可以看到ondraw里面有很多自定义变量,这些变量即用来控制当前item里面所有元素的状态和位置的,变量简介如下:
//导航栏item的字体大小
private int activeTextSize, inActiveTextSize;
//导航栏item的字体颜色
private int activeTextColor, inActiveTextColor;
//导航栏item顶部和底部的间距
private int activeItemPaddingTop, inActiveItemPaddingTop, activeItemPaddingBottom, inActiveItemPaddingBottom;
//导航栏item上的提示(点/数字/文字等)的位置,以item的图片的右边距和上边距为准
private int tipMarginLeft, tipMarginTop;
//导航栏item上的提示文字的大小
private int tipTextSize;
//导航栏item上的提示文字的颜色
private int tipTextColor;
//导航栏item上的提示文字或点的背景颜色
private int tipBgColor;
//导航栏item上的提示为文字时的背景圆角半径 为圆点时的圆点半径
private int tipBgCornerRadius, tipDotRadius;
相信看了这些变量就基本可以布局item里面各元素的位置了,接下来就是如何给这些变量赋值以及如何控制当前item的状态了。
该类里面定义了一个Builder类,通过构建模式来给上述变量赋值,具体实现可看Demo。
控制item的状态实现了三个方法:
-void setActive(boolean isActive):设置当前item是否为选中状态
-void showTip(String tipMessage):设置当前item展示文字提示,如果为空,则不展示
-void showTipDot(boolean showTipDot):设置当前item展示原点提示
BottomNavigationView
该类即是BottomNavigationItemView的父容器了,用于动态添加、控制item以及和fragment进行交互,也是需要在xml布局文件里面实现的。
xml布局文件实现如下:
自定义属性
从这里可以看出BottomNavigationItemView里面的变量均是在这个地方定义的,也是由BottomNavigationView传递进来的,实现如下:
/**
* 初始化底部导航栏的所有item
*/
private void initItemViews() {
if (fragments == null || fragments.isEmpty()) {
return;
}
removeAllViews();
LayoutParams layoutParams = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
layoutParams.weight = 1;
int count = fragments.size();
for (int i = 0; i < count; i++) {
IFragment fragment = fragments.get(i);
BottomNavigationItemView itemView = new BottomNavigationItemView.Builder(getContext())
.setTitle(fragment.getTitle())
.setIcon(fragment.getActiveIconRes(), fragment.getInActiveIconRes())
.setTitleTextColor(activeTextColor, inActiveTextColor)
.setTitleTextSize(activeTextSize, inActiveTextSize)
.setPaddingTopAndBottom(activeItemPaddingTop, inActiveItemPaddingTop, activeItemPaddingBottom, inActiveItemPaddingBottom)
.setTipMarginLeftAndTop(tipMarginLeft, tipMarginTop)
.setTipTextSize(tipTextSize)
.setTipTextColor(tipTextColor)
.setTipBgColor(tipBgColor)
.setTipBgCornerRadius(tipBgCornerRadius)
.setTipDotRadius(tipDotRadius)
.build();
itemView.setLayoutParams(layoutParams);
itemView.setId(i);
itemView.setOnClickListener(this);
addView(itemView);
}
switchFragment(0);
}
initFragments方法
先看一下实现:
public void initFragments(FragmentManager fragmentManager, int containerId, List fragments) {
this.fragmentManager = fragmentManager;
this.containerId = containerId;
this.fragments = fragments;
initItemViews();
}
可以看出该方法即是整个底部导航栏内容初始化的入口,调用了该方法,则基础的导航栏功能就可以正常使用了。
IFragment
该类是一个接口,也是每个fragment必须实现的接口,可自行扩展,代码如下:
public interface IFragment {
/**
* 获取底部导航栏标题
*
* @return
*/
String getTitle();
/**
* 获取底部导航栏已选中图片
*
* @return
*/
int getActiveIconRes();
/**
* 获取底部导航栏未选中图片
*
* @return
*/
int getInActiveIconRes();
/**
* 同一个导航栏item被连续点击时调用,可用于回到顶部,刷新列表等
*/
void onContinueClick();
/**
* 是否拦截点击事件
*
* @return
*/
boolean onInterceptClick(Context context);
}
实现了一些通用的方法,底部导航栏item的图片、标题等也是各个fragment自行管理。
最后看看每个fragment是如何进行切换的吧:
public void switchFragment(int index) {
if (fragments == null || index >= fragments.size()) {
return;
}
//当前的fragment
IFragment currentFragment = curIndex < 0 ? null : fragments.get(curIndex);
//将要跳转的fragment
IFragment nextFragment = fragments.get(index);
if (curIndex == index) {
//同一个导航栏item被连续点击时调用
if (currentFragment != null) {
currentFragment.onContinueClick();
}
return;
}
//检测是否拦截点击事件
if (nextFragment.onInterceptClick(getContext())) {
return;
}
FragmentTransaction transaction = fragmentManager.beginTransaction();
if (currentFragment != null) {
int curIndex = fragments.indexOf(currentFragment);
((BottomNavigationItemView) getChildAt(curIndex)).setActive(false);
if (((Fragment) currentFragment).isAdded()) {
transaction.hide((Fragment) currentFragment);
}
}
((BottomNavigationItemView) getChildAt(index)).setActive(true);
if (((Fragment) nextFragment).isAdded()) {
transaction.show((Fragment) nextFragment);
} else {
transaction.add(containerId, (Fragment) nextFragment);
}
curIndex = index;
transaction.commit();
}