一款好用的底部导航栏(NavigationBar)

Demo地址:https://github.com/Pedestrian0209/NavigationBar

该导航栏结合fragment实现,代码结构简单,每个item通过自定义view的方式绘制出来,只需设置一些简单的参数,即可达到想要的效果,支持文字提示、圆点提示等功能,效果如下图:


image.png

代码结构:

image.png

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();
    }

你可能感兴趣的:(一款好用的底部导航栏(NavigationBar))