本文共 10098 字,大约阅读时间需要 33 分钟。
这是【从零撸美团】系列文章第三篇
【从零撸美团】是一个高仿美团的开源项目,旨在巩固 Android 相关知识的同时,帮助到有需要的小伙伴。 GitHub 源码地址:每个项目基本都会有多个 Tab ,以期在有限的屏幕空间展现更多的功能。
有需求就会有市场,如今也出现了很多优秀的 tab 切换框架,使用者众多。但是深入思考之后还是决定自己造轮子~
因为框架虽好,可不要贪杯哦~
使用第三方框架最大的问题在于并不能完全满足实际需求,有的是 icon 图片 跟文字间距无法调整,有的后期会出现各种各样问题,不利于维护。 最重要的是自己写一个也不是很复杂,有研究框架填坑的时间也就写出来了。
先看怎么用:一句代码搞定
tabWidget.init(getSupportFragmentManager(), fragmentList);
再上效果图:
你没看错,长得跟美团一模一样,毕竟这个项目就叫 ㄟ( ▔, ▔ )ㄏ
底部 tab 布局有很多实现方式,比如 RadioButton、FragmentTabHost、自定义组合View等。这里采用的是自定义组合View方式,因为可定制度更高。
滑动切换基本都是采用 ViewPager + Fragment ,集成简单,方案较成熟。这里同样采用这种方式。开始之前需要准备两样东西:
- 五个 tab 的选中和未选中状态的 icon 图片共计10张
- 五个 Fragment
这是最基本的素材,有了素材之后就开始干活吧~
由于要实现点击选中图片和文字都变色成选中状态,没有选中就变成灰色,所以要对每组 icon 建立一个selector
xml文件实现状态切换。 这里用了 android:state_activated
作为状态标记,因为最常用的 pressed
和 focused
都达不到长久保持状态的要求,都是松开手指之后就恢复了。在代码中手动设置 activated
值就好。
注意:
此处设置的是 icon 图片,所以用 android:drawable
,与下面文字使用的 android:color
有区别。 设置完图片资源后,该设置文字颜色的 selector
了,因为文字的颜色也要跟着变。
注意图片用 android:drawable
,文字用 android:color
。
准备工作做完之后,就开始正式的自定义View啦。
首先是布局文件:
widget_custom_bottom_tab.xml
最外层用竖向排列的 LinearLayout
包裹,它有两个子节点,上面是用于滑动和装载 Fragment
的 ViewPager
,下面是五个 Tab
的布局。
ImageView
和 TextView
的共有属性抽取到 styles.xml
里了: 有了布局文件之后,就开始真正的自定义 View
吧。
新建 java 文件 CustomBottomTabWidget
继承自 LinearLayout
。为什么继承 LinearLayout
呢?因为我们的布局文件根节点就是 LinearLayout
呀,根节点是什么就继承什么。
先上代码吧:
package com.cachecats.meituan.widget.bottomtab;import android.content.Context;import android.support.annotation.Nullable;import android.support.v4.app.FragmentManager;import android.support.v4.view.ViewPager;import android.util.AttributeSet;import android.view.View;import android.widget.LinearLayout;import com.cachecats.meituan.R;import com.cachecats.meituan.base.BaseFragment;import java.util.List;import butterknife.BindView;import butterknife.ButterKnife;import butterknife.OnClick;public class CustomBottomTabWidget extends LinearLayout { @BindView(R.id.ll_menu_home_page) LinearLayout llMenuHome; @BindView(R.id.ll_menu_nearby) LinearLayout llMenuNearby; @BindView(R.id.ll_menu_discover) LinearLayout llMenuDiscover; @BindView(R.id.ll_menu_order) LinearLayout llMenuOrder; @BindView(R.id.ll_menu_mine) LinearLayout llMenuMine; @BindView(R.id.vp_tab_widget) ViewPager viewPager; private FragmentManager mFragmentManager; private ListmFragmentList; private TabPagerAdapter mAdapter; public CustomBottomTabWidget(Context context) { this(context, null, 0); } public CustomBottomTabWidget(Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } public CustomBottomTabWidget(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); View view = View.inflate(context, R.layout.widget_custom_bottom_tab, this); ButterKnife.bind(view); //设置默认的选中项 selectTab(MenuTab.HOME); } /** * 外部调用初始化,传入必要的参数 * * @param fm */ public void init(FragmentManager fm, List fragmentList) { mFragmentManager = fm; mFragmentList = fragmentList; initViewPager(); } /** * 初始化 ViewPager */ private void initViewPager() { mAdapter = new TabPagerAdapter(mFragmentManager, mFragmentList); viewPager.setAdapter(mAdapter); viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() { @Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { } @Override public void onPageSelected(int position) { //将ViewPager与下面的tab关联起来 switch (position) { case 0: selectTab(MenuTab.HOME); break; case 1: selectTab(MenuTab.NEARBY); break; case 2: selectTab(MenuTab.DISCOVER); break; case 3: selectTab(MenuTab.ORDER); break; case 4: selectTab(MenuTab.MINE); break; default: selectTab(MenuTab.HOME); break; } } @Override public void onPageScrollStateChanged(int state) { } }); } /** * 点击事件集合 */ @OnClick({R.id.ll_menu_home_page, R.id.ll_menu_nearby, R.id.ll_menu_discover, R.id.ll_menu_order, R.id.ll_menu_mine}) public void onViewClicked(View view) { switch (view.getId()) { case R.id.ll_menu_home_page: selectTab(MenuTab.HOME); //使ViewPager跟随tab点击事件滑动 viewPager.setCurrentItem(0); break; case R.id.ll_menu_nearby: selectTab(MenuTab.NEARBY); viewPager.setCurrentItem(1); break; case R.id.ll_menu_discover: selectTab(MenuTab.DISCOVER); viewPager.setCurrentItem(2); break; case R.id.ll_menu_order: selectTab(MenuTab.ORDER); viewPager.setCurrentItem(3); break; case R.id.ll_menu_mine: selectTab(MenuTab.MINE); viewPager.setCurrentItem(4); break; } } /** * 设置 Tab 的选中状态 * * @param tab 要选中的标签 */ public void selectTab(MenuTab tab) { //先将所有tab取消选中,再单独设置要选中的tab unCheckedAll(); switch (tab) { case HOME: llMenuHome.setActivated(true); break; case NEARBY: llMenuNearby.setActivated(true); break; case DISCOVER: llMenuDiscover.setActivated(true); break; case ORDER: llMenuOrder.setActivated(true); break; case MINE: llMenuMine.setActivated(true); } } //让所有tab都取消选中 private void unCheckedAll() { llMenuHome.setActivated(false); llMenuNearby.setActivated(false); llMenuDiscover.setActivated(false); llMenuOrder.setActivated(false); llMenuMine.setActivated(false); } /** * tab的枚举类型 */ public enum MenuTab { HOME, NEARBY, DISCOVER, ORDER, MINE }}
注释应该写的很清楚了,这里再强调几个点:
super(context)
,和两个参数的 super(context, attrs)
分别改成:this(context, null, 0)
和 this(context, attrs, 0)
。这样无论走的哪个构造函数,最终都会走到三个参数的构造函数里,我们只要把初始化操作放在这个函数里就行了。View view = View.inflate(context, R.layout.widget_custom_bottom_tab, this);将
widget_custom_bottom_tab.xml
文件与 java 代码绑定了起来,注意最后 一个参数是 this
而不是 null
。ButterKnife
从 findViewById()
解脱出来。unCheckedAll ()
将所有 tab 都置为未选中状态,再单独设置要选中的 tab 为选中状态 llMenuHome.setActivated(true);
ViewPager
的滑动绑定需要在两个地方写逻辑: 1)tab 的点击回调里执行下面两行代码,分别使 tab 变为选中状态和让 ViewPager
滑动到相应位置。selectTab(MenuTab.HOME);//使ViewPager跟随tab点击事件滑动viewPager.setCurrentItem(0);2)在
ViewPager
的监听方法 onPageSelected()
中,每滑动到一个页面,就调用 selectTab(MenuTab.HOME)
方法将对应的 tab 设置为选中状态。//设置默认的选中项 selectTab(MenuTab.HOME);
好啦,到这自定义 View 已经完成了。下面看看怎么使用。
在主页的布局文件里直接引用:
然后在 Activity 里一句话调用:
tabWidget.init(getSupportFragmentManager(), fragmentList);
就是这么简单!
是不是很爽很清新?贴出 MainActivity
完整代码:
package com.cachecats.meituan.app;import android.os.Bundle;import com.cachecats.meituan.MyApplication;import com.cachecats.meituan.R;import com.cachecats.meituan.app.discover.DiscoverFragment;import com.cachecats.meituan.app.home.HomeFragment;import com.cachecats.meituan.app.mine.MineFragment;import com.cachecats.meituan.app.nearby.NearbyFragment;import com.cachecats.meituan.app.order.OrderFragment;import com.cachecats.meituan.base.BaseActivity;import com.cachecats.meituan.base.BaseFragment;import com.cachecats.meituan.di.DIHelper;import com.cachecats.meituan.di.components.DaggerActivityComponent;import com.cachecats.meituan.di.modules.ActivityModule;import com.cachecats.meituan.widget.bottomtab.CustomBottomTabWidget;import java.util.ArrayList;import java.util.List;import butterknife.BindView;import butterknife.ButterKnife;public class MainActivity extends BaseActivity { @BindView(R.id.tabWidget) CustomBottomTabWidget tabWidget; private ListfragmentList; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ButterKnife.bind(this); DaggerActivityComponent.builder() .applicationComponent(MyApplication.getApplicationComponent()) .activityModule(new ActivityModule(this)) .build().inject(this); //初始化 init(); } private void init() { //构造Fragment的集合 fragmentList = new ArrayList<>(); fragmentList.add(new HomeFragment()); fragmentList.add(new NearbyFragment()); fragmentList.add(new DiscoverFragment()); fragmentList.add(new OrderFragment()); fragmentList.add(new MineFragment()); //初始化CustomBottomTabWidget tabWidget.init(getSupportFragmentManager(), fragmentList); }}
整个代码很简单,只需要构造出 Fragment
的列表传给 CustomBottomTabWidget
就好啦。
总结:自己造轮子可能前期封装花些时间,但自己写的代码自己最清楚,几个月后再改需求改代码能快速的定位到要改的地方,便于维护。
并且最后封装完用起来也很简单啊,不用在 Activity 里写那么多配置代码,整体逻辑更清晰,耦合度更低。
以上就是用自定义 View 的方式实现高度定制化的多 tab 标签滑动切换实例。
源码地址: 欢迎下载,欢迎star
,欢迎点赞~ 转载地址:http://mvvpi.baihongyu.com/