ViewPager导航控件——SimpleViewpagerIndicator

写这个小控件是因为最近负责维护的一款app大改版,设计师给了一个新的ViewPager导航样式,但找了几个常用的导航控件发现都无法100%实现设计师给的效果,于是就干脆自己动手丰衣足食了。

控件只有一个单独的java类,代码也很简单,放出来希望能帮到需要的人。

效果

控件提供了比较丰富的可配置选项,下面是两个例子:

1.所有配置项均使用默认值(tab宽度包裹内容、indicator与文字等宽…):

2.tab宽度平分父控件剩余空间、indicator与tab等宽…:

配置项

在调用setViewPager前,使用一系列setXXX方法进行设置即可,支持链式调用:

indicator.setExpand(true)//设置tab宽度为包裹内容还是平分父控件剩余空间,默认值:false,包裹内容
    .setIndicatorWrapText(false)//设置indicator是与文字等宽还是与整个tab等宽,默认值:true,与文字等宽
    .setIndicatorColor(Color.parseColor("#ff3300"))//indicator颜色
    .setIndicatorHeight(2)//indicator高度
    .setShowUnderline(true, Color.parseColor("#dddddd"), 2)//设置是否展示underline,默认不展示
    .setShowDivider(true, Color.parseColor("#dddddd"), 10, 1)//设置是否展示分隔线,默认不展示
    .setTabTextSize(16)//文字大小
    .setTabTextColor(Color.parseColor("#ff999999"))//文字颜色
    .setTabTypeface(null)//字体
    .setTabTypefaceStyle(Typeface.NORMAL)//字体样式:粗体、斜体等
    .setTabBackgroundResId(0)//设置tab的背景
    .setTabPadding(0)//设置tab的左右padding
    .setSelectedTabTextSize(20)//被选中的文字大小
    .setSelectedTabTextColor(Color.parseColor("#ff3300"))//被选中的文字颜色
    .setSelectedTabTypeface(null)
    .setSelectedTabTypefaceStyle(Typeface.BOLD)
    .setScrollOffset(120);//滚动偏移量

indicator.setViewPager(viewPager);

所有的配置项均有默认值,也就是说不进行任何设置也是可以的,效果参考上面的第一张图。

代码

源码不多,注释也比较详细,所以就不多废话了:

/**
 * 使用方式:
 * --在调用setViewPager之前使用setxxx方法进行样式设置,支持链式调用
 * --调用setViewPager方法将viewPager绑定到simpleViewpagerIndicator(viewPager的adapter必须实现getPageTitle方法)
 * --调用setOnPageChangeListener来设置viewPager的页面切换监听
 */
public class SimpleViewpagerIndicator extends HorizontalScrollView {

//配置属性 START-----------------------------------------------------------------------------

    /*
     * true:每个tab宽度为平分父控件剩余空间
     * false:每个tab宽度为包裹内容
     */
    private boolean expand = false;

    /*
     * 指示器(被选中的tab下的短横线)
     */
    private boolean indicatorWrapText = true;//true:indicator与文字等长;false:indicator与整个tab等长
    private int indicatorColor = Color.parseColor("#ff666666");
    private int indicatorHeight = 2;//dp

    /*
     * 底线(指示器的背景滑轨)
     */
    private boolean showUnderline = false;//是否展示底线
    private int underlineColor;
    private int underlineHeight;//dp

    /*
     * tab之间的分割线
     */
    private boolean showDivider = false;//是否展示分隔线
    private int dividerColor;
    private int dividerPadding;//分隔线上下的padding,dp
    private int dividerWidth;//分隔线宽度,dp

    /*
     * tab
     */
    private int tabTextSize = 16;//tab字号,dp
    private int tabTextColor = Color.parseColor("#ff999999");//tab字色
    private Typeface tabTypeface = null;//tab字体
    private int tabTypefaceStyle = Typeface.NORMAL;//tab字体样式
    private int tabBackgroundResId = 0;//每个tab的背景资源id
    private int tabPadding = 24;//每个tab的左右内边距,dp

    /*
     * 被选中的tab
     */
    private int selectedTabTextSize = 16;//dp
    private int selectedTabTextColor = Color.parseColor("#ff666666");
    private Typeface selectedTabTypeface = null;
    private int selectedTabTypefaceStyle = Typeface.BOLD;

    /*
     * scrollView整体滚动的偏移量,dp
     */
    private int scrollOffset = 100;

//配置属性 End-------------------------------------------------------------------------------

    private LinearLayout.LayoutParams wrapTabLayoutParams;
    private LinearLayout.LayoutParams expandTabLayoutParams;

    private Paint rectPaint;
    private Paint dividerPaint;
    private Paint measureTextPaint;//测量文字宽度用的画笔

    private final PageListener pageListener = new PageListener();
    private OnPageChangeListener userPageListener;

    private LinearLayout tabsContainer;//tab的容器
    private ViewPager viewPager;

    private int currentPosition = 0;//viewPager当前页面
    private float currentPositionOffset = 0f;//viewPager当前页面的偏移百分比(取值:0~1)
    private int selectedPosition = 0;//viewPager当前被选中的页面

