美文网首页Android Android控件android UI系列专题
Android最好用的底部导航栏,开源框架

Android最好用的底部导航栏,开源框架

作者: 嘤嘤嘤999 | 来源:发表于2018-09-12 17:34 被阅读171次

    转载自这个项目的github地址:https://github.com/xubinhong/BottomBar

    这个底部导航栏的特点:

    1.告别xml中的item布局,一切icon、title统统绘制得出;

    2.扁平化,由于icon、title都是绘制得出的,所以只需要一个view即可,无需父布局

    3.为你处理好碎片切换事务,告别冗余代码,让你从此光速开发

    4.不怕需求变动,拔插式体验,增删item,只需修改1行代码

    5.源代码十分简单,有助于使用者开发高度适配自身需求的底部

    使用方式

    1.只需要到给出的github地址中拷贝BottomBar类到你的包下即可,或者自己创建一个类名字叫BottomBar,复制如下代码并导包:

    public class BottomBar extends View{
    
        private Context context;
    
        public BottomBar(Context context, @Nullable AttributeSet attrs) {
            super(context, attrs);
            this.context = context;
        }
    
        //////////////////////////////////////////////////
        //提供的api 并且根据api做一定的物理基础准备
        //////////////////////////////////////////////////
    
        private int containerId;
    
        private List<Class> fragmentClassList = new ArrayList<>();
        private List<String> titleList = new ArrayList<>();
        private List<Integer> iconResBeforeList = new ArrayList<>();
        private List<Integer> iconResAfterList = new ArrayList<>();
    
        private List<Fragment> fragmentList = new ArrayList<>();
    
        private int itemCount;
    
        private Paint paint = new Paint();
    
        private List<Bitmap> iconBitmapBeforeList = new ArrayList<>();
        private List<Bitmap> iconBitmapAfterList = new ArrayList<>();
        private List<Rect> iconRectList = new ArrayList<>();
    
        private int currentCheckedIndex;
        private int firstCheckedIndex;
    
        private int titleColorBefore = Color.parseColor("#999999");
        private int titleColorAfter = Color.parseColor("#ff5d5e");
    
        private int titleSizeInDp = 10;
        private int iconWidth = 20;
        private int iconHeight = 20;
        private int titleIconMargin = 5;
    
        public BottomBar setContainer(int containerId) {
            this.containerId = containerId;
            return this;
        }
    
        public BottomBar setTitleBeforeAndAfterColor(String beforeResCode, String AfterResCode) {//支持"#333333"这种形式
            titleColorBefore = Color.parseColor(beforeResCode);
            titleColorAfter = Color.parseColor(AfterResCode);
            return this;
        }
    
        public BottomBar setTitleSize(int titleSizeInDp) {
            this.titleSizeInDp = titleSizeInDp;
            return this;
        }
    
        public BottomBar setIconWidth(int iconWidth) {
            this.iconWidth = iconWidth;
            return this;
        }
    
        public BottomBar setTitleIconMargin(int titleIconMargin) {
            this.titleIconMargin = titleIconMargin;
            return this;
        }
    
        public BottomBar setIconHeight(int iconHeight) {
            this.iconHeight = iconHeight;
            return this;
        }
    
        public BottomBar addItem(Class fragmentClass, String title, int iconResBefore, int iconResAfter) {
            fragmentClassList.add(fragmentClass);
            titleList.add(title);
            iconResBeforeList.add(iconResBefore);
            iconResAfterList.add(iconResAfter);
            return this;
        }
    
        public BottomBar setFirstChecked(int firstCheckedIndex) {//从0开始
            this.firstCheckedIndex = firstCheckedIndex;
            return this;
        }
    
        public void build() {
            itemCount = fragmentClassList.size();
            //预创建bitmap的Rect并缓存
            //预创建icon的Rect并缓存
            for (int i = 0; i < itemCount; i++) {
                Bitmap beforeBitmap = getBitmap(iconResBeforeList.get(i));
                iconBitmapBeforeList.add(beforeBitmap);
    
                Bitmap afterBitmap = getBitmap(iconResAfterList.get(i));
                iconBitmapAfterList.add(afterBitmap);
    
                Rect rect = new Rect();
                iconRectList.add(rect);
    
                Class clx = fragmentClassList.get(i);
                try {
                    Fragment fragment = (Fragment) clx.newInstance();
                    fragmentList.add(fragment);
                } catch (InstantiationException | IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
    
            currentCheckedIndex = firstCheckedIndex;
            switchFragment(currentCheckedIndex);
    
            invalidate();
        }
    
        private Bitmap getBitmap(int resId) {
            BitmapDrawable bitmapDrawable = (BitmapDrawable) context.getResources().getDrawable(resId);
            return bitmapDrawable.getBitmap();
        }
    
        //////////////////////////////////////////////////
        //初始化数据基础
        //////////////////////////////////////////////////
    
        @Override
        protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
            super.onLayout(changed, left, top, right, bottom);
            initParam();
        }
    
        private int titleBaseLine;
        private List<Integer> titleXList = new ArrayList<>();
    
        private int parentItemWidth;
    
        private void initParam() {
            if (itemCount != 0) {
                //单个item宽高
                parentItemWidth = getWidth() / itemCount;
                int parentItemHeight = getHeight();
    
                //图标边长
                int iconWidth = dp2px(this.iconWidth);//先指定20dp
                int iconHeight = dp2px(this.iconHeight);
    
                //图标文字margin
                int textIconMargin = dp2px(((float)titleIconMargin)/2);//先指定5dp,这里除以一半才是正常的margin,不知道为啥,可能是图片的原因
    
                //标题高度
                int titleSize = dp2px(titleSizeInDp);//这里先指定10dp
                paint.setTextSize(titleSize);
                Rect rect = new Rect();
                paint.getTextBounds(titleList.get(0), 0, titleList.get(0).length(), rect);
                int titleHeight = rect.height();
    
                //从而计算得出图标的起始top坐标、文本的baseLine
                int iconTop = (parentItemHeight - iconHeight - textIconMargin - titleHeight)/2;
                titleBaseLine = parentItemHeight - iconTop;
    
                //对icon的rect的参数进行赋值
                int firstRectX = (parentItemWidth - iconWidth) / 2;//第一个icon的左
                for (int i = 0; i < itemCount; i++) {
                    int rectX = i * parentItemWidth + firstRectX;
    
                    Rect temp = iconRectList.get(i);
    
                    temp.left = rectX;
                    temp.top = iconTop ;
                    temp.right = rectX + iconWidth;
                    temp.bottom = iconTop + iconHeight;
                }
    
                //标题(单位是个问题)
                for (int i = 0; i < itemCount; i ++) {
                    String title = titleList.get(i);
                    paint.getTextBounds(title, 0, title.length(), rect);
                    titleXList.add((parentItemWidth - rect.width()) / 2 + parentItemWidth * i);
                }
            }
        }
    
        private int dp2px(float dpValue) {
            float scale = context.getResources().getDisplayMetrics().density;
            return (int) (dpValue * scale + 0.5f);
        }
    
        //////////////////////////////////////////////////
        //根据得到的参数绘制
        //////////////////////////////////////////////////
    
        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);//这里让view自身替我们画背景 如果指定的话
    
            if (itemCount != 0) {
                //画背景
                paint.setAntiAlias(false);
                for (int i = 0; i < itemCount; i++) {
                    Bitmap bitmap = null;
                    if (i == currentCheckedIndex) {
                        bitmap = iconBitmapAfterList.get(i);
                    } else {
                        bitmap = iconBitmapBeforeList.get(i);
                    }
                    Rect rect = iconRectList.get(i);
                    canvas.drawBitmap(bitmap, null, rect, paint);//null代表bitmap全部画出
                }
    
                //画文字
                paint.setAntiAlias(true);
                for (int i = 0; i < itemCount; i ++) {
                    String title = titleList.get(i);
                    if (i == currentCheckedIndex) {
                        paint.setColor(titleColorAfter);
                    } else {
                        paint.setColor(titleColorBefore);
                    }
                    int x = titleXList.get(i);
                    canvas.drawText(title, x, titleBaseLine, paint);
                }
            }
        }
    
        //////////////////////////////////////////////////
        //点击事件:我观察了微博和掌盟,发现down和up都在该区域内才响应
        //////////////////////////////////////////////////
    
        int target = -1;
    
        @SuppressLint("ClickableViewAccessibility")
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN :
                    target = withinWhichArea((int)event.getX());
                    break;
                case MotionEvent.ACTION_UP :
                    if (event.getY() < 0) {
                        break;
                    }
                    if (target == withinWhichArea((int)event.getX())) {
                        //这里触发点击事件
                        switchFragment(target);
                        currentCheckedIndex = target;
                        invalidate();
                    }
                    target = -1;
                    break;
            }
            return true;
            //这里return super为什么up执行不到?是因为return super的值,全部取决于你是否
            //clickable,当你down事件来临,不可点击,所以return false,也就是说,而且你没
            //有设置onTouchListener,并且控件是ENABLE的,所以dispatchTouchEvent的返回值
            //也是false,所以在view group的dispatchTransformedTouchEvent也是返回false,
            //这样一来,view group中的first touch target就是空的,所以intercept标记位
            //果断为false,然后就再也进不到循环取孩子的步骤了,直接调用dispatch-
            // TransformedTouchEvent并传孩子为null,所以直接调用view group自身的dispatch-
            // TouchEvent了
        }
    
        private int withinWhichArea(int x) { return x/parentItemWidth; }//从0开始
    
        //////////////////////////////////////////////////
        //碎片处理代码
        //////////////////////////////////////////////////
        private Fragment currentFragment;
    
        //注意 这里是只支持AppCompatActivity 需要支持其他老版的 自行修改
        protected void switchFragment(int whichFragment) {
            Fragment fragment = fragmentList.get(whichFragment);
            int frameLayoutId = containerId;
    
            if (fragment != null) {
                FragmentTransaction transaction = ((AppCompatActivity)context).getSupportFragmentManager().beginTransaction();
                if (fragment.isAdded()) {
                    if (currentFragment != null) {
                        transaction.hide(currentFragment).show(fragment);
                    } else {
                        transaction.show(fragment);
                    }
                } else {
                    if (currentFragment != null) {
                        transaction.hide(currentFragment).add(frameLayoutId, fragment);
                    } else {
                        transaction.add(frameLayoutId, fragment);
                    }
                }
                currentFragment = fragment;
                transaction.commit();
            }
        }
    }
    
    

    2.xml中

    <com.example.bottombar.BottomBar
        android:background="#FFFFFF"
        android:id="@+id/bottom_bar"
        android:layout_width="match_parent"
        android:layout_height="46dp"
        android:layout_gravity="bottom" />
    

    3.java代码中

    BottomBar bottomBar = findViewById(R.id.bottom_bar);
    bottomBar.setContainer(R.id.fl_container)
            .setTitleBeforeAndAfterColor("#999999", "#ff5d5e")
            .addItem(Fragment1.class,
                    "首页",
                    R.drawable.item1_before,
                    R.drawable.item1_after)
            .addItem(Fragment2.class,
                    "订单",
                    R.drawable.item2_before,
                    R.drawable.item2_after)
            .addItem(Fragment3.class,
                    "我的",
                    R.drawable.item3_before,
                    R.drawable.item3_after)
            .build();
    

    设置了容器frame layout

    设置了字体选中前后的颜色

    增加了item,并且给item绑定碎片,设定选中前后的drawable以及文本

    就这么简单的代码,就搞定了一切!效果如下:

    image.png

    而如果你正常写一个底部导航栏是怎样的?

    1.item布局,你还得精心设置半天

    2.底部title布局,引入若干

    3.title布局放到主布局中

    4.java代码中要通过findViewById找到所有item

    5.给所有item设置点击事件

    6.点击事件内作碎片的切换

    7.当你如果要增加一个item的时候,前面的又要大幅度修改,而且代码冗余程度极高

    如果你对里面icon、title的位置不满意,有更多的api供你选择

    setTitleSize,以dp为单位

    setIconWidth,图标宽度

    setIconHeight,图标高度

    setTitleIconMargin,标题图标间距

    setFirstChecked,设置第一个默认选中item

    由于源代码简单,易于阅读,开发者更可以自行修改源码

    底部导航栏设计思路

    根据api中获取的参数,计算出icon、title的精确位置,并在onDraw中绘制

    在onTouchEvent里,根据触摸点,获知点击区域,响应Icon、title的更改事件以及碎片的切换事件

    另一个BottomBar的实战应用可以看:

    https://blog.csdn.net/qq_36523667/article/details/79983010

    再次证明了,BottomBar,实在太方便啦!

    相关文章

      网友评论

      • yemoumou:人生在世不称意,明朝散发弄扁舟-简书朋友你好,我是币圈一老友,我的写作方向是区块链和数字货币,初到简书,望多多关照。互粉互赞,已赞,期待您的回赞哦。-椦
        嘤嘤嘤999:你开心就好

      本文标题:Android最好用的底部导航栏,开源框架

      本文链接:https://www.haomeiwen.com/subject/fykbgftx.html