- 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) 方法。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!

发布评论