前言
很久很久沒寫過源碼解析了,不是自己沒有看了,只是沒有記錄了,卻發(fā)現(xiàn)不記錄的話,似懂非懂,時間久了就忘得差不多了,用到了還是得再學一遍,忍住提筆一篇 TabLayout 源碼學習。
Hello World
依賴
添加 support design 包
implementation 'com.android.support:design:27.1.1'
xml
添加一個 TabLayout 就可以了
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout 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"
tools:context=".TabLayoutActivity">
<android.support.design.widget.TabLayout
android:id="@+id/tab_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</android.support.constraint.ConstraintLayout>
MainActivity.java
import android.support.design.widget.TabLayout;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
public class TabLayoutActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_tab_layout);
TabLayout mTabLayout = findViewById(R.id.tab_layout);
// 添加 tab item
mTabLayout.addTab(mTabLayout.newTab().setText("TAB1"));
mTabLayout.addTab(mTabLayout.newTab().setText("TAB2"));
mTabLayout.addTab(mTabLayout.newTab().setText("TAB3"));
mTabLayout.addTab(mTabLayout.newTab().setText("TAB4"));
}
}
效果

源碼學習
其實,實現(xiàn)這樣一個布局并不難,讓我們來看看里面所有的內容
前世今生
繼承自 HorizontalScrollView 因為他支持滾動
public class TabLayout extends HorizontalScrollView
TabLayout 支持兩種模式,一種是固定的,一種是可滾動的(tab 太多,一屏顯示不下,可使用這種模式,否則默認為平分)
/**
* Scrollable tabs display a subset of tabs at any given moment, and can contain longer tab
* labels and a larger number of tabs. They are best used for browsing contexts in touch
* interfaces when users don’t need to directly compare the tab labels.
*
* @see #setTabMode(int)
* @see #getTabMode()
*/
public static final int MODE_SCROLLABLE = 0;
/**
* Fixed tabs display all tabs concurrently and are best used with content that benefits from
* quick pivots between tabs. The maximum number of tabs is limited by the view’s width.
* Fixed tabs have equal width, based on the widest tab label.
*
* @see #setTabMode(int)
* @see #getTabMode()
*/
public static final int MODE_FIXED = 1;
/**
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
@IntDef(value = {MODE_SCROLLABLE, MODE_FIXED})
@Retention(RetentionPolicy.SOURCE)
public @interface Mode {}
Tab 的位置有兩種,一種是居中,一種是平分
/**
* Gravity used to fill the {@link TabLayout} as much as possible. This option only takes effect
* when used with {@link #MODE_FIXED}.
*
* @see #setTabGravity(int)
* @see #getTabGravity()
*/
public static final int GRAVITY_FILL = 0;
/**
* Gravity used to lay out the tabs in the center of the {@link TabLayout}.
*
* @see #setTabGravity(int)
* @see #getTabGravity()
*/
public static final int GRAVITY_CENTER = 1;
/**
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
@IntDef(flag = true, value = {GRAVITY_FILL, GRAVITY_CENTER})
@Retention(RetentionPolicy.SOURCE)
public @interface TabGravity {}
居中模式

創(chuàng)建 Tab
使用代碼創(chuàng)建 Tab
public Tab newTab() {
Tab tab = sTabPool.acquire();
if (tab == null) {
tab = new Tab();
}
tab.mParent = this;
tab.mView = createTabView(tab);
return tab;
}
Tab 還使用了 Pool,還是挺細心的
private static final Pools.Pool<Tab> sTabPool = new Pools.SynchronizedPool<>(16);
可滑動的指示條形圖
自定義 ViewGroup
private class SlidingTabStrip extends LinearLayout
onMeasure
如果設置了 MODE_FIXED 和 GRAVITY_CENTER 則需要重新測量,目的就是讓居中,每個 ITEM 的寬度都是一樣的,而且等于最大的一個,如果一屏放得下則需要重新設置每個 ITEM 的大小,并且重新測量。如果發(fā)不下,那么側設置GRAVITY_FILL
@Override
protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY) {
// HorizontalScrollView will first measure use with UNSPECIFIED, and then with
// EXACTLY. Ignore the first call since anything we do will be overwritten anyway
return;
}
// 重新測量
if (mMode == MODE_FIXED && mTabGravity == GRAVITY_CENTER) {
final int count = getChildCount();
// First we'll find the widest tab
int largestTabWidth = 0;
for (int i = 0, z = count; i < z; i++) {
View child = getChildAt(i);
if (child.getVisibility() == VISIBLE) {
largestTabWidth = Math.max(largestTabWidth, child.getMeasuredWidth());
}
}
if (largestTabWidth <= 0) {
// If we don't have a largest child yet, skip until the next measure pass
return;
}
// 間隔
final int gutter = dpToPx(FIXED_WRAP_GUTTER_MIN);
boolean remeasure = false;
// 一屏放得下
if (largestTabWidth * count <= getMeasuredWidth() - gutter * 2) {
// If the tabs fit within our width minus gutters, we will set all tabs to have
// the same width
for (int i = 0; i < count; i++) {
final LinearLayout.LayoutParams lp =
(LayoutParams) getChildAt(i).getLayoutParams();
if (lp.width != largestTabWidth || lp.weight != 0) {
lp.width = largestTabWidth;
lp.weight = 0;
remeasure = true;
}
}
} else {
// If the tabs will wrap to be larger than the width minus gutters, we need
// to switch to GRAVITY_FILL
mTabGravity = GRAVITY_FILL;
updateTabViews(false);
remeasure = true;
}
if (remeasure) {
// Now re-measure after our changes
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
}
如何實現(xiàn)動畫?
通過移動 IndicatorView
onLayout
mIndicatorAnimator 是動畫輔助類,在 onLayout 中,非空而且正在運行則看取消,然后調用 animateIndicatorToPosition,動畫調用,否則直接設置位置,不支持動畫
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
if (mIndicatorAnimator != null && mIndicatorAnimator.isRunning()) {
// If we're currently running an animation, lets cancel it and start a
// new animation with the remaining duration
mIndicatorAnimator.cancel();
final long duration = mIndicatorAnimator.getDuration();
animateIndicatorToPosition(mSelectedPosition,
Math.round((1f - mIndicatorAnimator.getAnimatedFraction()) * duration));
} else {
// If we've been layed out, update the indicator position
updateIndicatorPosition();
}
}
updateIndicatorPosition 首先獲取選中的 View,然后看 mSelectionOffset 是否大于零,說明發(fā)生滾動,則需要重新計算新位置
private void updateIndicatorPosition() {
final View selectedTitle = getChildAt(mSelectedPosition);
int left, right;
if (selectedTitle != null && selectedTitle.getWidth() > 0) {
left = selectedTitle.getLeft();
right = selectedTitle.getRight();
if (mSelectionOffset > 0f && mSelectedPosition < getChildCount() - 1) {
// Draw the selection partway between the tabs
View nextTitle = getChildAt(mSelectedPosition + 1);
left = (int) (mSelectionOffset * nextTitle.getLeft() +
(1.0f - mSelectionOffset) * left);
right = (int) (mSelectionOffset * nextTitle.getRight() +
(1.0f - mSelectionOffset) * right);
}
} else {
left = right = -1;
}
setIndicatorPosition(left, right);
}
// mIndicatorLeft 和 mIndicatorRight 控制了線的起始位置
void setIndicatorPosition(int left, int right) {
if (left != mIndicatorLeft || right != mIndicatorRight) {
// If the indicator's left/right has changed, invalidate
mIndicatorLeft = left;
mIndicatorRight = right;
ViewCompat.postInvalidateOnAnimation(this);
}
}
移動動畫,移動間隔大的話,并不會從當前位置直接移動,而是跳躍一段距離再移動,通過 startLeft 和 startRight 控制,并且使用 ValueAnimator 來實現(xiàn)動畫
這個不錯,同意了 fraction 0-1 ,通過函數(shù)計算進度
setIndicatorPosition(
AnimationUtils.lerp(startLeft, targetLeft, fraction),
AnimationUtils.lerp(startRight, targetRight, fraction));
void animateIndicatorToPosition(final int position, int duration) {
if (mIndicatorAnimator != null && mIndicatorAnimator.isRunning()) {
mIndicatorAnimator.cancel();
}
final boolean isRtl = ViewCompat.getLayoutDirection(this)
== ViewCompat.LAYOUT_DIRECTION_RTL;
final View targetView = getChildAt(position);
if (targetView == null) {
// If we don't have a view, just update the position now and return
updateIndicatorPosition();
return;
}
final int targetLeft = targetView.getLeft();
final int targetRight = targetView.getRight();
final int startLeft;
final int startRight;
if (Math.abs(position - mSelectedPosition) <= 1) {
// If the views are adjacent, we'll animate from edge-to-edge
startLeft = mIndicatorLeft;
startRight = mIndicatorRight;
} else {
// Else, we'll just grow from the nearest edge
final int offset = dpToPx(MOTION_NON_ADJACENT_OFFSET);
if (position < mSelectedPosition) {
// We're going end-to-start
if (isRtl) {
startLeft = startRight = targetLeft - offset;
} else {
startLeft = startRight = targetRight + offset;
}
} else {
// We're going start-to-end
if (isRtl) {
startLeft = startRight = targetRight + offset;
} else {
startLeft = startRight = targetLeft - offset;
}
}
}
// 開始移動位置
if (startLeft != targetLeft || startRight != targetRight) {
ValueAnimator animator = mIndicatorAnimator = new ValueAnimator();
animator.setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR);
animator.setDuration(duration);
animator.setFloatValues(0, 1);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animator) {
final float fraction = animator.getAnimatedFraction();
setIndicatorPosition(
AnimationUtils.lerp(startLeft, targetLeft, fraction),
AnimationUtils.lerp(startRight, targetRight, fraction));
}
});
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animator) {
mSelectedPosition = position;
mSelectionOffset = 0f;
}
});
animator.start();
}
}
onDraw 很簡單
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
// Thick colored underline below the current selection
if (mIndicatorLeft >= 0 && mIndicatorRight > mIndicatorLeft) {
canvas.drawRect(mIndicatorLeft, getHeight() - mSelectedIndicatorHeight,
mIndicatorRight, getHeight(), mSelectedIndicatorPaint);
}
}
TabView
接下來再看看上面的內容
class TabView extends LinearLayout {
private Tab mTab;
private TextView mTextView;
private ImageView mIconView;
private View mCustomView;
private TextView mCustomTextView;
private ImageView mCustomIconView;
private int mDefaultMaxLines = 2;
}
如何實現(xiàn)監(jiān)聽的,對每個 TabView 設置點擊事件,重寫了 performClick,其中調用 mTab.select,mTab 擁有 TabLayout 的引用,
@Override
public boolean performClick() {
final boolean handled = super.performClick();
if (mTab != null) {
if (!handled) {
playSoundEffect(SoundEffectConstants.CLICK);
}
mTab.select();
return true;
} else {
return handled;
}
}
/**
* Select this tab. Only valid if the tab has been added to the action bar.
*/
public void select() {
if (mParent == null) {
throw new IllegalArgumentException("Tab not attached to a TabLayout");
}
mParent.selectTab(this);
}
tabLayout 回調事件,
void selectTab(Tab tab) {
selectTab(tab, true);
}
void selectTab(final Tab tab, boolean updateIndicator) {
final Tab currentTab = mSelectedTab;
if (currentTab == tab) {
if (currentTab != null) {
dispatchTabReselected(tab);
animateToTab(tab.getPosition());
}
} else {
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;
if (tab != null) {
dispatchTabSelected(tab);
}
}
}
移動 Tab,動畫
private void animateToTab(int newPosition) {
if (newPosition == Tab.INVALID_POSITION) {
return;
}
if (getWindowToken() == null || !ViewCompat.isLaidOut(this)
|| mTabStrip.childrenNeedLayout()) {
// If we don't have a window token, or we haven't been laid out yet just dra
// position now
setScrollPosition(newPosition, 0f, true);
return;
}
final int startScrollX = getScrollX();
final int targetScrollX = calculateScrollXForTab(newPosition, 0);
if (startScrollX != targetScrollX) {
ensureScrollAnimator();
mScrollAnimator.setIntValues(startScrollX, targetScrollX);
mScrollAnimator.start();
}
// Now animate the indicator
mTabStrip.animateIndicatorToPosition(newPosition, ANIMATION_DURATION);
}
計算移動的距離,讓選中的 tab 位于中間位置,由于 Android ScrollView 默認不會滾動超出邊界,所以如果到達邊界也不會繼續(xù)滾動了
private int calculateScrollXForTab(int position, float positionOffset) {
if (mMode == MODE_SCROLLABLE) {
final View selectedChild = mTabStrip.getChildAt(position);
final View nextChild = position + 1 < mTabStrip.getChildCount()
? mTabStrip.getChildAt(position + 1)
: null;
final int selectedWidth = selectedChild != null ? selectedChild.getWidth() : 0;
final int nextWidth = nextChild != null ? nextChild.getWidth() : 0;
// base scroll amount: places center of tab in center of parent
int scrollBase = selectedChild.getLeft() + (selectedWidth / 2) - (getWidth() / 2);
// offset amount: fraction of the distance between centers of tabs
int scrollOffset = (int) ((selectedWidth + nextWidth) * 0.5f * positionOffset);
return (ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_LTR)
? scrollBase + scrollOffset
: scrollBase - scrollOffset;
}
return 0;
}
ViewPager
其實很簡單,就是給 ViewPager 添加一個 OnPageChangeListener 就行了,代碼也很簡單,在 onPageScrolled 中改變 指示條 的位置,在 onPageSelected 中改變 選中狀態(tài)
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;
}
}
小結
基本上看完了,但對于一些細節(jié),滾動邊界問題還沒有深刻的理解,只知道大概的邏輯