TabLayout --- 修改指示器

【记录】记录点滴

场景:修改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();
    ...
}

猜你喜欢

转载自blog.csdn.net/FeiLaughing/article/details/82887207