【记录】记录点滴
场景:修改TabLayout的指示器长度
需求:未使用自定义Tab样式的情况下,指示器长度为文字内容长度
1. 方法基本有1)反射修改,局限性较大;2)修改TabLayout文件,根据自身的需求实现指示器
2. 为了保证灵活性和较好的用户体验,修改了TabLayout文件修改指示器长度
追踪源码,知道SlidingTabStrip负责展示Tab及对应的指示器。
@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);
}
}
SlidingTabStrip在绘制完主要内容后,会继续绘制圆角矩形作为指示器。修改mIndicatorLeft及mIndicatorRight就可以改变指示器的长度,setIndicatorPosition()方法会修改它们的值并触发绘制。
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);
}
}
PS: 简单描述下结合ViewPager使用时,TabLayout如何处理指示器绘制的
updateIndicatorPosition() 和 animateIndicatorToPosition() 调用了setIndicatorPosition(),为了定制指示器,需要修改这两个方法。在updateIndicatorPosition()方法中,从某个Tab项 -> 下一个Tab项(右侧的Tab),mSelectionOffset 的变化范围为[0,1],其实mSelectionOffset 和mSelectedPosition对应的就是ViewPager的scrollOffset和position,根据变化和需求计算得到left和right,就可以改变指示器的长度,如:
private void updateIndicatorPosition() {
final TabView selectedTitle = (TabView) getChildAt(mSelectedPosition);
int left, right;
if (selectedTitle != null && selectedTitle.getWidth() > 0) {
//修改TabView,新增getTextLeft()和getTextRight()方法
//获得Tab中TextView的左右x轴坐标
left = selectedTitle.getTextLeft();
right = selectedTitle.getTextRight();
//mSelectedPosition = Math.round(viewPagerPosition + viewPagerScrollOffset)
//mSelectedOffset = viewPagerScrollOffset
if (mSelectionOffset > 0f && mSelectedPosition < getChildCount() - 1) {
//mSelectionOffset > 0f -> 有滑动 &&
//mSelectedPosition < getChildCount() - 1 -> 有下一个Tab项
// Draw the selection partway between the tabs
TabView nextTitle = (TabView) getChildAt(mSelectedPosition + 1);
left = (int) (mSelectionOffset * nextTitle.getTextLeft() +
(1.0f - mSelectionOffset) * left);
right = (int) (mSelectionOffset * nextTitle.getTextRight() +
(1.0f - mSelectionOffset) * right);
}
} else {
left = right = -1;
}
setIndicatorPosition(left, right);
}
在TabView中新增方法
public int getTextLeft(){
return getLeft() + mTextView.getLeft();
}
public int getTextRight(){
return getLeft() + mTextView.getRight();
}
结合ViewPager使用时,会调用setupWithViewPager(),为viewPager设置自定义的TabLayoutOnPageChangeListener(也就是ViewPager.OnPageChangeistener)和ViewPagerOnTabSelectedListener来响应ViewPager的滑动和切换。
页面滑动时(手指滑动),触发TabLayoutOnPageChangeListener的onPageScrolled来调用setScrollPosition()方法,当滑动状态满足条件时,就会更新绘制指示器。这里涉及到ViewPager的状态变化,共有3中状态:
/**
* Indicates that the pager is in an idle, settled state. The current page
* is fully in view and no animation is in progress.
*/
public static final int SCROLL_STATE_IDLE = 0;
/**
* Indicates that the pager is currently being dragged by the user.
*/
public static final int SCROLL_STATE_DRAGGING = 1;
/**
* Indicates that the pager is in the process of settling to a final position.
*/
public static final int SCROLL_STATE_SETTLING = 2;
(1)滑动时,state = SCROLL_STATE_DRAGGING,松手后状态 state = SCROLL_STATE_SETTLING -> state = SCROLL_STATE_IDLE。也就是state -> 1 -> 2 -> 0
setCurrentItem()来切换页面时,state = SCROLL_STATE_SETTLING -> state = SCROLL_STATE_IDLE。也就是state -> 2 -> 0
// 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);
在滑动ViewPager时,一些关键方法的调用顺序是 setScrollPosition() -> setIndicatorPositionFromTabPosition() -> updateIndicatorPosition() -> setIndicatorPosition()更新指示器
(2)页面滑动后(手指抬起),ViewPager的onTouchEvent中,抬起时会判断targetPage,在计算将滑动的距离后调用Scroller的startScroll。在computeScroll()中,scrollTo()来滑动ViewPager并由onPageScrolled()回调OnPageChangeListener的更新指示器。当前Tab和前一个记录的Tab不同时,ViewPager中还会调用onPageSelected切换页面,执行selectTab(此时updateIndicator = false)并再次调用setCurrentItem,不过它会执行setScrollingCacheEnabled后结束。
void setCurrentItemInternal(int item, boolean smoothScroll, boolean always, int velocity) {
if (mAdapter == null || mAdapter.getCount() <= 0) {
setScrollingCacheEnabled(false);
return;
}
if (!always && mCurItem == item && mItems.size() != 0) {
setScrollingCacheEnabled(false);
return;
}
if (item < 0) {
item = 0;
} else if (item >= mAdapter.getCount()) {
item = mAdapter.getCount() - 1;
}
final int pageLimit = mOffscreenPageLimit;
if (item > (mCurItem + pageLimit) || item < (mCurItem - pageLimit)) {
// We are doing a jump by more than one page. To avoid
// glitches, we want to keep all current pages in the view
// until the scroll ends.
for (int i = 0; i < mItems.size(); i++) {
mItems.get(i).scrolling = true;
}
}
final boolean dispatchSelected = mCurItem != item;
if (mFirstLayout) {
// We don't have any idea how big we are yet and shouldn't have any pages either.
// Just set things up and let the pending layout handle things.
mCurItem = item;
if (dispatchSelected) {
dispatchOnPageSelected(item);
}
requestLayout();
} else {
populate(item);
scrollToItem(item, smoothScroll, velocity, dispatchSelected);
}
}
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) {
// If we don't currently have a tab, just draw the indicator
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);
}
}
}
(3)点击Tab项时,会调用selectTab() -> animateToTab() -> animateIndicatorToPosition()
所以animateIndicatorToPosition(),稍作修改就能得到效果
void animateIndicatorToPosition(final int position, int duration) {
...
final int targetLeft = targetView.getTextLeft();
final int targetRight = targetView.getTextRight();
...
}