這是【從零擼美團】系列文章第三篇
【從零擼美團】是一個高仿美團的開源專案,旨在鞏固 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
,歡迎點贊~