- CompoundButton 源码分析
- LinearLayout 源码分析
- SearchView 源码解析
- LruCache 源码解析
- ViewDragHelper 源码解析
- BottomSheets 源码解析
- Media Player 源码分析
- NavigationView 源码解析
- Service 源码解析
- Binder 源码分析
- Android 应用 Preference 相关及源码浅析 SharePreferences 篇
- ScrollView 源码解析
- Handler 源码解析
- NestedScrollView 源码解析
- SQLiteOpenHelper/SQLiteDatabase/Cursor 源码解析
- Bundle 源码解析
- LocalBroadcastManager 源码解析
- Toast 源码解析
- TextInputLayout
- LayoutInflater 和 LayoutInflaterCompat 源码解析
- TextView 源码解析
- NestedScrolling 事件机制源码解析
- ViewGroup 源码解析
- StaticLayout 源码分析
- AtomicFile 源码解析
- AtomicFile 源码解析
- Spannable 源码分析
- Notification 之 Android 5.0 实现原理
- CoordinatorLayout 源码分析
- Scroller 源码解析
- SwipeRefreshLayout 源码分析
- FloatingActionButton 源码解析
- AsyncTask 源码分析
- TabLayout 源码解析
3.2 分析
3.2.1 TabLayout 子 View 唯一性保证
前面介绍 TabLayout
继承于 HorizontalScrollView
最多只能有 1 个子 View. 但 TabLayout
可以在 layout 中添加多个子 View 节点. 这是怎么回事呢?
<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)
。
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
这个属性。我们继续来看。
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(...)
有三个重载方法,最终都会调用如下方法:
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
的构造方法中:
public TabLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); // 禁用横向滑动条 setHorizontalScrollBarEnabled(false); // new 一个'SlidingTabStrip'的实例,并作为唯一的子 View add 进'TabLayout'. mTabStrip = new SlidingTabStrip(context); super.addView(mTabStrip, 0, new HorizontalScrollView.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT)); // 省略下面的无关代码... }
至此,我们就明白了 TabLayout
中子 View 的一致性是如何保证的。也明白了 TabView
其实才是亲生的, TabItem
其实是后娘养的! 这些代码都很简单,不过我们可以从中学习到很多有用的思想。
至此,一个清晰的 View
层级图应该就出现在了各位同学的眼前。
3.2.2 与 ViewPager 搭配使用
有了上面的的基础,我们再来看看 TabLayout
是如何和它的好基友 ViewPager
搭配使用的。
public void setupWithViewPager(@Nullable final ViewPager viewPager) { //... //为理解简单起见,删掉边角性干扰代码,主要来看核心逻辑 mViewPager = viewPager; // Add our custom OnPageChangeListener to the ViewPager if (mPageChangeListener == null) { mPageChangeListener = new TabLayoutOnPageChangeListener(this); } mPageChangeListener.reset(); viewPager.addOnPageChangeListener(mPageChangeListener); // Now we'll add a tab selected listener to set ViewPager's current item setOnTabSelectedListener(new ViewPagerOnTabSelectedListener(viewPager)); // Now we'll populate ourselves from the pager adapter 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) { // If we already have a PagerAdapter, unregister our observer mPagerAdapter.unregisterDataSetObserver(mPagerAdapterObserver); } mPagerAdapter = adapter; if (addObserver && adapter != null) { // Register our observer on the new adapter if (mPagerAdapterObserver == null) { mPagerAdapterObserver = new PagerAdapterObserver(); } adapter.registerDataSetObserver(mPagerAdapterObserver); } // Finally make sure we reflect the new adapter 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 的。
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); } // Make sure we reflect the currently set ViewPager item if (mViewPager != null && adapterCount > 0) { final int curItem = mViewPager.getCurrentItem(); if (curItem != getSelectedTabPosition() && curItem < getTabCount()) { selectTab(getTabAt(curItem)); } } } else { removeAllTabs(); } } public void removeAllTabs() { // Remove all the views 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 Model mTabs
中移除所有 Tab
对象。然后从 adapter 中获取 tab 信息,循环调用 addTab(Tab, boolean)
方法重新添加 TabView
。最后调用 ViewPager
对象的 getCurrentItem()
方法,获取当前位置,然后调用 selectTab(int position) 恢复 TabView
的选中状态(针对 TabView 的选中,3.2.4 中有详细介绍)。
3.2.3 ViewPager 与 TabLayout 的 Tab 及 indicaotr 协同滚动
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) { // 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(int position) { final TabLayout tabLayout = mTabLayoutRef.get(); if (tabLayout != null && tabLayout.getSelectedTabPosition() != position) { // 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); } } private void reset() { mPreviousScrollState = mScrollState = SCROLL_STATE_IDLE; } }
用过 ViewPager
的同学对 OnPageChangeListener
不会陌生,不多赘述。 TabLayoutOnPageChangeListener
实现了 OnPageChangeListener
, 在 onPageScrolled(...)
方法中做协同滚动处理。滚动的条件是:
final boolean updateIndicator = !(mScrollState == SCROLL_STATE_SETTLING && mPreviousScrollState == SCROLL_STATE_IDLE);
调用 TabLayout 的 setScrollPosition(...)
方法来控制 TabLayout
中 TabView
和 indocator 的协同滚动。
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; } // 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); } }
3.2.3.1 TabLayout 的 Indicator 协同滚动
indicator 的滚动由 SlidingTabStrip 来处理: ``
// Set the indicator position, if enabled if (updateIndicatorPosition) { mTabStrip.setIndicatorPositionFromTabPosition(position, positionOffset); }
这里的 position
是当前选中的位置。 positionOffset
是: 距当前 Tab 滑动的距离
/ 从当前 tab 滑动到下一个 tab 的总距离
这样一个范围在[0,1]间的小数。
SlidingTabStrip#setIndicatorPositionFromTabPosition(int, float)
void setIndicatorPositionFromTabPosition(int position, float positionOffset) { if (mIndicatorAnimator != null && mIndicatorAnimator.isRunning()) { mIndicatorAnimator.cancel(); } mSelectedPosition = position; mSelectionOffset = positionOffset; updateIndicatorPosition(); }
SlidingTabStrip#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) { // 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); }
通过 getChildAt(mSelectedPosition)
, 获取到到 mSelectedPosition
处的 TabView。若滑动的 mSelectionOffset>0f
且当前选中的位置 mSelectedPosition
不是最后一个 TabView. 获取到下一个 TabView,并计算出 indicator 的 left 和 right。
SlidingTabStrip#setIndicatorPosition(int, int)
private 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); } }
非常简单的代码,在调用 ViewCompat.postInvalidateOnAnimation(this)
重绘 View 之前,去掉一些重复绘制的帧。
@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); } }
绘制逻辑很简单。调用 canvas.drawRect(float left, float top, float right, float bottom, Paint paint)
来绘制 indicator.这里:
left = mIndicatorLeft; top = getHeight() - mSelectedIndicatorHeight; right = mIndicatorRight; bottom = getHeight();
3.2.3.2 TabLayout 的 TabView 协同滚动
我们回头来看 3.2.3 中 setScrollPosition(...)
方法
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; } // 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); } }
在 3.2.3.1 中我们知道 indicator 的滚动是通过 mTabStrip.setIndicatorPositionFromTabPosition(position, positionOffset)
实现的。那 TabView 的滚动呢?我们知道 TabLayout
是继承 HorizonScrollView
天生就是一个可以横行滚动的 View
,所以,我们只需要调用 scrollTo(int x, int y)
方法就可以实现横向滚动。
scrollTo(calculateScrollXForTab(position, positionOffset), 0);
这里 x 方向的偏移量调用 calculateScrollXForTab(position, positionOffset)
实时计算得出,y 方向的偏移量为 0。
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 选中状态
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)
方法。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论