返回介绍

3.2 分析

发布于 2024-12-23 22:32:13 字数 17123 浏览 0 评论 0 收藏 0

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 层级图应该就出现在了各位同学的眼前。

TabLayout Hierarchy

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(...) 方法来控制 TabLayoutTabView 和 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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文