史上最全的TabLayout源码分析
TabLayout在Android开发中十分重要,其关键元素如下所示:
Tab类和TabView类和SlidingTabStrip类为TabLayout提供了三个基本的元素。
对于Tab类:还有一个TagView的mView成员变量,可以通过setIcon()和setCustomView()方法设置tab的mView参数,
public static final class Tab { public static final int INVALID_POSITION = -1; private Object mTag; private Drawable mIcon; private CharSequence mText; private CharSequence mContentDesc; private int mPosition = INVALID_POSITION; private View mCustomView; TabLayout mParent; TabView mView; Tab() { } @Nullable public Object getTag() { return mTag; } @NonNull public Tab setTag(@Nullable Object tag) { mTag = tag; return this; } @Nullable public View getCustomView() { return mCustomView; } @NonNull public Tab setCustomView(@Nullable View view) { mCustomView = view; updateView();//更新TabView里的undateView()函数 return this; } @NonNull public Tab setCustomView(@LayoutRes int resId) { final LayoutInflater inflater = LayoutInflater.from(mView.getContext()); return setCustomView(inflater.inflate(resId, mView, false)); } @Nullable public Drawable getIcon() { return mIcon; } public int getPosition() { return mPosition; } void setPosition(int position) { mPosition = position; } @Nullable public CharSequence getText() { return mText; } @NonNull public Tab setIcon(@Nullable Drawable icon) { mIcon = icon; updateView(); return this; } @NonNull public Tab setIcon(@DrawableRes int resId) { if (mParent == null) { throw new IllegalArgumentException("Tab not attached to a TabLayout"); } return setIcon(AppCompatResources.getDrawable(mParent.getContext(), resId)); } @NonNull public Tab setText(@Nullable CharSequence text) { mText = text; updateView(); return this; } @NonNull public Tab setText(@StringRes int resId) { if (mParent == null) { throw new IllegalArgumentException("Tab not attached to a TabLayout"); } return setText(mParent.getResources().getText(resId)); } public void select() { if (mParent == null) { throw new IllegalArgumentException("Tab not attached to a TabLayout"); } mParent.selectTab(this); } public boolean isSelected() { if (mParent == null) { throw new IllegalArgumentException("Tab not attached to a TabLayout"); } return mParent.getSelectedTabPosition() == mPosition; } @NonNull public Tab setContentDescription(@StringRes int resId) { if (mParent == null) { throw new IllegalArgumentException("Tab not attached to a TabLayout"); } return setContentDescription(mParent.getResources().getText(resId)); } @NonNull public Tab setContentDescription(@Nullable CharSequence contentDesc) { mContentDesc = contentDesc; updateView(); return this; } @Nullable public CharSequence getContentDescription() { return mContentDesc; } void updateView() { if (mView != null) { mView.update();//这里调用TabView的update()函数 } } void reset() { mParent = null; mView = null; mTag = null; mIcon = null; mText = null; mContentDesc = null; mPosition = INVALID_POSITION; mCustomView = null; } }
接下来看下负责view更新的TabView类:其中通过createTabView建立Tab和TabView直接的联系(如下)。
private TabView createTabView(@NonNull final Tab tab) { TabView tabView = mTabViewPool != null ? mTabViewPool.acquire() : null; if (tabView == null) { tabView = new TabView(getContext()); } tabView.setTab(tab); tabView.setFocusable(true); tabView.setMinimumWidth(getTabMinWidth()); return tabView; }
TabView类如下:其中updateTextAndIcon负责是否更新TextView和IconView的重绘操作
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; public TabView(Context context) { super(context); if (mTabBackgroundResId != 0) { ViewCompat.setBackground( this, AppCompatResources.getDrawable(context, mTabBackgroundResId)); } ViewCompat.setPaddingRelative(this, mTabPaddingStart, mTabPaddingTop, mTabPaddingEnd, mTabPaddingBottom); setGravity(Gravity.CENTER); setOrientation(VERTICAL); setClickable(true); ViewCompat.setPointerIcon(this, PointerIconCompat.getSystemIcon(getContext(), PointerIconCompat.TYPE_HAND)); } @Override public boolean performClick() { final boolean handled = super.performClick(); if (mTab != null) { if (!handled) { playSoundEffect(SoundEffectConstants.CLICK); } mTab.select(); return true; } else { return handled; } } @Override public void setSelected(final boolean selected) { final boolean changed = isSelected() != selected; super.setSelected(selected); if (changed && selected && Build.VERSION.SDK_INT < 16) { sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); } if (mTextView != null) { mTextView.setSelected(selected); } if (mIconView != null) { mIconView.setSelected(selected); } if (mCustomView != null) { mCustomView.setSelected(selected); } } @Override public void onInitializeAccessibilityEvent(AccessibilityEvent event) { super.onInitializeAccessibilityEvent(event); event.setClassName(ActionBar.Tab.class.getName()); } @Override public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(info); info.setClassName(ActionBar.Tab.class.getName()); } @Override public void onMeasure(final int origWidthMeasureSpec, final int origHeightMeasureSpec) { final int specWidthSize = MeasureSpec.getSize(origWidthMeasureSpec); final int specWidthMode = MeasureSpec.getMode(origWidthMeasureSpec); final int maxWidth = getTabMaxWidth(); final int widthMeasureSpec; final int heightMeasureSpec = origHeightMeasureSpec; if (maxWidth > 0 && (specWidthMode == MeasureSpec.UNSPECIFIED || specWidthSize > maxWidth)) { widthMeasureSpec = MeasureSpec.makeMeasureSpec(mTabMaxWidth, MeasureSpec.AT_MOST); } else { widthMeasureSpec = origWidthMeasureSpec; } super.onMeasure(widthMeasureSpec, heightMeasureSpec); if (mTextView != null) { final Resources res = getResources(); float textSize = mTabTextSize; int maxLines = mDefaultMaxLines; if (mIconView != null && mIconView.getVisibility() == VISIBLE) { maxLines = 1; } else if (mTextView != null && mTextView.getLineCount() > 1) { textSize = mTabTextMultiLineSize; } final float curTextSize = mTextView.getTextSize(); final int curLineCount = mTextView.getLineCount(); final int curMaxLines = TextViewCompat.getMaxLines(mTextView); if (textSize != curTextSize || (curMaxLines >= 0 && maxLines != curMaxLines)) { boolean updateTextView = true; if (mMode == MODE_FIXED && textSize > curTextSize && curLineCount == 1) { final Layout layout = mTextView.getLayout(); if (layout == null || approximateLineWidth(layout, 0, textSize) > getMeasuredWidth() - getPaddingLeft() - getPaddingRight()) { updateTextView = false; } } if (updateTextView) { mTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize); mTextView.setMaxLines(maxLines); super.onMeasure(widthMeasureSpec, heightMeasureSpec); } } } } void setTab(@Nullable final Tab tab) {//设置Tab联系 if (tab != mTab) { mTab = tab; update(); } } void reset() { setTab(null); setSelected(false); } final void update() { final Tab tab = mTab; final View custom = tab != null ? tab.getCustomView() : null;//先看getCustomView中有无View if (custom != null) { final ViewParent customParent = custom.getParent(); if (customParent != this) { if (customParent != null) { ((ViewGroup) customParent).removeView(custom); } addView(custom); } mCustomView = custom; if (mTextView != null) { mTextView.setVisibility(GONE); } if (mIconView != null) { mIconView.setVisibility(GONE); mIconView.setImageDrawable(null); } mCustomTextView = (TextView) custom.findViewById(android.R.id.text1); if (mCustomTextView != null) { mDefaultMaxLines = TextViewCompat.getMaxLines(mCustomTextView); } mCustomIconView = (ImageView) custom.findViewById(android.R.id.icon); } else { if (mCustomView != null) { removeView(mCustomView); mCustomView = null; } mCustomTextView = null; mCustomIconView = null; } if (mCustomView == null) {//如果mCustomView没有,则通过mIconView更新 if (mIconView == null) { ImageView iconView = (ImageView) LayoutInflater.from(getContext()) .inflate(R.layout.design_layout_tab_icon, this, false); addView(iconView, 0); mIconView = iconView; } if (mTextView == null) { TextView textView = (TextView) LayoutInflater.from(getContext()) .inflate(R.layout.design_layout_tab_text, this, false); addView(textView); mTextView = textView; mDefaultMaxLines = TextViewCompat.getMaxLines(mTextView); } TextViewCompat.setTextAppearance(mTextView, mTabTextAppearance); if (mTabTextColors != null) { mTextView.setTextColor(mTabTextColors); } updateTextAndIcon(mTextView, mIconView); } else { if (mCustomTextView != null || mCustomIconView != null) { updateTextAndIcon(mCustomTextView, mCustomIconView); } } setSelected(tab != null && tab.isSelected()); } private void updateTextAndIcon(@Nullable final TextView textView, @Nullable final ImageView iconView) { final Drawable icon = mTab != null ? mTab.getIcon() : null; final CharSequence text = mTab != null ? mTab.getText() : null; final CharSequence contentDesc = mTab != null ? mTab.getContentDescription() : null; if (iconView != null) { if (icon != null) { iconView.setImageDrawable(icon); iconView.setVisibility(VISIBLE); setVisibility(VISIBLE); } else { iconView.setVisibility(GONE); iconView.setImageDrawable(null); } iconView.setContentDescription(contentDesc); } final boolean hasText = !TextUtils.isEmpty(text); if (textView != null) { if (hasText) { textView.setText(text); textView.setVisibility(VISIBLE); setVisibility(VISIBLE); } else { textView.setVisibility(GONE); textView.setText(null); } textView.setContentDescription(contentDesc); } if (iconView != null) { MarginLayout Params lp = ((MarginLayoutParams) iconView.getLayoutParams()); int bottomMargin = 0; if (hasText && iconView.getVisibility() == VISIBLE) { bottomMargin = dpToPx(DEFAULT_GAP_TEXT_ICON); } if (bottomMargin != lp.bottomMargin) { lp.bottomMargin = bottomMargin; iconView.requestLayout();//判断图片是否要更新 } } TooltipCompat.setTooltipText(this, hasText ? null : contentDesc); } public Tab getTab() { return mTab; } private float approximateLineWidth(Layout layout, int line, float textSize) { return layout.getLineWidth(line) * (textSize / layout.getPaint().getTextSize()); } }当TabLayout开始执行时,先是设置一些默认参数,然后定义两种Tab模式
TabLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr); ThemeUtils.checkAppCompatTheme(context); setHorizontalScrollBarEnabled(false);// 使Scroll Bar不可用 mTabStrip = new SlidingTabStrip(context); //添加Tab下滑线 super.addView(mTabStrip, 0, new HorizontalScrollView.LayoutParams( LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT)); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TabLayout, defStyleAttr, R.style.Widget_Design_TabLayout); mTabStrip.setSelectedIndicatorHeight( a.getDimensionPixelSize(R.styleable.TabLayout_tabIndicatorHeight, 0)); mTabStrip.setSelectedIndicatorColor(a.getColor(R.styleable.TabLayout_tabIndicatorColor, 0)); mTabPaddingStart = mTabPaddingTop = mTabPaddingEnd = mTabPaddingBottom = a .getDimensionPixelSize(R.styleable.TabLayout_tabPadding, 0); mTabPaddingStart = a.getDimensionPixelSize(R.styleable.TabLayout_tabPaddingStart, mTabPaddingStart); mTabPaddingTop = a.getDimensionPixelSize(R.styleable.TabLayout_tabPaddingTop, mTabPaddingTop); mTabPaddingEnd = a.getDimensionPixelSize(R.styleable.TabLayout_tabPaddingEnd, mTabPaddingEnd); mTabPaddingBottom = a.getDimensionPixelSize(R.styleable.TabLayout_tabPaddingBottom, mTabPaddingBottom); mTabTextAppearance = a.getResourceId(R.styleable.TabLayout_tabTextAppearance, R.style.TextAppearance_Design_Tab); final TypedArray ta = context.obtainStyledAttributes(mTabTextAppearance, android.support.v7.appcompat.R.styleable.TextAppearance); try { mTabTextSize = ta.getDimensionPixelSize( android.support.v7.appcompat.R.styleable.TextAppearance_android_textSize, 0); mTabTextColors = ta.getColorStateList( android.support.v7.appcompat.R.styleable.TextAppearance_android_textColor); } finally { ta.recycle(); } if (a.hasValue(R.styleable.TabLayout_tabTextColor)) { mTabTextColors = a.getColorStateList(R.styleable.TabLayout_tabTextColor); } if (a.hasValue(R.styleable.TabLayout_tabSelectedTextColor)) { final int selected = a.getColor(R.styleable.TabLayout_tabSelectedTextColor, 0); mTabTextColors = createColorStateList(mTabTextColors.getDefaultColor(), selected); } mRequestedTabMinWidth = a.getDimensionPixelSize(R.styleable.TabLayout_tabMinWidth, INVALID_WIDTH); mRequestedTabMaxWidth = a.getDimensionPixelSize(R.styleable.TabLayout_tabMaxWidth, INVALID_WIDTH); mTabBackgroundResId = a.getResourceId(R.styleable.TabLayout_tabBackground, 0); mContentInsetStart = a.getDimensionPixelSize(R.styleable.TabLayout_tabContentStart, 0); mMode = a.getInt(R.styleable.TabLayout_tabMode, MODE_FIXED); mTabGravity = a.getInt(R.styleable.TabLayout_tabGravity, GRAVITY_FILL); a.recycle(); final Resources res = getResources(); mTabTextMultiLineSize = res.getDimensionPixelSize(R.dimen.design_tab_text_size_2line); mScrollableTabMinWidth = res.getDimensionPixelSize(R.dimen.design_tab_scrollable_min_width); applyModeAndGravity();// 加载tab mode and gravity }
接下来是applyModeAndGravity()方法
private void applyModeAndGravity() { int paddingStart = 0; if (mMode == MODE_SCROLLABLE) { paddingStart = Math.max(0, mContentInsetStart - mTabPaddingStart); } ViewCompat.setPaddingRelative(mTabStrip, paddingStart, 0, 0, 0); switch (mMode) { case MODE_FIXED: mTabStrip.setGravity(Gravity.CENTER_HORIZONTAL); break; case MODE_SCROLLABLE: mTabStrip.setGravity(GravityCompat.START); break; //根据模式设置Gravity方式 } updateTabViews(true); }
调用updateTabViews方法
void updateTabViews(final boolean requestLayout) { for (int i = 0; i < mTabStrip.getChildCount(); i++) { View child = mTabStrip.getChildAt(i); child.setMinimumWidth(getTabMinWidth()); updateTabViewLayoutParams((LinearLayout.LayoutParams) child.getLayoutParams()); if (requestLayout) { child.requestLayout(); } } }
默认情况下mTabStrip是没有东西的,得通过布局文件或者动态加载的方式为其添加内容。
如tabLayout.addTab(tabLayout.newTab()),该方法调用addTab函数
public void addTab(@NonNull Tab tab) {//其实是对mTabStrip的操作 addTab(tab, mTabs.isEmpty()); } public void addTab(@NonNull Tab tab, int position) { addTab(tab, position, mTabs.isEmpty()); } public void addTab(@NonNull Tab tab, boolean setSelected) { addTab(tab, mTabs.size(), setSelected); } 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(); } }
先调用configureTab(Tab tab, int position)函数:
private void configureTab(Tab tab, int position) { tab.setPosition(position); mTabs.add(position, tab); final int count = mTabs.size(); for (int i = position + 1; i < count; i++) { mTabs.get(i).setPosition(i); } }
然后调用addTabView(tab)函数:
private void addTabView(Tab tab) { final TabView tabView = tab.mView;//这个在Tab内部类中mView初始化时定义的 mTabStrip.addView(tabView, tab.getPosition(), createLayoutParamsForTabs());//这里执行的mTabStrip同步加载对于Tabview的操作 }
当用户执行setupWithViewPager是便到了最关键的步骤,其做的工作是和ViewPager建立关系,
将PagerAdapter适配到tabLayout。
private void setupWithViewPager(@Nullable final ViewPager viewPager, boolean autoRefresh, boolean implicitSetup) { if (mViewPager != null) { if (mPageChangeListener != null) { mViewPager.removeOnPageChangeListener(mPageChangeListener);//有mPageChangeListener监听,删除监听 } if (mAdapterChangeListener != null) { mViewPager.removeOnAdapterChangeListener(mAdapterChangeListener);//有适配改变监听,删除 } } if (mCurrentVpSelectedListener != null) { removeOnTabSelectedListener(mCurrentVpSelectedListener); //有tab改变监听,删除 mCurrentVpSelectedListener = null; } if (viewPager != null) { mViewPager = viewPager; if (mPageChangeListener == null) { mPageChangeListener = new TabLayoutOnPageChangeListener(this); } mPageChangeListener.reset(); viewPager.addOnPageChangeListener(mPageChangeListener);//加入viewPager改变监听 mCurrentVpSelectedListener = new ViewPagerOnTabSelectedListener(viewPager); addOnTabSelectedListener(mCurrentVpSelectedListener); //加入tab改变监听 final PagerAdapter adapter = viewPager.getAdapter(); if (adapter != null) { setPagerAdapter(adapter, autoRefresh); } if (mAdapterChangeListener == null) { mAdapterChangeListener = new AdapterChangeListener(); } mAdapterChangeListener.setAutoRefresh(autoRefresh); viewPager.addOnAdapterChangeListener(mAdapterChangeListener); //加入适配改变监听 setScrollPosition(viewPager.getCurrentItem(), 0f, true); } else { ViewPager = null; setPagerAdapter(null, false); } mSetupViewPagerImplicitly = implicitSetup; }
上述方法中setPagerAdapter()注册一个观察者,观察adapter的变化,然后根据变化映射到tab,
是tab也发生相应变化。
void setPagerAdapter(@Nullable final PagerAdapter adapter, final boolean addObserver) { if (mPagerAdapter != null && mPagerAdapterObserver != null) { mPagerAdapter.unregisterDataSetObserver(mPagerAdapterObserver); } mPagerAdapter = adapter; if (addObserver && adapter != null) { if (mPagerAdapterObserver == null) { mPagerAdapterObserver = new PagerAdapterObserver(); } adapter.registerDataSetObserver(mPagerAdapterObserver); } populateFromPagerAdapter(); }
PopulateFromPagerAdapter()函数负责映射到了这个新的adapter。
void populateFromPagerAdapter() {
removeAllTabs();//清除所有的tab,所以你如果在设置适配器之前addTab就是多余的了
if (mPagerAdapter != null) { final int adapterCount = mPagerAdapter.getCount(); for (int i = 0; i < adapterCount; i++) { addTab(newTab().setText(mPagerAdapter.getPageTitle(i)), false);//重新添加tab通过PagerAdapter的getPageTitle方法设置 } if (mViewPager != null && adapterCount > 0) { final int curItem = mViewPager.getCurrentItem(); if (curItem != getSelectedTabPosition() && curItem < getTabCount()) { selectTab(getTabAt(curItem)); } } } }
主要的selectTab函数操作如下:
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) { 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切换的时候,我们需要切换页面的内容,
这时候就需要为它设置一个监听器TabLayout.OnTabSelectedListener。
public static class ViewPagerOnTabSelectedListener implements TabLayout.OnTabSelectedListener { private final ViewPager mViewPager; public ViewPagerOnTabSelectedListener(ViewPager viewPager) { mViewPager = viewPager; } @Override public void onTabSelected(TabLayout.Tab tab) { mViewPager.setCurrentItem(tab.getPosition());//设置更新的页面 } @Override public void onTabUnselected(TabLayout.Tab tab) { // No-op } @Override public void onTabReselected(TabLayout.Tab tab) { // No-op } }
TabLayoutOnPageChangeListener负责对page改变的监听
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) { final boolean updateText = mScrollState != SCROLL_STATE_SETTLING || mPreviousScrollState == SCROLL_STATE_DRAGGING; 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()) { final boolean updateIndicator = mScrollState == SCROLL_STATE_IDLE || (mScrollState == SCROLL_STATE_SETTLING && mPreviousScrollState == SCROLL_STATE_IDLE); tabLayout.selectTab(tabLayout.getTabAt(position), updateIndicator); } }
根据前面讨论的,SlidingTabStrip实现了tab的真正绘制实现操作,
下面看下这个类(主要执行onMeasure()->onLayout()->onDraw()的过程):
private class SlidingTabStrip extends LinearLayout { private int mSelectedIndicatorHeight; private final Paint mSelectedIndicatorPaint; int mSelectedPosition = -1; float mSelectionOffset; private int mLayoutDirection = -1; private int mIndicatorLeft = -1; private int mIndicatorRight = -1; private ValueAnimator mIndicatorAnimator; SlidingTabStrip(Context context) { super(context); setWillNotDraw(false);//重写draw()不起作用时,需要添加这么一句 mSelectedIndicatorPaint = new Paint(); } void setSelectedIndicatorColor(int color) { if (mSelectedIndicatorPaint.getColor() != color) { mSelectedIndicatorPaint.setColor(color); ViewCompat.postInvalidateOnAnimation(this); } } void setSelectedIndicatorHeight(int height) { if (mSelectedIndicatorHeight != height) { mSelectedIndicatorHeight = height; ViewCompat.postInvalidateOnAnimation(this); } } boolean childrenNeedLayout() { for (int i = 0, z = getChildCount(); i < z; i++) { final View child = getChildAt(i); if (child.getWidth() <= 0) { return true; } } return false; } void setIndicatorPositionFromTabPosition(int position, float positionOffset) { if (mIndicatorAnimator != null && mIndicatorAnimator.isRunning()) { mIndicatorAnimator.cancel(); } mSelectedPosition = position; mSelectionOffset = positionOffset; updateIndicatorPosition(); } float getIndicatorPosition() { return mSelectedPosition + mSelectionOffset; } @Override public void onRtlPropertiesChanged(int layoutDirection) { super.onRtlPropertiesChanged(layoutDirection); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { if (mLayoutDirection != layoutDirection) { requestLayout(); mLayoutDirection = layoutDirection; } } } @Override protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); if (MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY) { return; } if (mMode == MODE_FIXED && mTabGravity == GRAVITY_CENTER) { final int count = getChildCount(); 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) { return; } final int gutter = dpToPx(FIXED_WRAP_GUTTER_MIN); boolean remeasure = false; if (largestTabWidth * count <= getMeasuredWidth() - gutter * 2) { 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 { mTabGravity = GRAVITY_FILL; updateTabViews(false); remeasure = true; } if (remeasure) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); } } } @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()) { mIndicatorAnimator.cancel(); final long duration = mIndicatorAnimator.getDuration(); animateIndicatorToPosition(mSelectedPosition, Math.round((1f - mIndicatorAnimator.getAnimatedFraction()) * duration)); } else { updateIndicatorPosition(); } } 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) { 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); } void setIndicatorPosition(int left, int right) { if (left != mIndicatorLeft || right != mIndicatorRight) { mIndicatorLeft = left; mIndicatorRight = right; ViewCompat.postInvalidateOnAnimation(this); } } 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) { updateIndicatorPosition(); return; } final int targetLeft = targetView.getLeft(); final int targetRight = targetView.getRight(); final int startLeft; final int startRight; if (Math.abs(position - mSelectedPosition) <= 1) { startLeft = mIndicatorLeft; startRight = mIndicatorRight; } else { final int offset = dpToPx(MOTION_NON_ADJACENT_OFFSET); if (position < mSelectedPosition) { if (isRtl) { startLeft = startRight = targetLeft - offset; } else { startLeft = startRight = targetRight + offset; } } else { 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(); } } @Override public void draw(Canvas canvas) { super.draw(canvas); if (mIndicatorLeft >= 0 && mIndicatorRight > mIndicatorLeft) { canvas.drawRect(mIndicatorLeft, getHeight() - mSelectedIndicatorHeight, mIndicatorRight, getHeight(), mSelectedIndicatorPaint); } } }