    private int tabCount;
    private int lastScrollX = 0;

    public SimpleViewpagerIndicator(Context context) {
        this(context, null);
    }

    public SimpleViewpagerIndicator(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public SimpleViewpagerIndicator(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        setFillViewport(true);
        setWillNotDraw(false);
    }

    public SimpleViewpagerIndicator setViewPager(ViewPager viewPager) {
        this.viewPager = viewPager;
        if (viewPager.getAdapter() == null) {
            throw new IllegalStateException("ViewPager does not have adapter instance.");
        }

        viewPager.setOnPageChangeListener(pageListener);

        init();
        initView();

        return this;
    }

    public SimpleViewpagerIndicator setOnPageChangeListener(OnPageChangeListener listener) {
        this.userPageListener = listener;
        return this;
    }

    private void init() {
        /*
         * 将dp换算为px
         */
        float density = getContext().getResources().getDisplayMetrics().density;
        indicatorHeight = (int) (indicatorHeight * density);
        underlineHeight = (int) (underlineHeight * density);
        dividerPadding = (int) (dividerPadding * density);
        dividerWidth = (int) (dividerWidth * density);
        tabTextSize = (int) (tabTextSize * density);
        tabPadding = (int) (tabPadding * density);
        selectedTabTextSize = (int) (selectedTabTextSize * density);
        scrollOffset = (int) (scrollOffset * density);

        /*
         * 创建tab的容器(LinearLayout)
         */
        tabsContainer = new LinearLayout(getContext());
        tabsContainer.setOrientation(LinearLayout.HORIZONTAL);
        tabsContainer.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
        addView(tabsContainer);

        /*
         * 创建画笔
         */
        rectPaint = new Paint();
        rectPaint.setAntiAlias(true);
        rectPaint.setStyle(Style.FILL);

        dividerPaint = new Paint();
        dividerPaint.setAntiAlias(true);
        dividerPaint.setStrokeWidth(dividerWidth);

        measureTextPaint = new Paint();
        measureTextPaint.setTextSize(selectedTabTextSize);

        /*
         * 创建两个Tab的LayoutParams,一个为宽度包裹内容,一个为宽度等分父控件剩余空间
         */
        wrapTabLayoutParams = new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);//宽度包裹内容
        expandTabLayoutParams = new LinearLayout.LayoutParams(0, LayoutParams.MATCH_PARENT, 1.0f);//宽度等分
    }

    private void initView() {
        //注意:currentPosition和selectedPosition的含义并不相同,它们分别在onPageScroll和onPageSelected中被赋值
        //在从tab1往tab2滑动的过程中,selectedPosition会比currentPosition先由1变成2
        currentPosition = viewPager.getCurrentItem();
        selectedPosition = viewPager.getCurrentItem();

        tabsContainer.removeAllViews();
        tabCount = viewPager.getAdapter().getCount();

        //创建tab并添加到tabsContainer中
        for (int i = 0; i < tabCount; i++) {
            addTab(i, viewPager.getAdapter().getPageTitle(i).toString());
        }

        //遍历tab,设置tab文字大小和样式
        updateTextStyle();

        //滚动scrollView
        getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
                    getViewTreeObserver().removeGlobalOnLayoutListener(this);
                } else {
                    getViewTreeObserver().removeOnGlobalLayoutListener(this);
                }

