Menu getMenu():
获取设置的 menu。
int getMaxItemCount():
返回 BottomNavgation 能显示的最大 Item 个数,源码里固定为5。
setOnNavigationItemSelectedListener
(
BottomNavigationView.OnNavigationItemSelectedListener listener
):给 BottomNavigationView 设置监听器,监听 Item 的选中情况。
MenuItem点击动效解析
在 Google 推出官方的 Bottom Navigation 组件以前,我使用过
Ashok-Varma/BottomNavigation
,我知道的类似库还有
tyzlmjj/PagerBottomTabStrip
和
sephiroth74/Material-BottomNavigation
。不得不说,相比于这些第三方开源库,BottomNavigationView 还是功能比较简单的。
BottomNavigationView 本身是个继承自 FrameLayout 的 View,我觉得比较值得看看的,就是它的 Item 点击的动画效果。
首先要知道的是,BottomNavigationView 的 Item 有两种形式:
Fixed
和
Shifted
,如图所示:
查看源码会发现,设置动画效果的核心代码都在
BottomNavigationItemView
的
setChecked(boolean checked)
方法里,我将分析写成注释写进代码里,这样看起来应该容易理解一些:
@Override public void setChecked(boolean checked) { /** * 首先,我们要分析 Fixed 和 Shifted 模式的动画效果: * * Fixed 模式里 Item 的selected状态和非selected状态,其实仅仅是 Item 的文字大小变了,图标并没有变 * Item 的 title 就是这里的 mLargeLabel 和 mSmallLabel,他们是两个TextView。 * * Shifted 模式里 Item 的selected状态为图标和文字均可见,非selected状态仅为图标可见 * 同时,可见的 Item 占有一块较大的区域,且图标和文字居中显示。 */ /** * 首先设置大文字和小文字的坐标 */ ViewCompat.setPivotX(mLargeLabel, mLargeLabel.getWidth() / 2); ViewCompat.setPivotY(mLargeLabel, mLargeLabel.getBaseline()); ViewCompat.setPivotX(mSmallLabel, mSmallLabel.getWidth() / 2); ViewCompat.setPivotY(mSmallLabel, mSmallLabel.getBaseline()); /** * 这里判断是否为 Shifted 模式,进行不同的设置 * * 再根据item是否 checked,进行不同的设置 * */ if (mShiftingMode) { if (checked) { FrameLayout.LayoutParams iconParams = (FrameLayout.LayoutParams) mIcon.getLayoutParams(); iconParams.gravity = Gravity.CENTER_HORIZONTAL | Gravity.TOP; iconParams.topMargin = mDefaultMargin; mIcon.setLayoutParams(iconParams); mLargeLabel.setVisibility(VISIBLE); ViewCompat.setScaleX(mLargeLabel, 1f); ViewCompat.setScaleY(mLargeLabel, 1f); } else { FrameLayout.LayoutParams iconParams = (FrameLayout.LayoutParams) mIcon.getLayoutParams(); iconParams.gravity = Gravity.CENTER; iconParams.topMargin = mDefaultMargin; mIcon.setLayoutParams(iconParams); mLargeLabel.setVisibility(INVISIBLE); ViewCompat.setScaleX(mLargeLabel, 0.5f); ViewCompat.setScaleY(mLargeLabel, 0.5f); } mSmallLabel.setVisibility(INVISIBLE); } else { if (checked) { FrameLayout.LayoutParams iconParams = (FrameLayout.LayoutParams) mIcon.getLayoutParams(); iconParams.gravity = Gravity.CENTER_HORIZONTAL | Gravity.TOP; iconParams.topMargin = mDefaultMargin + mShiftAmount; mIcon.setLayoutParams(iconParams); mLargeLabel.setVisibility(VISIBLE); mSmallLabel.setVisibility(INVISIBLE); ViewCompat.setScaleX(mLargeLabel, 1f); ViewCompat.setScaleY(mLargeLabel, 1f); ViewCompat.setScaleX(mSmallLabel, mScaleUpFactor); ViewCompat.setScaleY(mSmallLabel, mScaleUpFactor); } else { FrameLayout.LayoutParams iconParams = (FrameLayout.LayoutParams) mIcon.getLayoutParams(); iconParams.gravity = Gravity.CENTER_HORIZONTAL | Gravity.TOP; iconParams.topMargin = mDefaultMargin; mIcon.setLayoutParams(iconParams); mLargeLabel.setVisibility(INVISIBLE); mSmallLabel.setVisibility(VISIBLE); ViewCompat.setScaleX(mLargeLabel, mScaleDownFactor); ViewCompat.setScaleY(mLargeLabel, mScaleDownFactor); ViewCompat.setScaleX(mSmallLabel, 1f); ViewCompat.setScaleY(mSmallLabel, 1f); } } /** * 设置完后,调用 refreshDrawableState() 方法强制 View 刷新 Drawable 状态 */ refreshDrawableState(); }
BottomNavigationView + ViewPager + Fragment 懒加载
首先要说的是,这个示例其实是不大符合 Material Design 规范的,
在 Bottom Navigation 的规范里,明确指出在应当避免在内容区域使用滑动手势来切换视图,两个视图之间的切换应通过点击 BottomNavigationView 的 Item 来实现,在切换时,视图之间的过渡应有淡入淡出效果。
为什么我还要写这个代码呢?....先看吧,最后我会说明原因。
首先是
效果视频
(知乎专栏不支持 gif 动图,只有给大家看视频了)
BottomNavigationView_效果视频2—在线播放—优酷网,视频高清在线观看
http://v.youku.com/v_show/id_XMTg3NDYwNjEwOA==.html
接下来是代码部分,这个 Demo 主要有两部分,一个是 Fragment 的懒加载,还有个就是将 BottomNavigationView 与 ViewPager 联系起来。
首先,Fragment 懒加载部分,先定义一个 BaseFragment:
/** * @author bugdev * @email [email protected] */ public abstract class BaseFragment extends Fragment { protected Activity mActivity; protected View mRootView; private Unbinder unbinder; /** * 说明:在此处保存全局的Context * * @param context 上下文 */ @Override public void onAttach(Context context) { super.onAttach(context); mActivity = (Activity) context; } @Nullable @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { mRootView = inflater.inflate(getLayoutId(), container, false); unbinder = ButterKnife.bind(this, mRootView); init(); return mRootView; } /** * @return 返回该Fragment的layout id */ protected abstract int getLayoutId(); /** * 说明:创建视图时的初始化操作均写在该方法 */ protected abstract void init(); /** * 获取控件对象 * * @param id 控件id * @return 控件对象 */ public View findViewById(int id) { if (getContentView() != null) { return getContentView().findViewById(id); } else { return null; } } /** * 说明:返回当前View * * @return view */ protected View getContentView() { return mRootView; } @Override public void onDestroyView() { super.onDestroyView(); unbinder.unbind(); } }
接下来将LazyLoadFragment继承自Fragment:
/** * 懒加载Fragment * * 可以加载数据的条件: * 1.视图已经初始化 * 2.视图对用户可见 * * @author bugdev * @email [email protected] */ public abstract class LazyLoadFragment extends BaseFragment { public boolean isInit = false;//视图是否已经初始化 public boolean isLoad = false;//视图是否已经加载过 /** * 初始化 */ @Override protected void init() { isInit = true; isCanLoadData(); } /** * 判断是否可以加载数据,如果可以便进行数据的加载 */ private void isCanLoadData() { if (!isInit) { return; } if (getUserVisibleHint()) { lazyLoad(); isLoad = true; } else { if (isLoad) { stopLoad(); } } } /** * 当视图初始化并且对可见时加载数据 */ public abstract void lazyLoad(); /** * 当该视图对用户不可见并且已经加载过数据的时候,如果需要在切换到其他页面时停止加载数据,通过覆写此方法实现 */ public void stopLoad() { } /** * 说明:当前视图可见性发生变化时调用该方法 * * @param isVisibleToUser 当前视图是否可见 */ @Override public void setUserVisibleHint(boolean isVisibleToUser) { super.setUserVisibleHint(isVisibleToUser); isCanLoadData(); } /** * 视图销毁时将Fragment是否初始化的状态变为false */ @Override public void onDestroyView() { super.onDestroyView(); isInit = false; isLoad = false; } }
以上就完成了一个懒加载的Fragment,下面编写Activity部分,首先是布局文件:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout 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:id="@+id/activity_bottom_navigation_t" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context="bugdev.blogsource.activity.BottomNavigationTActivity"> <android.support.design.widget.AppBarLayout android:id="@+id/appbar_container" android:layout_width="match_parent" android:layout_height="wrap_content" android:theme="@style/AppTheme.AppBarOverlay"> <android.support.v7.widget.Toolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:background="?attr/colorPrimary" app:popupTheme="@style/AppTheme.PopupOverlay"> <TextView android:id="@+id/toolbar_title_tv" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:fontFamily="cursive" android:text="@string/app_name" android:textColor="@color/colorPrimaryLight" android:textSize="28sp" android:textStyle="bold" /> </android.support.v7.widget.Toolbar> </android.support.design.widget.AppBarLayout> <android.support.v4.view.ViewPager android:id="@+id/home_view_pager" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" /> <android.support.design.widget.BottomNavigationView android:id="@+id/bottom_nav_view" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@color/colorPrimary" android:elevation="8dp" app:itemIconTint="@color/item_bottom_nav_selector" app:itemTextColor="@color/item_bottom_nav_selector" app:menu="@menu/menu_bottom_nav" /> </LinearLayout>
接下来就是Activity部分:
public class BottomNavigationTActivity extends AppCompatActivity { @BindView(R.id.toolbar_title_tv) TextView toolbarTitleTv; @BindView(R.id.toolbar) Toolbar toolbar; @BindView(R.id.home_view_pager) ViewPager homeViewPager; @BindView(R.id.bottom_nav_view) BottomNavigationView bottomNavView; @BindView(R.id.activity_bottom_navigation_t) LinearLayout activityBottomNavigationT; MenuItem prevMenuItem; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_bottom_navigation_t); ButterKnife.bind(this); //初始化Toolbar initToolbar(); //初始化Viewpager initViewPager(); //初始化Bottom Navigation initBottomNav(); } private void initToolbar() { toolbar.setTitle(""); setSupportActionBar(toolbar); if (getSupportActionBar() != null) getSupportActionBar().setDisplayHomeAsUpEnabled(false); } private void initViewPager() { homeViewPager.setAdapter(new FragmentStatePagerAdapter(getSupportFragmentManager()) { @Override public Fragment getItem(int position) { Fragment fragment = null; switch (position) { case 0: fragment = new HomeFragment(); break; case 1: fragment = new ExploreFragment(); break; case 2: fragment = new MineFragment(); break; } return fragment; } @Override public int getCount() { return 3; } }); homeViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() { @Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { } @Override public void onPageSelected(int position) { invalidateOptionsMenu(); /** * 该方法只有在有新的页面被选中时才会回调 * * 如果 preMenuItem 为 null,说明该方法还没有被回调过 * 则ViewPager从创建到现在都处于 position 为 0 的页面 * 所以当该方法第一次被回调的时候,直接将 position 为 0 的页面的 selected 状态设为 false 即可 * * 如果 preMenuItem 不为 null,说明该方法内的 * "prevMenuItem = bottomNavView.getMenu().getItem(position);" * 之前至少被调用过一次 * 所以当该方法再次被回调的时候,直接将上一个 prevMenuItem 的 selected 状态设为 false 即可 * 在做完上一句的事情后,将当前页面设为 prevMenuItem,以备下次调用 * * 我注释写这么详细,是不是要给我搭个赏~ (ಥ_ಥ) */ if (prevMenuItem == null) { bottomNavView.getMenu().getItem(0).setChecked(false); } else { prevMenuItem.setChecked(false); } bottomNavView.getMenu().getItem(position).setChecked(true); prevMenuItem = bottomNavView.getMenu().getItem(position); } @Override public void onPageScrollStateChanged(int state) { } }); } private void initBottomNav() { bottomNavView.setOnNavigationItemSelectedListener( new BottomNavigationView.OnNavigationItemSelectedListener() { @Override public boolean onNavigationItemSelected(@NonNull MenuItem item) { homeViewPager.setCurrentItem(item.getOrder()); return true; } }); } }
Fragment部分很简单,我就不放出来了,以上就是一个 BottomNavigationView + ViewPager + Fragment 懒加载的例子。
我在代码里将注释都写的很详细,所以也不用再过多的讲解。
讲道理注释这么良心,应该打个赏。
讲道理注释这么良心,应该打个赏。
讲道理注释这么良心,应该打个赏。
(ಥ_ಥ)
最后总结一下思路
懒加载 Fragment 的思路:
通过两个布尔值变量来分别控制 Fragment 是否初始化和是否已经加载过。
ViewPager + BottomNavigationView 的思路:
通过监听 ViewPager 的滑动来将 BottomNavigationView 相应位置上的 Item 设为 selected 状态,同时监听 BottomNavigationView ,在 Item 被点击的时候也将 ViewPager 调至相应的页面。
需要注意的是:
由于 BottomNavigationView 没有提供公开的API,可以对 Item 的布局模式进行设置,所以如果你的视图超过了 3 个,就只有改写或弃用这种滑动切换视图的方式了。
Navigation Drawer 与 Bottom Navigation的比较
首先说些大方向上的东西
第一,设计规范是不断演化更新的;
第二,规范是由一部分比较擅长这方面的人写出来的,但他们同时也受到其他方面的影响,所以他们正确的概率比较大,但不是100%;
第三,规范存在的意义是为了帮助开发者创造出更好的应用,用专业的理解来帮助其他人,使得整个应用环境更好,让用户体验更棒;
第四,在产品设计上,我们做出的所有决定都应该基于“为用户带来更好的使用体验”,在这之上才是商业或其他方面的东西。
所以当我们面临类似 Navigation Drawer 与 Bottom Navigation 的取舍,又或是否可以允许通过滑动配合 Bottom Navigation 来切换页面的选择的时候,我们首先想到的应该是,这样做了好不好?这样是否真的给 App 带来了体验的提升?
就我来说,我很喜欢微信 Android 端可以滑动切换页面的方式,我并不 care 它是否符合规范,它只是让我的使用更加舒服了。
所以,我希望看到这段话的人,在接下来做自己产品的时候,除了参考规范,更要有自己的思考。说到底,App 是你的作品。
再说下 Bottom Navigation 与 Navigation Drawer 的比较
前段时间我去了北京的 2016 Google 开发者大会,听了其中一场有关 Material Design 的演讲,讲者正好就提到了这二者的比较,深得我心。我加入自己的观点总结一下就是:
Drawer 的好处在于,在手机端上,它是可以隐藏的,不会始终占用屏幕。但它的坏处也是这个,它会隐藏。所以如果你希望自己的 App 多提高一些内容模块的展示率,你可以考虑使用 Bottom Navigation,用户可以方便的点击查看更多的内容。那么如果你做了个类似 Gmail 的邮件应用,Drawer 或许是个更好的导航选择。这些都不需要非常的死板,完全取决于你希望自己 App 的导航风格是怎样的,取决于你认为自己的作品怎样做才能给用户最好的使用体验。
最后,希望大家都可以做出优秀的作品。
以上:-)
Bottom Navigation | Material Design
BottomNavigationView | Android Developers
Easy Way To Connect BottomNavigationView & ViewPager in Android
Android中ViewPager+Fragment取消(禁止)预加载延迟加载(懒加载)问题解决方案
Tab Bars - iOS Human Interface Guidelines
http://
weixin.qq.com/r/yTgrM-D
ESK03rbRQ923b
(二维码自动识别)
微信公众号: BugDev「bugdev」
欢迎关注啦~