目錄
- TabLayout原理
- 具體實作
- 遇到的問題
- 總結
一、TabLayout原理
1.1 TabLayout與ViewPager的綁定原理
往往TabLayout都是和ViewPager關聯使用,下面就從TabLayout源碼進行分析ViewPager和TabLayout如何配合使用。
下面的代碼是最簡單的一個viewpager+tablayout+fragment的使用場景,那麼最開始就從setupWithViewPage()對源碼進行分析。
mFragments = new ArrayList<>();
mFragments.add(new NewsTypeFragment());
mFragments.add(new NewsTypeFragment());
mFragments.add(new NewsTypeFragment());
mViewPagerFragmentAdapter = new ViewPagerFragmentAdapter(getChildFragmentManager(), mFragments);
viewpager.setAdapter(mViewPagerFragmentAdapter);
tablayout.setupWithViewPager(viewpager);
viewpager和tablayout存在雙向綁定的機制:

綁定流程如下:
通過監聽viewpager, 與之綁定的TabLayout也随viewpager更改視圖,以下是TabLayoutOnPageChangeListener的源碼。
public static class TabLayoutOnPageChangeListener implements ViewPager.OnPageChangeListener {
private final WeakReference<TabLayout> mTabLayoutRef;
private int mPreviousScrollState;
private int mScrollState;
public TabLayoutOnPageChangeListener(TabLayout tabLayout) {
mTabLayoutRef = new WeakReference<>(tabLayout);
}
@Override
public void onPageScrollStateChanged(final int state) {
mPreviousScrollState = mScrollState;
mScrollState = state;
}
@Override
public void onPageScrolled(final int position, final float positionOffset,
final int positionOffsetPixels) {
final TabLayout tabLayout = mTabLayoutRef.get();
if (tabLayout != null) {
// Only update the text selection if we're not settling, or we are settling after
// being dragged
final boolean updateText = mScrollState != SCROLL_STATE_SETTLING ||
mPreviousScrollState == SCROLL_STATE_DRAGGING;
// Update the indicator if we're not settling after being idle. This is caused
// from a setCurrentItem() call and will be handled by an animation from
// onPageSelected() instead.
final boolean updateIndicator = !(mScrollState == SCROLL_STATE_SETTLING
&& mPreviousScrollState == SCROLL_STATE_IDLE);
tabLayout.setScrollPosition(position, positionOffset, updateText, updateIndicator);
}
}
@Override
public void onPageSelected(final int position) {
final TabLayout tabLayout = mTabLayoutRef.get();
if (tabLayout != null && tabLayout.getSelectedTabPosition() != position
&& position < tabLayout.getTabCount()) {
// Select the tab, only updating the indicator if we're not being dragged/settled
// (since onPageScrolled will handle that).
final boolean updateIndicator = mScrollState == SCROLL_STATE_IDLE
|| (mScrollState == SCROLL_STATE_SETTLING
&& mPreviousScrollState == SCROLL_STATE_IDLE);
tabLayout.selectTab(tabLayout.getTabAt(position), updateIndicator);
}
}
void reset() {
mPreviousScrollState = mScrollState = SCROLL_STATE_IDLE;
}
}
其中onPageScrollStateChanged() 得到viewpager的三種狀态,并儲存前置狀态和目前狀态,影響後續頁面布局和動畫效果。
/**
* Indicates that the pager is in an idle, settled state. The current page
* is fully in view and no animation is in progress.
* 表示viewpager的狀态為靜止狀态(無動畫、無滑動)
*/
public static final int SCROLL_STATE_IDLE = ;
/**
* Indicates that the pager is currently being dragged by the user.
* 表示viewpager的狀态滑動狀态
*/
public static final int SCROLL_STATE_DRAGGING = ;
/**
* Indicates that the pager is in the process of settling to a final position.
*/
public static final int SCROLL_STATE_SETTLING = ;
public void onPageScrolled(final int position, final float positionOffset,final int positionOffsetPixels)該方法監聽的是Viewpager的位置以及每個page的偏移量(這裡解釋一下positionOffset,它對應ViewPager目前page的偏移量,其中左劃數值從0-1,右滑數值從1-0,後續會根據positionOffset計算整個HorizontalScrollView的位置)、對應的像素位置,==onPageScrolled()和下面onPageSelected() 是與TabLayout關聯最關鍵的兩個方法== ,在這個方法中,會将position和positionOffset傳遞給setScrollPosition(),并通過這個方法更新TabLayout視圖,其中包括,底部indicater(tab追蹤條),text(tab的名稱),HorizontalScrollView的偏移位置,運用對偏移量四舍五入的計算方法,設定tab标題顔色。這裡要尤其注意,onPageScrolled傳回的position會根據滑動方向改變,左滑position保持目前pager的值,而從靜止開始往右滑動則變成目前page-1,尤其區分這裡的position和onPageSelected傳回的position。
void setScrollPosition(int position, float positionOffset, boolean updateSelectedText,
boolean updateIndicatorPosition) {
final int roundedPosition = Math.round(position + positionOffset);
if (roundedPosition < || roundedPosition >= mTabStrip.getChildCount()) {
return;
}
// Set the indicator position, if enabled
if (updateIndicatorPosition) {
mTabStrip.setIndicatorPositionFromTabPosition(position, positionOffset);
}
// Now update the scroll position, canceling any running animation
if (mScrollAnimator != null && mScrollAnimator.isRunning()) {
mScrollAnimator.cancel();
}
scrollTo(calculateScrollXForTab(position, positionOffset), );
// Update the 'selected state' view as we scroll, if enabled
if (updateSelectedText) {
setSelectedTabView(roundedPosition);
}
}
public void onPageSelected(final int position) 該方法隻有在動畫完成,頁面靜止的時候調用,position顯示目前page的頁數(從0開始)
二、具體實作
2.1 tab底部indicator自定義
原生TabLayout的底部indicator預設是矩形條,并且隻能修改其高度,是以它的可定制性非常低,而繪制矩形條的類SlidingTabStrip是私密内部類,是以為了自定義indcator需要将tablayout整體移植到自己的工程項目内,并修改SlidingTabStrip這個類。這裡提供簡單的三種自定義圖形
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
// Thick colored underline below the current selection
if (mIndicatorLeft >= && mIndicatorRight > mIndicatorLeft) {
//自定義畫圓
//canvas.drawCircle((mIndicatorLeft + mIndicatorRight) / 2, getHeight() - mSelectedIndicatorHeight, mSelectedIndicatorHeight, mSelectedIndicatorPaint);
//自定義三角形
Path path = new Path();
path.moveTo((mIndicatorLeft + mIndicatorRight) / , getHeight() - mSelectedIndicatorHeight - );
path.lineTo((mIndicatorLeft + mIndicatorRight) / - mSelectedIndicatorHeight - , getHeight());
path.lineTo((mIndicatorLeft + mIndicatorRight) / + mSelectedIndicatorHeight + , getHeight());
path.close();
canvas.drawPath(path, mSelectedIndicatorPaint);
//自定義矩形、條形(預設)
//canvas.drawRect(mIndicatorLeft, getHeight() - mSelectedIndicatorHeight,
// mIndicatorRight, getHeight(), mSelectedIndicatorPaint);
}
}
2.2 tab滑動機制自定義
通常TabLayout與fragment+ViewPager一起使用,不知道大家有沒有遇到過這種情況,當設定ViewPager的setCurrentItem方法時,可以選擇pager的滑動是否是smooth,true的時候,tablayout也是smooth,false的時候,tablayout的切換也變得生硬,包括現在的網易新聞,今日頭條的tablayout就是這種機制。産生這種不協調的原因是因為上述監聽ViewPager的onPageScrolled方法,點選tab的時候onPageScrolled方法傳回的positionOffset一直為0,每次點選tab時,最後一次調用的是onPageScrolled方法而不是onPageSelected方法,通過點選tab時候的log可以看出來:
- :: -/com.deli.newsdemo D/mTabLayoutRef: onPageScrolled:
- :: -/com.deli.newsdemo D/mTabLayoutRef: onPageSelected:
- :: -/com.deli.newsdemo D/mTabLayoutRef: onPageScrolled:
==是以,以最後一次onPageScrolled的監聽為主,同時positionOffset為0,就導緻沒有動畫效果,也就是導緻點選tab很生硬的主要原因==!
那有沒有一種機制一能防止viewpager産生過渡動畫,又能讓tablayout有過渡動畫。 其實很簡單,就是監聽positionOffset,當positionOffset大于0時執行setScrollPosition方法:
@Override
public void onPageScrolled(final int position, final float positionOffset,
final int positionOffsetPixels) {
final TabLayout tabLayout = mTabLayoutRef.get();
Log.d("mTabLayoutRef", "onPageScrolled:1 ");
if (tabLayout != null) {
// Only update the text selection if we're not settling, or we are settling after
// being dragged
final boolean updateText = mScrollState != SCROLL_STATE_SETTLING ||
mPreviousScrollState == SCROLL_STATE_DRAGGING;
// Update the indicator if we're not settling after being idle. This is caused
// from a setCurrentItem() call and will be handled by an animation from
// onPageSelected() instead.
final boolean updateIndicator = !(mScrollState == SCROLL_STATE_SETTLING
&& mPreviousScrollState == SCROLL_STATE_IDLE);
if (positionOffset>)
tabLayout.setScrollPosition(position, positionOffset, updateText, updateIndicator);
}
}
三、遇到的問題
遇到的最主要的問題就是在tab滑動機制自定義時,由于兩個監聽用了同一種動畫,是以監聽結果的順序就很重要,不然顯示的結果差強人意,通過debug發現傳回position的順序是最後傳回onPageScrolled方法而不是onPageSelected,才發現問題所在。
四、總結
這次在寫自己的demo的時候,本來是想仿寫網易新聞和今日頭條的頂部滑動菜單欄,然後發現都有這種點選tab時菜單欄無滾動效果的問題,通過看了TabLayout的源碼,并改寫才完善了這個功能,提高了使用者體驗,自己也積累了不少知識,總之再小的功能都有不斷發掘和革新的價值!
附:Demo位址