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

释放双眼,带上耳机,听听看~!

中间凹陷的 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);
}
}

 

人已赞赏
Android文章

Android开发之消息循环与Looper(深入了解)

2020-2-1 6:47:23

Android文章

Android开发多线程的实现方法

2020-2-1 7:44:54

个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
有新消息 消息中心
搜索