1、 問題描述java
最近項目中有個需求:一個頁面頂部有3個tab,每個tab分別展現一個不一樣的頁面,點擊tab 切換到對應頁面。進入頁面是默認選中第一個頁面。ide
這不很簡單的一個需求嘛?很明顯,用TabLayout 分分鐘實現,因而打開Android Studio ,幾分鐘後寫下了以下代碼:源碼分析
public class TabActivity extends AppCompatActivity {
private TabLayout mTabLayout;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.tab_layout_ac2);
mTabLayout = (TabLayout) findViewById(R.id.tab_layout2);
mTabLayout.addTab(mTabLayout.newTab().setText("個性推薦"),true);//設置默認選中
mTabLayout.addTab(mTabLayout.newTab().setText("歌單"));
mTabLayout.addTab(mTabLayout.newTab().setText("主播電臺"));
final List<Fragment> fragments = new ArrayList<>();
fragments.add(FirstFragment.newInstance());
fragments.add(SecondFragment.newInstance());
fragments.add(ThirdFragment.newInstance());
mTabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {
Log.e("TAG","tab position:"+tab.getPosition());
replaceFragment(fragments.get(tab.getPosition()));
}
@Override
public void onTabUnselected(TabLayout.Tab tab) {
}
@Override
public void onTabReselected(TabLayout.Tab tab) {
}
});
}
private void replaceFragment(Fragment fragment){
getSupportFragmentManager().beginTransaction().replace(R.id.tab_container,fragment).commit();
}
}複製代碼
寫完以後,一運行,發現tab 顯示出來了,第一個tab也選中了(效果以下圖),可是第一頁的內容咋沒展現出來呢? 而後點擊tab切換,切換到後面2個tab時,能夠加載出頁面,而後再次點擊第一個tab ,第一個tab 的頁面也展現出來了。測試
第一次進來時,tab 下面的頁面內容沒有展現出來,很明顯,那就是第一次進來的時候onTabSelected 回調沒有被執行。由於咱們是在onTabSelected 來加載頁面的。通過幾回反覆測試(日誌和斷點調試),肯定了是第一次進入的時候,onTabSelected沒有被回調。this
那麼,爲何第一次進入的時候,onTabSelected沒有被回調了?反覆檢查了幾回代碼,沒有發現問題。既然沒有發現問題,那麼,咱們就只有去看源碼了,看一下TabLayoout 初始化完成後,在何時調用的onTabSelected 回調方法?spa
2、源碼追蹤設計
咱們要看一下源碼中TabLayout初始化後,在何時調用的onTabSelected。咱們注意到,添加Tab的時候,有這麼一個方法:3d
mTabLayout.addTab(mTabLayout.newTab().setText("個性推薦"),true);複製代碼
addTab 方法有2個參數,第一個是要添加的Tab,第二個參數是是否設置爲默認選中。上面這行代碼的意思是,添加一個Tab,而且設置這個tab爲默認選中的Tab。調試
接下來就走讀一下源碼,看一下在什麼時候回調的onTabSelected
方法:日誌
1,首先看一下OnTabSelectedListener 的設置
public void addOnTabSelectedListener(@NonNull OnTabSelectedListener listener) {
if (!mSelectedListeners.contains(listener)) {
mSelectedListeners.add(listener);
}
}複製代碼
很簡單,就是將OnTabSelectedListener保存到一個列表裏。沒有作其餘事情。
2, 以 addTab
方法爲入口,順藤摸瓜。
// 添加一個Tab ,而且設置爲是否選中
// 實際調用方法 addTab(@NonNull Tab tab, int position, boolean setSelected)
public void addTab(@NonNull Tab tab, boolean setSelected) {
addTab(tab, mTabs.size(), setSelected);
}
//1, 首先將Tab 保存到一個列表中,記錄位置
//2, 將tab 添加到TabLayout 中
//3, 判斷時候選中(這就是咱們要的)
public void addTab(@NonNull Tab tab, int position, boolean setSelected) {
if (tab.mParent != this) {
throw new IllegalArgumentException("Tab belongs to a different TabLayout.");
}
configureTab(tab, position);
addTabView(tab);
// 是否選中
if (setSelected) {
tab.select();
}
}複製代碼
如上代碼,若是setSelected
爲true
,就調用了tab 的select()
方法,咱們看一下select()
方法:
public void select() {
if (mParent == null) {
throw new IllegalArgumentException("Tab not attached to a TabLayout");
}
mParent.selectTab(this);
}複製代碼
調用了Parent的select
方法,Parent 是誰?固然是TabLayout 啦。因此繼續深刻,看看TabLayout的select
方法。
void selectTab(Tab tab) {
//實際調用 selectTab(final Tab tab, boolean updateIndicator)
selectTab(tab, true);
}
//
void selectTab(final Tab tab, boolean updateIndicator) {
final Tab currentTab = mSelectedTab;
if (currentTab == tab) { // 若是新選中的Tab 和當前Tab 相同,回調onTabReselected 方法
if (currentTab != null) {
dispatchTabReselected(tab);
animateToTab(tab.getPosition());
}
} else { // 若是不一樣,則回調 onTabSelected方法
final int newPosition = tab != null ? tab.getPosition() : Tab.INVALID_POSITION;
if (updateIndicator) {
if ((currentTab == null || currentTab.getPosition() == Tab.INVALID_POSITION)
&& newPosition != Tab.INVALID_POSITION) {
// If we don't currently have a tab, just draw the indicator
setScrollPosition(newPosition, 0f, true);
} else {
animateToTab(newPosition);
}
if (newPosition != Tab.INVALID_POSITION) {
setSelectedTabView(newPosition);
}
}
if (currentTab != null) {
dispatchTabUnselected(currentTab);
}
mSelectedTab = tab; // 記錄選中的Tab
if (tab != null) {
dispatchTabSelected(tab); // 處理選中Tab
}
}
}複製代碼
TabLayout 的selectTab
方法中最終調用了dispatchTabSelected方法:
private void dispatchTabSelected(@NonNull final Tab tab) {
for (int i = mSelectedListeners.size() - 1; i >= 0; i--) {
mSelectedListeners.get(i).onTabSelected(tab); // 找到了,在這裏循環列表,分別調用onTabSelected 方法。
}
}複製代碼
好了,到這裏就找到了onTabSelected
回調的地方了,有沒有很熟悉 mSelectedListeners
?固然了,在咱們調用addOnTabSelectedListener
設置監聽器的時候,就是保存到mSelectedListeners
的。咱們來捋一捋:
如上圖:
當咱們執行addTab方法添加Tab的時候,最後會調用到 dispatchTabSelected
方法,在dispatchTabSelected
方法裏面調用addOnTabSelectedListener
的onTabSelected()
方法。可是這個時候,mSelectedListeners 爲空(由於這個時候咱們尚未設置OnTabSelectedListener),所以,就沒有回調到onTabSelected
。
分析到此咱們也就明白了,第一次沒有回調到 onTabSelected 方法,是由於咱們寫的程序的順序問題,應該在添加Tab 以前 添加OnTabSelectedListener 監聽。
3、解決方案
經過上面的源碼分析,咱們知道了,第一次沒有執行OnTabSelected回調,是由於咱們的代碼順序問題(這個是Google 的坑,設計得有問題啊),所以,要想第一次進入的時候回調到OnTabSelected方法,咱們應該先設置
addOnTabSelectedListener 監聽器,再添加Tab,咱們原來的程序順序調整一下:
// 1, 設置 addOnTabSelectedListener
// 設置 addOnTabSelectedListener 必須在 addTab 以前。
mTabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {
Log.e("TAG","tab position:"+tab.getPosition());
replaceFragment(fragments.get(tab.getPosition()));
}
@Override
public void onTabUnselected(TabLayout.Tab tab) {
}
@Override
public void onTabReselected(TabLayout.Tab tab) {
}
});
// 2.添加Tab
mTabLayout.addTab(mTabLayout.newTab().setText("個性推薦"),true);
mTabLayout.addTab(mTabLayout.newTab().setText("歌單"));
mTabLayout.addTab(mTabLayout.newTab().setText("主播電臺"));複製代碼
經過如上調整,再運行程序,完美解決,第一次進入以下:
4、總結
本篇文章是對TabLayout 使用過程當中遇到的一個的坑記錄和總結,可能有的人不會遇到,也沒有人會注意到(若是你一開始就把順序寫對了的話),可是,若是遇到的話,要看源碼才知道緣由。這也是咱們解決問題的一種思路。不少時候,咱們須要去了解一下原理和源碼的實現,這樣才能幫助咱們更好的理解,以避免使用的時候踩坑。本文沒有介紹TabLayout的詳細使用方法和使用場景,要了解更多請看我前面的文章Material Design 之 TabLayout 使用