美文网首页view
中间凹陷的底部导航栏(NavigationView)+ 源码分析

中间凹陷的底部导航栏(NavigationView)+ 源码分析

作者: ZEKI安卓学弟 | 来源:发表于2019-11-26 14:21 被阅读0次

    中间凹陷的 BottomNavigationView(请滑倒最底部直接复制使用)


    直接上代码

    注:使用时一定先指定Background为透明色

    添加menu为奇数个,最中间item的icon title都为空  
    

    xml:

    <?xml version="1.0" encoding="utf-8"?>
    
            xmlns:android="http://schemas.android.com/apk/res/android"
    
            xmlns:app="http://schemas.android.com/apk/res-auto"
    
            xmlns:tools="http://schemas.android.com/tools"
    
            android:orientation="vertical"
    
            android:layout_width="match_parent"
    
            android:layout_height="match_parent"
    
            android:background="#00BCD4">
    
                android:layout_width="match_parent"
    
                android:layout_height="wrap_content"
    
                app:layout_constraintStart_toStartOf="parent"
    
                app:layout_constraintEnd_toEndOf="parent"
    
                android:background="#00FFFFFF"
    
                app:menu="@menu/navigation"
    
                app:layout_constraintBottom_toBottomOf="parent"/>
    
    </androidx.constraintlayout.widget.ConstraintLayout>
    

    Menu:

    <?xml version="1.0" encoding="utf-8"?>
    
    <menu xmlns:android="http://schemas.android.com/apk/res/android">
    
            android:id="@+id/navigation_home"
    
            android:icon="@drawable/nav_selector_home"
    
            android:title="首页" />
    
            android:id="@+id/navigation_find"
    
            android:icon="@drawable/nav_selector_find"
    
            android:title="发现" />
    
                android:id="@+id/navigation_null"
    
                android:icon="@null"
    
                android:title="@null"
    
            />
    
                android:id="@+id/navigation_message"
    
                android:icon="@drawable/nav_selector_message"
    
                android:title="消息" />
    
            android:id="@+id/navigation_mine"
    
            android:icon="@drawable/nav_selector_mine"
    
            android:title="我的" />
    
    </menu>
    

    GapNavigationView类:

    注:需先自行导入 BottomNavigationView

    public class GapNavigationView extends BottomNavigationView {
    
    Contextcontext;
    
        public GapNavigationView(Context context) {
    
    super(context);
    
            this.context = context;
    
        }
    
    public GapNavigationView(Context context, AttributeSet attrs) {
    
    super(context, attrs);
    
        }
    
    public GapNavigationView(Context context, AttributeSet attrs, int defStyleAttr) {
    
    super(context, attrs, defStyleAttr);
    
        }
    
    @RequiresApi(api = Build.VERSION_CODES.KITKAT)
    
    @SuppressLint("DrawAllocation")
    
    @Override
    
        protected void onDraw(Canvas canvas) {
    
    super.onDraw(canvas);
    
            setLayerType(View.LAYER_TYPE_SOFTWARE, null);
    
            //将中间类圆区域间距设置为总高度的 3/4
    
            int centerRadius = getHeight() *3/4;
    
            //设置阴影大小
    
            float shadowLength =5f;
    
            //创建画笔
    
            Paint paint =new Paint();
    
            //画笔抗锯齿
    
            paint.setAntiAlias(true);
    
            //创建路径
    
            Path path =new Path();
    
            //开始画View
    
    //将起点设置在阴影之下
    
            path.moveTo(0, shadowLength);
    
            //凹陷部分
    
            path.lineTo(getWidth() /2f - centerRadius, shadowLength);
    
            path.lineTo(getWidth()/2f - centerRadius/3f *2f ,shadowLength + centerRadius/4f);
    
            path.lineTo(getWidth()/2f - centerRadius/4f ,shadowLength + centerRadius *3/4f);
    
            path.lineTo(getWidth()/2f + centerRadius/4f ,shadowLength + centerRadius *3/4f);
    
            path.lineTo(getWidth()/2f + centerRadius/3f *2f ,shadowLength + centerRadius/4f);
    
            path.lineTo(getWidth()/2f + centerRadius,shadowLength);
    
            //封闭区域
    
            path.lineTo(getWidth(), shadowLength);
    
            path.lineTo(getWidth(), getHeight());
    
            path.lineTo(0, getHeight());
    
            path.lineTo(0, shadowLength);
    
            path.close();
    
            //设置挂角处的圆角角度
    
            paint.setPathEffect(new CornerPathEffect(centerRadius /4f));
    
            //画阴影
    
            paint.setStyle(Paint.Style.STROKE);
    
            paint.setColor(Color.GRAY);
    
            paint.setStrokeWidth(1);
    
            paint.setMaskFilter(new BlurMaskFilter(shadowLength -1, BlurMaskFilter.Blur.NORMAL));
    
            canvas.drawPath(path, paint);
    
            //填充背景
    
            paint.setStyle(Paint.Style.FILL);
    
            paint.setColor(Color.WHITE);
    
            paint.setStrokeWidth(1);
    
            paint.setMaskFilter(null);
    
            canvas.drawPath(path, paint);
    
        }
    
    }
    

    没有对它进行封装,代码很少,注释很多,根据注释修改需求即可!
    到这里凹陷导航栏已经完成了,除了样子不同于 BottomNavigationView ,其余与 NavigationView 是一摸一样的 。

        但是这里有个小bug,如果开启动画观察效果,你会发现当我点击导航栏底部中间时,同样是有效的,其余按钮的缩小动画会触发,因为中部本身为null,所以我们看不见。这种体验肯定是很差的,那么我们就需要屏蔽掉中间按钮的点击事件,如何屏蔽?看源码了~
    

    BottomNavigationView源码分析

    首先从我们的 BottomNavigationView 类入手

    image

    发现有一些属性 比较重要的三个 : menu menuView presenter , 看名字大概是MVP模式写的吧,不过不重要~

    分析一下这几个属性,我猜真实的点击在 menuView 里面(初始化在第三个构造函数里),那我们点进 BottomNavigationMenuView 看一下

    image

    果然发现了几个名字疑似的属性

    onClickListener
    itemPool
    buttons
    

    itemPool 只是一个存放了多个 BottomNavigationItemView 的池子,没有实际操作意义
    buttons 是 BottomNavigationItemView 的数组
    onClickListener 就是View的监听器,点击事件应该就在它里面!我们去看它在哪里被赋值,进入构造函数看看

    image

    果然在这里被赋值了,点击之后的事件在这里被消费,里面有view参数可以用来判断点击的是哪个button,那我只要能改变这个 onClickListener 再里面加上判断是否为中间按钮不就大功告成了吗?

    但是问题来了,这个属性是private!google 不希望我们修改它~于是我想到了反射,利用反射打开权限,赋给 onClickListener 自定义的值不就可以了?(前面 NavigationView 里的 menuView也是私有,也需要反射再写个BottomNavigationMenuView的衍生类),于是我真的这么做了!但是很遗憾 ,没有成功,没用的代码我就不贴了。

    放弃 menuView ,去看看 BottomNavigationView 的 menu*属性 ,它是 MenuBuilder 类 ,我们不熟这个类是做什么的,但大概猜出是个menu相关的构造类,我们找一下 menu 在哪里被赋值,发现就在构造函数里
    
    image

    这名字取得太明显了吧 CallBack 都出来了 ,里面实现了 onMenuItemSelected 和 onMenuModeChange 两个方法,选中时作了一个判空操作和一个是否是当前选项,不管是否通过判断都是有一 onNavigationItemSelected(onNavigationItemReselected)操作,

    所以都将事件传递给了以下两个监听者,返回 true 和 false 代表已处理点击和未处理点击

    image

    现在我们知道这里可以处理点击事件,那只要我们在它执行判断前再判断一次是否为中间按钮,是就直接返回true不就完成了吗?

    说干就干,同样的这里的 menu 是私有属性,我们可以使用反射将 menu 的callBack设置成我们刚才想要的,但是考虑到反射严重影响程序执行效率,我选择直接将 BottomNavigationView 源码 copy 下来修改。

    以下是主要修改部分:

    image

    其余修改部分:

    1.styleble根据IDE提示导入

    2.红线部分名字修改


    完整GapBottomNavigationView类代码(直接复制使用)

    **@SuppressLint("RestrictedApi")** 
    
    public class GapBottomNavigationViewextends FrameLayout {
    
    private static final int MENU_PRESENTER_ID =1;
    
        private final MenuBuildermenu;
    
        private final BottomNavigationMenuViewmenuView;
    
        private final BottomNavigationPresenterpresenter;
    
        private MenuInflatermenuInflater;
    
        private BottomNavigationView.OnNavigationItemSelectedListenerselectedListener;
    
        private BottomNavigationView.OnNavigationItemReselectedListenerreselectedListener;
    
        public GapBottomNavigationView(Context context) {
    
    this(context, (AttributeSet)null);
    
        }
    
    public GapBottomNavigationView(Context context, AttributeSet attrs) {
    
    this(context, attrs, attr.bottomNavigationStyle);
    
        }
    
    public GapBottomNavigationView(Context context, AttributeSet attrs, int defStyleAttr) {
    
    super(context, attrs, defStyleAttr);
    
            this.presenter =new BottomNavigationPresenter();
    
            this.menu =new BottomNavigationMenu(context);
    
            this.menuView =new BottomNavigationMenuView(context);
    
            LayoutParams params =new LayoutParams(-2, -2);
    
            params.gravity =17;
    
            this.menuView.setLayoutParams(params);
    
            this.presenter.setBottomNavigationMenuView(this.menuView);
    
            this.presenter.setId(1);
    
            this.menuView.setPresenter(this.presenter);
    
            this.menu.addMenuPresenter(this.presenter);
    
            this.presenter.initForMenu(this.getContext(), this.menu);
    
            TintTypedArray a = ThemeEnforcement.obtainTintedStyledAttributes(context, attrs, styleable.BottomNavigationView, defStyleAttr, style.Widget_Design_BottomNavigationView, new int[]{styleable.BottomNavigationView_itemTextAppearanceInactive, styleable.BottomNavigationView_itemTextAppearanceActive});
    
            if (a.hasValue(styleable.BottomNavigationView_itemIconTint)) {
    
    this.menuView.setIconTintList(a.getColorStateList(styleable.BottomNavigationView_itemIconTint));
    
            }else {
    
    this.menuView.setIconTintList(this.menuView.createDefaultColorStateList(16842808));
    
            }
    
    this.setItemIconSize(a.getDimensionPixelSize(styleable.BottomNavigationView_itemIconSize, this.getResources().getDimensionPixelSize(dimen.design_bottom_navigation_icon_size)));
    
            if (a.hasValue(styleable.BottomNavigationView_itemTextAppearanceInactive)) {
    
    this.setItemTextAppearanceInactive(a.getResourceId(styleable.BottomNavigationView_itemTextAppearanceInactive, 0));
    
            }
    
    if (a.hasValue(styleable.BottomNavigationView_itemTextAppearanceActive)) {
    
    this.setItemTextAppearanceActive(a.getResourceId(styleable.BottomNavigationView_itemTextAppearanceActive, 0));
    
            }
    
    if (a.hasValue(styleable.BottomNavigationView_itemTextColor)) {
    
    this.setItemTextColor(a.getColorStateList(styleable.BottomNavigationView_itemTextColor));
    
            }
    
    if (a.hasValue(styleable.BottomNavigationView_elevation)) {
    
    ViewCompat.setElevation(this, (float) a.getDimensionPixelSize(styleable.BottomNavigationView_elevation, 0));
    
            }
    
    this.setLabelVisibilityMode(a.getInteger(styleable.BottomNavigationView_labelVisibilityMode, -1));
    
            this.setItemHorizontalTranslationEnabled(a.getBoolean(styleable.BottomNavigationView_itemHorizontalTranslationEnabled, true));
    
            int itemBackground = a.getResourceId(styleable.BottomNavigationView_itemBackground, 0);
    
            this.menuView.setItemBackgroundRes(itemBackground);
    
            if (a.hasValue(styleable.BottomNavigationView_menu)) {
    
    this.inflateMenu(a.getResourceId(styleable.BottomNavigationView_menu, 0));
    
            }
    
    a.recycle();
    
            this.addView(this.menuView, params);
    
            if (VERSION.SDK_INT <21) {
    
    this.addCompatibilityTopDivider(context);
    
            }
    
    this.menu.setCallback(new Callback() {
    
    public boolean onMenuItemSelected(MenuBuilder menu, MenuItem item) {
    
    //menu必须为奇数个
    
                    if (menu.size() %2 !=0) {
    
    //屏蔽中间按钮的点击事件
    
                        if ( menu.getItem(menu.size()/2).equals(item)) {
    
    return true;
    
                        }
    
    }
    
    if (GapBottomNavigationView.this.reselectedListener !=null && item.getItemId() == GapBottomNavigationView.this.getSelectedItemId()) {
    
    GapBottomNavigationView.this.reselectedListener.onNavigationItemReselected(item);
    
    return true;
    
                    }else {
    
    return GapBottomNavigationView.this.selectedListener !=null && !GapBottomNavigationView.this.selectedListener.onNavigationItemSelected(item);
    
                    }
    
    }
    
    public void onMenuModeChange(MenuBuilder menu) {
    
    }
    
    });
    
        }
    
    public void setOnNavigationItemSelectedListener(@Nullable BottomNavigationView.OnNavigationItemSelectedListener listener) {
    
    this.selectedListener = listener;
    
        }
    
    public void setOnNavigationItemReselectedListener(@Nullable BottomNavigationView.OnNavigationItemReselectedListener listener) {
    
    this.reselectedListener = listener;
    
        }
    
    @NonNull
    
        public MenugetMenu() {
    
    return this.menu;
    
        }
    
    public void inflateMenu(int resId) {
    
    this.presenter.setUpdateSuspended(true);
    
            this.getMenuInflater().inflate(resId, this.menu);
    
            this.presenter.setUpdateSuspended(false);
    
            this.presenter.updateMenuView(true);
    
        }
    
    public int getMaxItemCount() {
    
    return 5;
    
        }
    
    @Nullable
    
        public ColorStateListgetItemIconTintList() {
    
    return this.menuView.getIconTintList();
    
        }
    
    public void setItemIconTintList(@Nullable ColorStateList tint) {
    
    this.menuView.setIconTintList(tint);
    
        }
    
    public void setItemIconSize(@Dimension int iconSize) {
    
    this.menuView.setItemIconSize(iconSize);
    
        }
    
    public void setItemIconSizeRes(@DimenRes int iconSizeRes) {
    
    this.setItemIconSize(this.getResources().getDimensionPixelSize(iconSizeRes));
    
        }
    
    @Dimension
    
        public int getItemIconSize() {
    
    return this.menuView.getItemIconSize();
    
        }
    
    @Nullable
    
        public ColorStateListgetItemTextColor() {
    
    return this.menuView.getItemTextColor();
    
        }
    
    public void setItemTextColor(@Nullable ColorStateList textColor) {
    
    this.menuView.setItemTextColor(textColor);
    
        }
    
    /**
    
        * @deprecated
    
        */
    
        @Deprecated
    
    @DrawableRes
    
        public int getItemBackgroundResource() {
    
    return this.menuView.getItemBackgroundRes();
    
        }
    
    public void setItemBackgroundResource(@DrawableRes int resId) {
    
    this.menuView.setItemBackgroundRes(resId);
    
        }
    
    @Nullable
    
        public DrawablegetItemBackground() {
    
    return this.menuView.getItemBackground();
    
        }
    
    public void setItemBackground(@Nullable Drawable background) {
    
    this.menuView.setItemBackground(background);
    
        }
    
    @IdRes
    
        public int getSelectedItemId() {
    
    return this.menuView.getSelectedItemId();
    
        }
    
    public void setSelectedItemId(@IdRes int itemId) {
    
    MenuItem item =this.menu.findItem(itemId);
    
            if (item !=null && !this.menu.performItemAction(item, this.presenter, 0)) {
    
    item.setChecked(true);
    
            }
    
    }
    
    public void setLabelVisibilityMode(int labelVisibilityMode) {
    
    if (this.menuView.getLabelVisibilityMode() != labelVisibilityMode) {
    
    this.menuView.setLabelVisibilityMode(labelVisibilityMode);
    
                this.presenter.updateMenuView(false);
    
            }
    
    }
    
    public int getLabelVisibilityMode() {
    
    return this.menuView.getLabelVisibilityMode();
    
        }
    
    public void setItemTextAppearanceInactive(@StyleRes int textAppearanceRes) {
    
    this.menuView.setItemTextAppearanceInactive(textAppearanceRes);
    
        }
    
    @StyleRes
    
        public int getItemTextAppearanceInactive() {
    
    return this.menuView.getItemTextAppearanceInactive();
    
        }
    
    public void setItemTextAppearanceActive(@StyleRes int textAppearanceRes) {
    
    this.menuView.setItemTextAppearanceActive(textAppearanceRes);
    
        }
    
    @StyleRes
    
        public int getItemTextAppearanceActive() {
    
    return this.menuView.getItemTextAppearanceActive();
    
        }
    
    public void setItemHorizontalTranslationEnabled(boolean itemHorizontalTranslationEnabled) {
    
    if (this.menuView.isItemHorizontalTranslationEnabled() != itemHorizontalTranslationEnabled) {
    
    this.menuView.setItemHorizontalTranslationEnabled(itemHorizontalTranslationEnabled);
    
                this.presenter.updateMenuView(false);
    
            }
    
    }
    
    public boolean isItemHorizontalTranslationEnabled() {
    
    return this.menuView.isItemHorizontalTranslationEnabled();
    
        }
    
    private void addCompatibilityTopDivider(Context context) {
    
    View divider =new View(context);
    
            divider.setBackgroundColor(ContextCompat.getColor(context, color.design_bottom_navigation_shadow_color));
    
            LayoutParams dividerParams =new LayoutParams(-1, this.getResources().getDimensionPixelSize(dimen.design_bottom_navigation_shadow_height));
    
            divider.setLayoutParams(dividerParams);
    
            this.addView(divider);
    
        }
    
    private MenuInflatergetMenuInflater() {
    
    if (this.menuInflater ==null) {
    
    this.menuInflater =new SupportMenuInflater(this.getContext());
    
            }
    
    return this.menuInflater;
    
        }
    
    protected ParcelableonSaveInstanceState() {
    
    Parcelable superState =super.onSaveInstanceState();
    
            GapBottomNavigationView.SavedState savedState =new GapBottomNavigationView.SavedState(superState);
    
            savedState.menuPresenterState =new Bundle();
    
            this.menu.savePresenterStates(savedState.menuPresenterState);
    
            return savedState;
    
        }
    
    protected void onRestoreInstanceState(Parcelable state) {
    
    if (!(stateinstanceof GapBottomNavigationView.SavedState)) {
    
    super.onRestoreInstanceState(state);
    
            }else {
    
    GapBottomNavigationView.SavedState savedState = (GapBottomNavigationView.SavedState) state;
    
                super.onRestoreInstanceState(savedState.getSuperState());
    
                this.menu.restorePresenterStates(savedState.menuPresenterState);
    
            }
    
    }
    
    static class SavedStateextends AbsSavedState {
    
    BundlemenuPresenterState;
    
            public static final CreatorCREATOR =new ClassLoaderCreator() {
    
    public GapBottomNavigationView.SavedStatecreateFromParcel(Parcel in, ClassLoader loader) {
    
    return new GapBottomNavigationView.SavedState(in, loader);
    
                }
    
    public GapBottomNavigationView.SavedStatecreateFromParcel(Parcel in) {
    
    return new GapBottomNavigationView.SavedState(in, (ClassLoader)null);
    
                }
    
    public GapBottomNavigationView.SavedState[]newArray(int size) {
    
    return new GapBottomNavigationView.SavedState[size];
    
                }
    
    };
    
            public SavedState(Parcelable superState) {
    
    super(superState);
    
            }
    
    public SavedState(Parcel source, ClassLoader loader) {
    
    super(source, loader);
    
                this.readFromParcel(source, loader);
    
            }
    
    public void writeToParcel(@NonNull Parcel out, int flags) {
    
    super.writeToParcel(out, flags);
    
                out.writeBundle(this.menuPresenterState);
    
            }
    
    private void readFromParcel(Parcel in, ClassLoader loader) {
    
    this.menuPresenterState = in.readBundle(loader);
    
            }
    
    }
    
    public interface OnNavigationItemReselectedListener {
    
    void onNavigationItemReselected(@NonNull MenuItem var1);
    
        }
    
    public interface OnNavigationItemSelectedListener {
    
    boolean onNavigationItemSelected(@NonNull MenuItem var1);
    
        }
    
    @RequiresApi(api = Build.VERSION_CODES.KITKAT)
    
    @SuppressLint("DrawAllocation")
    
    @Override
    
        protected void onDraw(Canvas canvas) {
    
    super.onDraw(canvas);
    
            //setLayerType(View.LAYER_TYPE_SOFTWARE, null);
    
            int centerRadius = getHeight() *3 /4;
    
            float shadowLength =5f;
    
            Paint paint =new Paint();
    
            paint.setAntiAlias(true);
    
            Path path =new Path();
    
            path.moveTo(0, shadowLength);
    
            path.lineTo(getWidth() /2f - centerRadius, shadowLength);
    
            path.lineTo(getWidth() /2f - centerRadius /3f *2f, shadowLength + centerRadius /4f);
    
            path.lineTo(getWidth() /2f - centerRadius /4f, shadowLength + centerRadius *3 /4f);
    
            path.lineTo(getWidth() /2f + centerRadius /4f, shadowLength + centerRadius *3 /4f);
    
            path.lineTo(getWidth() /2f + centerRadius /3f *2f, shadowLength + centerRadius /4f);
    
            path.lineTo(getWidth() /2f + centerRadius, shadowLength);
    
            path.lineTo(getWidth(), shadowLength);
    
            path.lineTo(getWidth(), getHeight());
    
            path.lineTo(0, getHeight());
    
            path.lineTo(0, shadowLength);
    
            path.close();
    
            paint.setPathEffect(new CornerPathEffect(centerRadius /4f));
    
            //画阴影
    
            paint.setStyle(Paint.Style.STROKE);
    
            paint.setColor(Color.GRAY);
    
            paint.setStrokeWidth(1);
    
            //paint.setMaskFilter(new BlurMaskFilter(shadowLength - 1, BlurMaskFilter.Blur.NORMAL));
    
            canvas.drawPath(path, paint);
    
            //填充白色
    
            paint.setStyle(Paint.Style.FILL);
    
            paint.setColor(Color.WHITE);
    
            paint.setStrokeWidth(1);
    
            paint.setMaskFilter(null);
    
            canvas.drawPath(path, paint);
    
        }
    
    }
    
    

    完美收工~喜欢记得点赞哦~

    相关文章

      网友评论

        本文标题:中间凹陷的底部导航栏(NavigationView)+ 源码分析

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