本文为转载&地址:项目地址: https://github.com/itgoyo/AndroidSource-Analysis
简介: Android源码分析,让你更清楚的理解每一个组件的功能与用法。
TabLayout 源码解析
1. 功能介绍
1.1 TabLayout
Tabs跟随Actionbar在Android 3.0进入大家的视线,是一个很经典的设计。它也是Material Design 规范中提及的Component
之一。Tabs or Bottom navigation?相信不少Android开发者与产品都撕过,就连微信在其中也有过抉择。Google在Google+以及Google Photo中相继采用Bottom navigation的设计把剧情推到向高潮,一度轰动整个社区。Google继而在Material Design 规范加入了Bottom navigation,表明了态度,也给这起争论画上了圆满的句号。
在 support desgin lib 发布前,大家基本都采用PagerSlidingTabStrip来实现tab效果。其实TabLayout
在实现上和PagerSlidingTabStrip
十分相似,今天我们来分析TabLayout
。
1.2 TabLayout使用
TabLayout
使用比较简单。既可以单独使用,也可以与ViewPager
配合使用。
1.2.1 TabLayout单独使用
在java代码中添加Tabs
1 2 3 4
| TabLayout tabLayout = (TabLayout) findViewById(R.id.tabLayout); tabLayout.addTab(tabLayout.newTab().setText("Tab 1")); tabLayout.addTab(tabLayout.newTab().setText("Tab 2")); tabLayout.addTab(tabLayout.newTab().setText("Tab 3"));
|
也可以在xml中添加Tabs
1 2 3 4 5 6 7 8 9 10 11
| <android.support.design.widget.TabLayout android:layout_height="wrap_content" android:layout_width="match_parent"> <android.support.design.widget.TabItem android:text="@string/tab_text"/> <android.support.design.widget.TabItem android:icon="@drawable/ic_android"/> </android.support.design.widget.TabLayout>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| TabLayout tabLayout = ...; ViewPager viewPager = ...; PagerAdapter adapter = new PagerAdapter(){ @Override public CharSequence getPageTitle(int position) { return "Tab 1"; } } viewPager.setAdapter(adapter); tabLayout.setupWithViewPager(viewPager);
|
2. 总体设计
TabLayout
继承HorizontalScrollView
天生就是一个可以横向滚动的ViewGroup
. 我们知道, HorizontalScrollView
与ScrollView
一样, 最多只能包含一个子View.
SlidingTabStrip
继承于LinearLayout
,是TabLayout
的内部类。它是TabLayout
唯一的子View. 所有的TabView
都是它的子View.
TabView
继承于LinearLayout
,以Tab
为数据源,来展示Tab的样式。最终用for循环被add进SlidingTabStrip
.
Tab
是一个简单的View Model实体类,控制TabView
的title, icon, custom layout id等属性。
TabItem
继承于View. 用于在layout xml中来描述Tab. 需要注意的是,它不会add到SlidingTabStrip
中去。它的作用是从xml中获取到text,icon,custom layout id等属性。TabLayout inflate到TabItem
并获取属性到装配到Tab
中,最终add到SlidingTabStrip
中的还是TabView
.
OnTabSelectedListener
是TabLayout中的内部接口,用于监听SlidingTabStrip
中子TabView
选中状态的改变。
Mode
是TabLayout滚动模式的描述,一共有两种状态。MODE_FIXED
不可滚动模式,以及MODE_SCROLLABLE
可以滚动模式。
Gravity
是TabView
在SlidingTabStrip
中layout方式的描述。分为:GRAVITY_FILL,GRAVITY_CENTER.
3. 详细设计
3.1 类关系图
3.2 分析
3.2.1 TabLayout子View唯一性保证
前面介绍TabLayout
继承于HorizontalScrollView
最多只能有1个子View. 但TabLayout
可以在layout中添加多个子View节点. 这是怎么回事呢?
1 2 3 4 5 6 7 8 9 10 11
| <android.support.design.widget.TabLayout android:layout_height="wrap_content" android:layout_width="match_parent"> <android.support.design.widget.TabItem android:text="@string/tab_text"/> <android.support.design.widget.TabItem android:icon="@drawable/ic_android"/> </android.support.design.widget.TabLayout>
|
看过LayoutInflater
源码的同学可能会知道这个过程:先inflate到生成View对象,再调用ViewGroup#addView(...)
系列方法把view添加到ViewGroup中。我们发现TabLayout的addView(...)
系列方法,都删去super调用,且调用了共同的一个方法,addViewInternal(View view)
。
1 2 3 4 5 6 7
| private void addViewInternal(final View child) { if (child instanceof TabItem) { addTabFromItemView((TabItem) child); } else { throw new IllegalArgumentException("Only TabItem instances can be added to TabLayout"); } }
|
可见,若child非TabItem
对象会抛出异常。所以xml中给TabLayout添加tab时,只能添加TabItem
对象。若想添加其它View类型怎么办?TabItem有android:customView
这个属性。我们继续来看。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| private void addTabFromItemView(@NonNull TabItem item) { final Tab tab = newTab(); if (item.mText != null) { tab.setText(item.mText); } if (item.mIcon != null) { tab.setIcon(item.mIcon); } if (item.mCustomLayout != 0) { tab.setCustomView(item.mCustomLayout); } addTab(tab); } public Tab newTab() { Tab tab = sTabPool.acquire(); if (tab == null) { tab = new Tab(); } tab.mParent = this; tab.mView = createTabView(tab); return tab; } 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; }
|
这里调newTab()
方法创建了一个tab对象,并且用对象池把创建的tab对象缓存起来。然后将TabItem
对象的属性都赋值给tab对象。在createTabView(Tab tab)
这个方法中,首先从TabView
池中获取TabView
对象,如果不存在,则实例化一个对象,并调用tabView.setTab(tab)
方法来进行了数据绑定。 addTab(...)
有三个重载方法,最终都会调用如下方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| public void addTab(@NonNull Tab tab, boolean setSelected) { if (tab.mParent != this) { throw new IllegalArgumentException("Tab belongs to a different TabLayout."); } addTabView(tab, setSelected); configureTab(tab, mTabs.size()); if (setSelected) { tab.select(); } } private void addTabView(Tab tab, int position, boolean setSelected) { final TabView tabView = tab.mView; mTabStrip.addView(tabView, position, createLayoutParamsForTabs()); if (setSelected) { tabView.setSelected(true); } } 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); } }
|
在addView(Tab, int, boolean)
方法中,把TabView
对象add进了SlidingTabStrip
这个ViewGroup
中。实际上SlidingTabStrip
的对象mTabStrip
才是TabLayout
的唯一子View.在TabLayout
的构造方法中:
1 2 3 4 5 6 7 8 9 10 11
| public TabLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); setHorizontalScrollBarEnabled(false); mTabStrip = new SlidingTabStrip(context); super.addView(mTabStrip, 0, new HorizontalScrollView.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT)); }
|
至此,我们就明白了TabLayout
中子View的一致性是如何保证的。也明白了TabView
其实才是亲生的,TabItem
其实是后娘养的! 这些代码都很简单,不过我们可以从中学习到很多有用的思想。
至此,一个清晰的View
层级图应该就出现在了各位同学的眼前。
有了上面的的基础,我们再来看看TabLayout
是如何和它的好基友ViewPager
搭配使用的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| public void setupWithViewPager(@Nullable final ViewPager viewPager) { mViewPager = viewPager; if (mPageChangeListener == null) { mPageChangeListener = new TabLayoutOnPageChangeListener(this); } mPageChangeListener.reset(); viewPager.addOnPageChangeListener(mPageChangeListener); setOnTabSelectedListener(new ViewPagerOnTabSelectedListener(viewPager)); setPagerAdapter(adapter, true); } public void setOnTabSelectedListener(OnTabSelectedListener onTabSelectedListener) { mOnTabSelectedListener = onTabSelectedListener; } private 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(); }
|
这里的TabLayoutOnPageChangeListener
实现了ViewPager.OnPageChangeListener
. 首先调用ViewPager
对象addOnPageChangeListener(OnPageChangeListener)
来监听ViewPager
的滑动以及当前也的选中。然后设置ViewPagerOnTabSelectedListener
对象,保证ViewPager的页面和TabLayout的item的选中状态保持一致,以及滚动的协同性。这里的监听在3.2.3中详细讲解。
我们一般调用viewPager.getAdapter().notifyDataSetChanged()
来进行ViewPager的刷新. 现在我们在ViewPager的adapter中注册一个监听器,监听ViewPager
的刷新行为。目的是为了刷新ViewPager
的同时也可以刷新TabLayout. 我们来看看PagerAdapterObserver
这个监听器是如何刷新TabLayout的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| private class PagerAdapterObserver extends DataSetObserver { @Override public void onChanged() { populateFromPagerAdapter(); } @Override public void onInvalidated() { populateFromPagerAdapter(); } } private void populateFromPagerAdapter() { removeAllTabs(); if (mPagerAdapter != null) { final int adapterCount = mPagerAdapter.getCount(); for (int i = 0; i < adapterCount; i++) { addTab(newTab().setText(mPagerAdapter.getPageTitle(i)), false); } if (mViewPager != null && adapterCount > 0) { final int curItem = mViewPager.getCurrentItem(); if (curItem != getSelectedTabPosition() && curItem < getTabCount()) { selectTab(getTabAt(curItem)); } } } else { removeAllTabs(); } } public void removeAllTabs() { for (int i = mTabStrip.getChildCount() - 1; i >= 0; i--) { removeTabViewAt(i); } for (final Iterator<Tab> i = mTabs.iterator(); i.hasNext();) { final Tab tab = i.next(); i.remove(); tab.reset(); sTabPool.release(tab); } mSelectedTab = null; }
|
刷新方式很简单粗暴,从SlidingTabStrip
对象中移除所有的TabView
,继而从View ModelmTabs
中移除所有Tab
对象。然后从adapter中获取tab信息,循环调用addTab(Tab, boolean)
方法重新添加TabView
。最后调用ViewPager
对象的getCurrentItem()
方法,获取当前位置,然后调用selectTab(int position)恢复TabView
的选中状态(针对TabView的选中,3.2.4中有详细介绍)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| 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(int state) { mPreviousScrollState = mScrollState; mScrollState = state; } @Override public void onPageScrolled(int position, float positionOffset, 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(int position) { final TabLayout tabLayout = mTabLayoutRef.get(); if (tabLayout != null && tabLayout.getSelectedTabPosition() != position) { final boolean updateIndicator = mScrollState == SCROLL_STATE_IDLE || (mScrollState == SCROLL_STATE_SETTLING && mPreviousScrollState == SCROLL_STATE_IDLE); tabLayout.selectTab(tabLayout.getTabAt(position), updateIndicator); } } private void reset() { mPreviousScrollState = mScrollState = SCROLL_STATE_IDLE; } }
|
用过ViewPager
的同学对OnPageChangeListener
不会陌生,不多赘述。TabLayoutOnPageChangeListener
实现了OnPageChangeListener
, 在onPageScrolled(...)
方法中做协同滚动处理。滚动的条件是:
1
| final boolean updateIndicator = !(mScrollState == SCROLL_STATE_SETTLING && mPreviousScrollState == SCROLL_STATE_IDLE);
|
调用TabLayout的setScrollPosition(...)
方法来控制TabLayout
中TabView
和indocator的协同滚动。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| private void setScrollPosition(int position, float positionOffset, boolean updateSelectedText, boolean updateIndicatorPosition) { final int roundedPosition = Math.round(position + positionOffset); if (roundedPosition < 0 || roundedPosition >= mTabStrip.getChildCount()) { return; } if (updateIndicatorPosition) { mTabStrip.setIndicatorPositionFromTabPosition(position, positionOffset); } if (mScrollAnimator != null && mScrollAnimator.isRunning()) { mScrollAnimator.cancel(); } scrollTo(calculateScrollXForTab(position, positionOffset), 0); if (updateSelectedText) { setSelectedTabView(roundedPosition); } }
|
3.2.3.1 TabLayout的Indicator协同滚动
indicator的滚动由SlidingTabStrip来处理:
1 2 3 4
| if (updateIndicatorPosition) { mTabStrip.setIndicatorPositionFromTabPosition(position, positionOffset); }
|
这里的position
是当前选中的位置。positionOffset
是: 距当前Tab滑动的距离
/从当前tab滑动到下一个tab的总距离
这样一个范围在[0,1]间的小数。
SlidingTabStrip#setIndicatorPositionFromTabPosition(int, float)
1 2 3 4 5 6 7 8 9
| void setIndicatorPositionFromTabPosition(int position, float positionOffset) { if (mIndicatorAnimator != null && mIndicatorAnimator.isRunning()) { mIndicatorAnimator.cancel(); } mSelectedPosition = position; mSelectionOffset = positionOffset; updateIndicatorPosition(); }
|
SlidingTabStrip#updateIndicatorPosition()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| 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); }
|
通过getChildAt(mSelectedPosition)
, 获取到到mSelectedPosition
处的TabView。若滑动的mSelectionOffset>0f
且当前选中的位置mSelectedPosition
不是最后一个TabView. 获取到下一个TabView,并计算出indicator的left和right。
SlidingTabStrip#setIndicatorPosition(int, int)
1 2 3 4 5 6 7 8
| private void setIndicatorPosition(int left, int right) { if (left != mIndicatorLeft || right != mIndicatorRight) { mIndicatorLeft = left; mIndicatorRight = right; ViewCompat.postInvalidateOnAnimation(this); } }
|
非常简单的代码,在调用ViewCompat.postInvalidateOnAnimation(this)
重绘View之前,去掉一些重复绘制的帧。
1 2 3 4 5 6 7 8 9 10
| @Override public void draw(Canvas canvas) { super.draw(canvas); if (mIndicatorLeft >= 0 && mIndicatorRight > mIndicatorLeft) { canvas.drawRect(mIndicatorLeft, getHeight() - mSelectedIndicatorHeight, mIndicatorRight, getHeight(), mSelectedIndicatorPaint); } }
|
绘制逻辑很简单。调用canvas.drawRect(float left, float top, float right, float bottom, Paint paint)
来绘制indicator.这里:
1 2 3 4
| left = mIndicatorLeft; top = getHeight() - mSelectedIndicatorHeight; right = mIndicatorRight; bottom = getHeight();
|
3.2.3.2 TabLayout的TabView协同滚动
我们回头来看 3.2.3中setScrollPosition(...)
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| private void setScrollPosition(int position, float positionOffset, boolean updateSelectedText, boolean updateIndicatorPosition) { final int roundedPosition = Math.round(position + positionOffset); if (roundedPosition < 0 || roundedPosition >= mTabStrip.getChildCount()) { return; } if (updateIndicatorPosition) { mTabStrip.setIndicatorPositionFromTabPosition(position, positionOffset); } if (mScrollAnimator != null && mScrollAnimator.isRunning()) { mScrollAnimator.cancel(); } scrollTo(calculateScrollXForTab(position, positionOffset), 0); if (updateSelectedText) { setSelectedTabView(roundedPosition); } }
|
在3.2.3.1中我们知道indicator的滚动是通过mTabStrip.setIndicatorPositionFromTabPosition(position, positionOffset)
实现的。那TabView的滚动呢?我们知道TabLayout
是继承HorizonScrollView
天生就是一个可以横行滚动的View
,所以,我们只需要调用scrollTo(int x, int y)
方法就可以实现横向滚动。
1
| scrollTo(calculateScrollXForTab(position, positionOffset), 0);
|
这里x方向的偏移量调用calculateScrollXForTab(position, positionOffset)
实时计算得出,y方向的偏移量为0。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 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; return selectedChild.getLeft() + ((int) ((selectedWidth + nextWidth) * positionOffset * 0.5f)) + (selectedChild.getWidth() / 2) - (getWidth() / 2); } return 0; }
|
至此,我们就明白了TabLayout
是如何随ViewPager
的滚动而滚动的。
3.2.4 Tab选中状态
1 2 3 4 5 6 7 8 9
| private void setSelectedTabView(int position) { final int tabCount = mTabStrip.getChildCount(); if (position < tabCount && !mTabStrip.getChildAt(position).isSelected()) { for (int i = 0; i < tabCount; i++) { final View child = mTabStrip.getChildAt(i); child.setSelected(i == position); } } }
|
调用View的setSelected(boolean)
方法。
4. 开源项目中的使用
开源项目中使用TabLayout的例子特别多, 这里给出我写的一个项目:
发现文章,代码有误、对内容有疑问,请在Issue上面提单,技术讨论可以给我Email,欢迎关注左边微信公众号或加QQ群获取更多信息。
Last updated:
扫一扫,分享到微信
{title}