内容简介:这是【从零撸美团】系列文章第三篇【从零撸美团】是一个高仿美团的开源项目,旨在巩固 Android 相关知识的同时,帮助到有需要的小伙伴。GitHub 源码地址:
这是【从零撸美团】系列文章第三篇
【从零撸美团】是一个高仿美团的开源项目,旨在巩固 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
滑动到相应位置。selectTab(MenuTab.HOME); //使ViewPager跟随tab点击事件滑动 viewPager.setCurrentItem(0); 复制代码
2)在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
,欢迎点赞~
以上所述就是小编给大家介绍的《Android从零撸美团(三) - Android多标签tab滑动切换 - 自定义View快速实现高度定制封装》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
程序员修炼之道(影印版)
Andrew Hunt、David Thomas / 中国电力出版社 / 2003-8-1 / 39.00
本书直击编程陈地,穿过了软件开发中日益增长的规范和技术藩篱,对核心过程进行了审视——即根据需求,创建用户乐于接受的、可工作和易维护的代码。本书包含的内容从个人责任到职业发展,直至保持代码灵活和易于改编重用的架构技术。从本书中将学到防止软件变质、消除复制知识的陷阱、编写灵活、动态和易适应的代码、避免出现相同的设计、用契约、断言和异常对代码进行防护等内容。一起来看看 《程序员修炼之道(影印版)》 这本书的介绍吧!