Android最好用的底部導(dǎo)航欄,開源框架

轉(zhuǎn)載自這個(gè)項(xiàng)目的github地址:https://github.com/xubinhong/BottomBar

這個(gè)底部導(dǎo)航欄的特點(diǎn):

1.告別xml中的item布局,一切icon、title統(tǒng)統(tǒng)繪制得出;

2.扁平化,由于icon、title都是繪制得出的,所以只需要一個(gè)view即可,無需父布局

3.為你處理好碎片切換事務(wù),告別冗余代碼,讓你從此光速開發(fā)

4.不怕需求變動(dòng),拔插式體驗(yàn),增刪item,只需修改1行代碼

5.源代碼十分簡單,有助于使用者開發(fā)高度適配自身需求的底部

使用方式

1.只需要到給出的github地址中拷貝BottomBar類到你的包下即可,或者自己創(chuàng)建一個(gè)類名字叫BottomBar,復(fù)制如下代碼并導(dǎo)包:

public class BottomBar extends View{

    private Context context;

    public BottomBar(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        this.context = context;
    }

    //////////////////////////////////////////////////
    //提供的api 并且根據(jù)api做一定的物理基礎(chǔ)準(zhǔn)備
    //////////////////////////////////////////////////

    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();
        //預(yù)創(chuàng)建bitmap的Rect并緩存
        //預(yù)創(chuàng)建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();
    }

    //////////////////////////////////////////////////
    //初始化數(shù)據(jù)基礎(chǔ)
    //////////////////////////////////////////////////

    @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) {
            //單個(gè)item寬高
            parentItemWidth = getWidth() / itemCount;
            int parentItemHeight = getHeight();

            //圖標(biāo)邊長
            int iconWidth = dp2px(this.iconWidth);//先指定20dp
            int iconHeight = dp2px(this.iconHeight);

            //圖標(biāo)文字margin
            int textIconMargin = dp2px(((float)titleIconMargin)/2);//先指定5dp,這里除以一半才是正常的margin,不知道為啥,可能是圖片的原因

            //標(biāo)題高度
            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();

            //從而計(jì)算得出圖標(biāo)的起始top坐標(biāo)、文本的baseLine
            int iconTop = (parentItemHeight - iconHeight - textIconMargin - titleHeight)/2;
            titleBaseLine = parentItemHeight - iconTop;

            //對icon的rect的參數(shù)進(jìn)行賦值
            int firstRectX = (parentItemWidth - iconWidth) / 2;//第一個(gè)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;
            }

            //標(biāo)題(單位是個(gè)問題)
            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);
    }

    //////////////////////////////////////////////////
    //根據(jù)得到的參數(shù)繪制
    //////////////////////////////////////////////////

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

    //////////////////////////////////////////////////
    //點(diǎn)擊事件:我觀察了微博和掌盟,發(fā)現(xiàn)down和up都在該區(qū)域內(nèi)才響應(yīng)
    //////////////////////////////////////////////////

    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())) {
                    //這里觸發(fā)點(diǎn)擊事件
                    switchFragment(target);
                    currentCheckedIndex = target;
                    invalidate();
                }
                target = -1;
                break;
        }
        return true;
        //這里return super為什么up執(zhí)行不到?是因?yàn)閞eturn super的值,全部取決于你是否
        //clickable,當(dāng)你down事件來臨,不可點(diǎn)擊,所以return false,也就是說,而且你沒
        //有設(shè)置onTouchListener,并且控件是ENABLE的,所以dispatchTouchEvent的返回值
        //也是false,所以在view group的dispatchTransformedTouchEvent也是返回false,
        //這樣一來,view group中的first touch target就是空的,所以intercept標(biāo)記位
        //果斷為false,然后就再也進(jìn)不到循環(huán)取孩子的步驟了,直接調(diào)用dispatch-
        // TransformedTouchEvent并傳孩子為null,所以直接調(diào)用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();

設(shè)置了容器frame layout

設(shè)置了字體選中前后的顏色

增加了item,并且給item綁定碎片,設(shè)定選中前后的drawable以及文本

就這么簡單的代碼,就搞定了一切!效果如下:

image.png

而如果你正常寫一個(gè)底部導(dǎo)航欄是怎樣的?

1.item布局,你還得精心設(shè)置半天

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

3.title布局放到主布局中

4.java代碼中要通過findViewById找到所有item

5.給所有item設(shè)置點(diǎn)擊事件

6.點(diǎn)擊事件內(nèi)作碎片的切換

7.當(dāng)你如果要增加一個(gè)item的時(shí)候,前面的又要大幅度修改,而且代碼冗余程度極高

如果你對里面icon、title的位置不滿意,有更多的api供你選擇

setTitleSize,以dp為單位

setIconWidth,圖標(biāo)寬度

setIconHeight,圖標(biāo)高度

setTitleIconMargin,標(biāo)題圖標(biāo)間距

setFirstChecked,設(shè)置第一個(gè)默認(rèn)選中item

由于源代碼簡單,易于閱讀,開發(fā)者更可以自行修改源碼

底部導(dǎo)航欄設(shè)計(jì)思路

根據(jù)api中獲取的參數(shù),計(jì)算出icon、title的精確位置,并在onDraw中繪制

在onTouchEvent里,根據(jù)觸摸點(diǎn),獲知點(diǎn)擊區(qū)域,響應(yīng)Icon、title的更改事件以及碎片的切換事件

另一個(gè)BottomBar的實(shí)戰(zhàn)應(yīng)用可以看:

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

再次證明了,BottomBar,實(shí)在太方便啦!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容