目錄
- TabLayout原理
- 具體實(shí)現(xiàn)
- 遇到的問(wèn)題
- 總結(jié)
一蜓陌、TabLayout原理
1.1 TabLayout與ViewPager的綁定原理
往往TabLayout都是和ViewPager聯(lián)動(dòng)使用晴竞,下面就從TabLayout源碼進(jìn)行分析ViewPager和TabLayout如何配合使用卵史。
下面的代碼是最簡(jiǎn)單的一個(gè)viewpager+tablayout+fragment的使用場(chǎng)景烛占,那么最開(kāi)始就從setupWithViewPage()對(duì)源碼進(jìn)行分析。
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存在雙向綁定的機(jī)制:
綁定流程如下:
通過(guò)監(jiān)聽(tīng)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的三種狀態(tài),并保存前置狀態(tài)和當(dāng)前狀態(tài),影響后續(xù)頁(yè)面布局和動(dòng)畫效果中燥。
/**
* Indicates that the pager is in an idle, settled state. The current page
* is fully in view and no animation is in progress.
* 表示viewpager的狀態(tài)為靜止?fàn)顟B(tài)(無(wú)動(dòng)畫寇甸、無(wú)滑動(dòng))
*/
public static final int SCROLL_STATE_IDLE = 0;
/**
* Indicates that the pager is currently being dragged by the user.
* 表示viewpager的狀態(tài)滑動(dòng)狀態(tài)
*/
public static final int SCROLL_STATE_DRAGGING = 1;
/**
* Indicates that the pager is in the process of settling to a final position.
*/
public static final int SCROLL_STATE_SETTLING = 2;
public void onPageScrolled(final int position, final float positionOffset,final int positionOffsetPixels)該方法監(jiān)聽(tīng)的是Viewpager的位置以及每個(gè)page的偏移量(這里解釋一下positionOffset,它對(duì)應(yīng)ViewPager當(dāng)前page的偏移量疗涉,其中左劃數(shù)值從0-1,右滑數(shù)值從1-0拿霉,后續(xù)會(huì)根據(jù)positionOffset計(jì)算整個(gè)HorizontalScrollView的位置)、對(duì)應(yīng)的像素位置咱扣,onPageScrolled()和下面onPageSelected() 是與TabLayout聯(lián)動(dòng)最關(guān)鍵的兩個(gè)方法 ,在這個(gè)方法中绽淘,會(huì)將position和positionOffset傳遞給setScrollPosition(),并通過(guò)這個(gè)方法更新TabLayout視圖,其中包括闹伪,底部indicater(tab追蹤條)沪铭,text(tab的名稱)壮池,HorizontalScrollView的偏移位置,運(yùn)用對(duì)偏移量四舍五入的計(jì)算方法杀怠,設(shè)置tab標(biāo)題顏色椰憋。這里要尤其注意,onPageScrolled返回的position會(huì)根據(jù)滑動(dòng)方向改變赔退,左滑position保持當(dāng)前pager的值橙依,而從靜止開(kāi)始往右滑動(dòng)則變成當(dāng)前page-1,尤其區(qū)分這里的position和onPageSelected返回的position离钝。
void setScrollPosition(int position, float positionOffset, boolean updateSelectedText,
boolean updateIndicatorPosition) {
final int roundedPosition = Math.round(position + positionOffset);
if (roundedPosition < 0 || 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), 0);
// Update the 'selected state' view as we scroll, if enabled
if (updateSelectedText) {
setSelectedTabView(roundedPosition);
}
}
public void onPageSelected(final int position) 該方法只有在動(dòng)畫完成票编,頁(yè)面靜止的時(shí)候調(diào)用,position顯示當(dāng)前page的頁(yè)數(shù)(從0開(kāi)始)
二卵渴、具體實(shí)現(xiàn)
2.1 tab底部indicator自定義
原生TabLayout的底部indicator默認(rèn)是矩形條慧域,并且只能修改其高度,所以它的可定制性非常低浪读,而繪制矩形條的類SlidingTabStrip是私密內(nèi)部類昔榴,所以為了自定義indcator需要將tablayout整體移植到自己的工程項(xiàng)目?jī)?nèi),并修改SlidingTabStrip這個(gè)類碘橘。這里提供簡(jiǎn)單的三種自定義圖形
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
// Thick colored underline below the current selection
if (mIndicatorLeft >= 0 && mIndicatorRight > mIndicatorLeft) {
//自定義畫圓
//canvas.drawCircle((mIndicatorLeft + mIndicatorRight) / 2, getHeight() - mSelectedIndicatorHeight, mSelectedIndicatorHeight, mSelectedIndicatorPaint);
//自定義三角形
Path path = new Path();
path.moveTo((mIndicatorLeft + mIndicatorRight) / 2, getHeight() - mSelectedIndicatorHeight - 10);
path.lineTo((mIndicatorLeft + mIndicatorRight) / 2 - mSelectedIndicatorHeight - 10, getHeight());
path.lineTo((mIndicatorLeft + mIndicatorRight) / 2 + mSelectedIndicatorHeight + 10, getHeight());
path.close();
canvas.drawPath(path, mSelectedIndicatorPaint);
//自定義矩形互订、條形(默認(rèn))
//canvas.drawRect(mIndicatorLeft, getHeight() - mSelectedIndicatorHeight,
// mIndicatorRight, getHeight(), mSelectedIndicatorPaint);
}
}
2.2 tab滑動(dòng)機(jī)制自定義
通常TabLayout與fragment+ViewPager一起使用,不知道大家有沒(méi)有遇到過(guò)這種情況痘拆,當(dāng)設(shè)置ViewPager的setCurrentItem方法時(shí)仰禽,可以選擇pager的滑動(dòng)是否是smooth夯缺,true的時(shí)候赔桌,tablayout也是smooth,false的時(shí)候工闺,tablayout的切換也變得生硬桥氏,包括現(xiàn)在的網(wǎng)易新聞温峭,今日頭條的tablayout就是這種機(jī)制。產(chǎn)生這種不協(xié)調(diào)的原因是因?yàn)樯鲜霰O(jiān)聽(tīng)ViewPager的onPageScrolled方法字支,點(diǎn)擊tab的時(shí)候onPageScrolled方法返回的positionOffset一直為0凤藏,每次點(diǎn)擊tab時(shí),最后一次調(diào)用的是onPageScrolled方法而不是onPageSelected方法堕伪,通過(guò)debug點(diǎn)擊tab時(shí)候的log可以看出來(lái):
02-04 08:35:59.965 7206-7206/com.deli.newsdemo D/mTabLayoutRef: onPageScrolled:1
02-04 08:36:08.591 7206-7206/com.deli.newsdemo D/mTabLayoutRef: onPageSelected: 2
02-04 08:36:08.597 7206-7206/com.deli.newsdemo D/mTabLayoutRef: onPageScrolled:1
所以揖庄,以最后一次onPageScrolled的監(jiān)聽(tīng)為主,同時(shí)positionOffset為0欠雌,就導(dǎo)致沒(méi)有動(dòng)畫效果抠艾,也就是導(dǎo)致點(diǎn)擊tab很生硬的主要原因!
那有沒(méi)有一種機(jī)制一能防止viewpager產(chǎn)生過(guò)渡動(dòng)畫桨昙,又能讓tablayout有過(guò)渡動(dòng)畫检号。 其實(shí)很簡(jiǎn)單,就是監(jiān)聽(tīng)positionOffset蛙酪,當(dāng)positionOffset大于0時(shí)執(zhí)行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>0)
tabLayout.setScrollPosition(position, positionOffset, updateText, updateIndicator);
}
}
三齐苛、遇到的問(wèn)題
遇到的最主要的問(wèn)題就是在tab滑動(dòng)機(jī)制自定義時(shí),由于兩個(gè)監(jiān)聽(tīng)用了同一種動(dòng)畫桂塞,所以監(jiān)聽(tīng)結(jié)果的順序就很重要凹蜂,不然顯示的結(jié)果差強(qiáng)人意,通過(guò)debug發(fā)現(xiàn)返回position的順序是最后返回onPageScrolled方法而不是onPageSelected阁危,才發(fā)現(xiàn)問(wèn)題所在玛痊。
四、總結(jié)
這次在寫自己的demo的時(shí)候狂打,本來(lái)是想仿寫網(wǎng)易新聞和今日頭條的頂部滑動(dòng)菜單欄擂煞,然后發(fā)現(xiàn)都有這種點(diǎn)擊tab時(shí)菜單欄無(wú)滾動(dòng)效果的問(wèn)題,通過(guò)看了TabLayout的源碼趴乡,并改寫才完善了這個(gè)功能对省,提高了用戶體驗(yàn),自己也積累了不少知識(shí)晾捏,總之再小的功能都有不斷發(fā)掘和革新的價(jià)值蒿涎!
附:Demo地址