这是【从零撸美团】系列文章第三篇
【从零撸美团】是一个高仿美团的开源项目,旨在巩固 Android 相关知识的同时,帮助到有需要的小伙伴。
GitHub 源码地址:github.com/cachecats/L…
Android从零撸美团(一) - 统一管理 Gradle 依赖 提取到单独文件中
Android从零撸美团(二) - 仿美团下拉刷新自定义动画
Android从零撸美团(四) - 美团首页布局解析及实现 - Banner+自定义View+SmartRefreshLayout下拉刷新上拉加载更多
每个项目基本都会有多个 Tab ,以期在有限的屏幕空间展现更多的功能。 有需求就会有市场,如今也出现了很多优秀的 tab 切换框架,使用者众多。
但是深入思考之后还是决定自己造轮子~
因为框架虽好,可不要贪杯哦~ 使用第三方框架最大的问题在于并不能完全满足实际需求,有的是 icon 图片 跟文字间距无法调整,有的后期会出现各种各样问题,不利于维护。 最重要的是自己写一个也不是很复杂,有研究框架填坑的时间也就写出来了。
先看怎么用:一句代码搞定
tabWidget.init(getSupportFragmentManager(), fragmentList);
再上效果图:

你没看错,长得跟美团一模一样,毕竟这个项目就叫【从零撸美团】 ㄟ( ▔, ▔ )ㄏ
一、思路
底部 tab 布局有很多实现方式,比如 RadioButton、FragmentTabHost、自定义组合View等。这里采用的是自定义组合View方式,因为可定制度更高。 滑动切换基本都是采用 ViewPager + Fragment ,集成简单,方案较成熟。这里同样采用这种方式。
二、准备
开始之前需要准备两样东西:
- 五个 tab 的选中和未选中状态的 icon 图片共计10张
- 五个 Fragment
这是最基本的素材,有了素材之后就开始干活吧~
由于要实现点击选中图片和文字都变色成选中状态,没有选中就变成灰色,所以要对每组 icon 建立一个 selector
xml文件实现状态切换。
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/ic_vector_home_pressed" android:state_activated="true" />
<item android:drawable="@drawable/ic_vector_home_normal" android:state_activated="false" />
</selector>
这里用了 android:state_activated
作为状态标记,因为最常用的 pressed
和 focused
都达不到长久保持状态的要求,都是松开手指之后就恢复了。在代码中手动设置 activated
值就好。
注意:
此处设置的是 icon 图片,所以用 android:drawable
,与下面文字使用的 android:color
有区别。
设置完图片资源后,该设置文字颜色的 selector
了,因为文字的颜色也要跟着变。
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@color/meituanGreen" android:state_activated="true" />
<item android:color="@color/gray666" android:state_activated="false" />
</selector>
注意图片用 android:drawable
,文字用 android:color
。
三、实现
准备工作做完之后,就开始正式的自定义View啦。
1. 写布局
首先是布局文件:
widget_custom_bottom_tab.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
>
<android.support.v4.view.ViewPager
android:id="@+id/vp_tab_widget"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<!--下面的tab标签布局-->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="3dp"
android:paddingTop="3dp"
>
<LinearLayout
android:id="@+id/ll_menu_home_page"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:orientation="vertical">
<ImageView
android:id="@+id/iv_menu_home"
style="@style/menuIconStyle"
android:src="@drawable/selector_icon_menu_home" />
<TextView
android:id="@+id/tv_menu_home"
style="@style/menuTextStyle"
android:text="首页" />
</LinearLayout>
<LinearLayout
android:id="@+id/ll_menu_nearby"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:orientation="vertical">
<ImageView
android:id="@+id/iv_menu_nearby"
style="@style/menuIconStyle"
android:src="@drawable/selector_icon_menu_nearby" />
<TextView
android:id="@+id/tv_menu_nearby"
style="@style/menuTextStyle"
android:text="附近" />
</LinearLayout>
<LinearLayout
android:id="@+id/ll_menu_discover"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:orientation="vertical">
<ImageView
android:id="@+id/iv_menu_discover"
style="@style/menuIconStyle"
android:src="@drawable/selector_icon_menu_discover" />
<TextView
android:id="@+id/tv_menu_discover"
style="@style/menuTextStyle"
android:text="发现" />
</LinearLayout>
<LinearLayout
android:id="@+id/ll_menu_order"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:orientation="vertical">
<ImageView
android:id="@+id/iv_menu_order"
style="@style/menuIconStyle"
android:src="@drawable/selector_icon_menu_order" />
<TextView
android:id="@+id/tv_menu_order"
style="@style/menuTextStyle"
android:text="订单" />
</LinearLayout>
<LinearLayout
android:id="@+id/ll_menu_mine"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:orientation="vertical">
<ImageView
android:id="@+id/iv_menu_mine"
style="@style/menuIconStyle"
android:src="@drawable/selector_icon_menu_mine" />
<TextView
android:id="@+id/tv_menu_mine"
style="@style/menuTextStyle"
android:text="我的" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
最外层用竖向排列的 LinearLayout
包裹,它有两个子节点,上面是用于滑动和装载 Fragment
的 ViewPager
,下面是五个 Tab
的布局。
为了方便管理把几个 ImageView
和 TextView
的共有属性抽取到 styles.xml
里了:
<!--菜单栏的图标样式-->
<style name="menuIconStyle" >
<item name="android:layout_width">25dp</item>
<item name="android:layout_height">25dp</item>
</style>
<!--菜单栏的文字样式-->
<style name="menuTextStyle">
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:textColor">@drawable/selector_menu_text_color</item>
<item name="android:textSize">12sp</item>
<item name="android:layout_marginTop">3dp</item>
</style>
有了布局文件之后,就开始真正的自定义 View
吧。
2. 写 Java 代码自定义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 List<BaseFragment> mFragmentList;
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<BaseFragment> 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);
- 实现 tab 的点击事件与
ViewPager
的滑动绑定需要在两个地方写逻辑: 1)tab 的点击回调里执行下面两行代码,分别使 tab 变为选中状态和让ViewPager
滑动到相应位置。
2)在selectTab(MenuTab.HOME); //使ViewPager跟随tab点击事件滑动 viewPager.setCurrentItem(0);
ViewPager
的监听方法onPageSelected()
中,每滑动到一个页面,就调用selectTab(MenuTab.HOME)
方法将对应的 tab 设置为选中状态。 - 记得在构造方法里设置默认的选中项:
//设置默认的选中项 selectTab(MenuTab.HOME);
好啦,到这自定义 View 已经完成了。下面看看怎么使用。
四、使用
在主页的布局文件里直接引用:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="com.cachecats.meituan.app.MainActivity">
<com.cachecats.meituan.widget.bottomtab.CustomBottomTabWidget
android:id="@+id/tabWidget"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
然后在 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 List<BaseFragment> fragmentList;
@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 标签滑动切换实例。
源码地址:github.com/cachecats/L…
欢迎下载,欢迎 star
,欢迎点赞~