                scrollToChild(currentPosition, 0);//滚动scrollView
            }
        });
    }

    /**
     * 添加tab
     */
    private void addTab(final int position, String title) {
        TextView tab = new TextView(getContext());

        tab.setGravity(Gravity.CENTER);
        tab.setSingleLine();
        tab.setText(title);
        if (tabBackgroundResId != 0) {
            tab.setBackgroundResource(tabBackgroundResId);
        }
        tab.setPadding(tabPadding, 0, tabPadding, 0);
        tab.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                viewPager.setCurrentItem(position);
            }
        });

        tabsContainer.addView(tab, position, expand ? expandTabLayoutParams : wrapTabLayoutParams);
    }

    /**
     * 遍历tab,设置tab文字大小和样式
     */
    private void updateTextStyle() {
        for (int i = 0; i < tabCount; i++) {
            TextView tvTab = (TextView) tabsContainer.getChildAt(i);

            if (i == selectedPosition) {//被选中的tab
                tvTab.setTextSize(TypedValue.COMPLEX_UNIT_PX, selectedTabTextSize);
                tvTab.setTypeface(selectedTabTypeface, selectedTabTypefaceStyle);
                tvTab.setTextColor(selectedTabTextColor);
            } else {//未被选中的tab
                tvTab.setTextSize(TypedValue.COMPLEX_UNIT_PX, tabTextSize);
                tvTab.setTypeface(tabTypeface, tabTypefaceStyle);
                tvTab.setTextColor(tabTextColor);
            }
        }
    }

    /**
     * 滚动scrollView
     * <p>
     * 注意:当普通文字字号(tabTextSize)与被选中的文字字号(selectedTabTextSize)相差过大,且tab的宽度模式为包裹内容(expand = false)时,
     * 由于文字选中状态切换时文字宽度突变,造成tab宽度突变,可能导致scrollView在滚动时出现轻微抖动。
     * 因此,当普通文字字号(tabTextSize)与被选中的文字字号(selectedTabTextSize)相差过大时,应避免使tab宽度包裹内容(expand = false)。
     */
    private void scrollToChild(int position, int offset) {
        if (tabCount == 0) return;

        //getLeft():tab相对于父控件,即tabsContainer的left
        int newScrollX = tabsContainer.getChildAt(position).getLeft() + offset;

        //附加一个偏移量,防止当前选中的tab太偏左
        //可以去掉看看是什么效果
        if (position > 0 || offset > 0) {
            newScrollX -= scrollOffset;
        }

        if (newScrollX != lastScrollX) {
            lastScrollX = newScrollX;
            scrollTo(newScrollX, 0);
        }
    }

    /**
     * 绘制indicator、underline和divider
     */
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        if (isInEditMode() || tabCount == 0) return;

        final int height = getHeight();

        /*
         * 绘制divider
         */
        if (showDivider) {
            dividerPaint.setColor(dividerColor);
            for (int i = 0; i < tabCount - 1; i++) {
                View tab = tabsContainer.getChildAt(i);
                canvas.drawLine(tab.getRight(), dividerPadding, tab.getRight(), height - dividerPadding, dividerPaint);
            }
        }

        /*
         * 绘制underline(indicator的背景线)
         */
        if (showUnderline) {
            rectPaint.setColor(underlineColor);
            canvas.drawRect(0, height - underlineHeight, tabsContainer.getWidth(), height, rectPaint);
        }

        /*
         * 绘制indicator
         */
        if (indicatorWrapText) {//indicator与文字等长
            rectPaint.setColor(indicatorColor);
            getTextLocation(currentPosition);
            float lineLeft = textLocation.left;
            float lineRight = textLocation.right;
            if (currentPositionOffset > 0f && currentPosition < tabCount - 1) {
                getTextLocation(currentPosition + 1);
                final float nextLeft = textLocation.left;
                final float nextRight = textLocation.right;

                lineLeft = lineLeft + (nextLeft - lineLeft) * currentPositionOffset;
                lineRight = lineRight + (nextRight - lineRight) * currentPositionOffset;
            }
            canvas.drawRect(lineLeft, height - indicatorHeight, lineRight, height, rectPaint);
        } else {//indicator与tab等长
            rectPaint.setColor(indicatorColor);
            View currentTab = tabsContainer.getChildAt(currentPosition);
            float lineLeft = currentTab.getLeft();
            float lineRight = currentTab.getRight();
            if (currentPositionOffset > 0f && currentPosition < tabCount - 1) {
                View nextTab = tabsContainer.getChildAt(currentPosition + 1);
                final float nextLeft = nextTab.getLeft();
                final float nextRight = nextTab.getRight();

                lineLeft = lineLeft + (nextLeft - lineLeft) * currentPositionOffset;
                lineRight = lineRight + (nextRight - lineRight) * currentPositionOffset;
            }
            canvas.drawRect(lineLeft, height - indicatorHeight, lineRight, height, rectPaint);
        }
    }

    /**
     * 获得指定tab中,文字的left和right
     */
    private void getTextLocation(int position) {
        View tab = tabsContainer.getChildAt(position);
        String tabText = viewPager.getAdapter().getPageTitle(position).toString();

        float textWidth = measureTextPaint.measureText(tabText);
        int tabWidth = tab.getWidth();
        textLocation.left = tab.getLeft() + (int) ((tabWidth - textWidth) / 2);
        textLocation.right = tab.getRight() - (int) ((tabWidth - textWidth) / 2);
    }

    private LeftRight textLocation = new LeftRight();

    class LeftRight {
        int left, right;
    }

    private class PageListener implements OnPageChangeListener {
        @Override
        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
            currentPosition = position;
            currentPositionOffset = positionOffset;

            //scrollView滚动
            scrollToChild(position, (int) (positionOffset * tabsContainer.getChildAt(position).getWidth()));

            invalidate();//invalidate后onDraw会被调用,绘制indicator、divider等

            if (userPageListener != null) {
                userPageListener.onPageScrolled(position, positionOffset, positionOffsetPixels);
            }
        }

        @Override
        public void onPageScrollStateChanged(int state) {
            if (state == ViewPager.SCROLL_STATE_IDLE) {
                scrollToChild(viewPager.getCurrentItem(), 0);//scrollView滚动
            }

            if (userPageListener != null) {
                userPageListener.onPageScrollStateChanged(state);
            }
        }

        @Override
        public void onPageSelected(int position) {
            selectedPosition = position;
            updateTextStyle();//更新tab文字大小和样式

            if (userPageListener != null) {
                userPageListener.onPageSelected(position);
            }
        }
    }

//setter------------------------------------------------------------------------------------
	......
}

完整代码和demo见:https://github.com/al4fun/SimpleViewpagerIndicator

发布了46 篇原创文章 · 获赞 38 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/al4fun/article/details/79767269