自定义强大的TabPagerIndicator

在Android开发中,我们会经常用到ViewPager,当然,与其配套的就是大神JakeWharton写的著名的开源库tabpagerindicator。虽然四年前已经停止维护了,但现在还被广泛使用。

对于tabpagerindicator这个库,可能使用最多的就是TabPageIndicator。目前新闻类的App基本都使用的这种方式:TabPageIndicator+Fragment构件几本的页面布局,如网页新闻:

纵然这种方式简单好用,但并不能满足我们所有的需求。JakeWharton在写这个TabPageIndicator的时候,可能其应用范围只限制于上面的需求,只是显示标题而已。当我们的需求变成标题栏需要现实两行文字或其他View的时候,TabPageIndicator显然就不能满足我们的需求了。这种情况就需要我们自己动手实现了。在此之前,我们可以先分析一下TabPageIndicator的实现思想。

PS:由于代码太多,这里就不贴了。可点击上面的链接查看具体的源码。

TabPageIndicator的实现思想

通过对源码的分析,可得出TabPageIndicator的实现流程:‘

  1. 继承自HorizontalScrollView,简化滑动过程;

  2. 实现PageIndicator接口(PagerIndicator继承了ViewPager.OnPageChangeListener接口),使ViewPager与TabPageIndicator关联起来(通过setViewPager获取ViewPager的引用),保证在ViewPager切换的时候,TabPageIndicator的位置也发生联动(在onPageSelected中调整TabPageIndicator的位置);

  3. 根据PagerAdapter中的getPageTitle动态添加Item(我们知道,HorizontalScrollView只能有一个ChildView, 那么我们只需要动态创建一个LinearLayout, 然后根据PagerAdapter中的getCount, 动态添加相应的Item即可);

  4. 实现TabPageIndicator中Item的点击事件,保证点击切换TabPageIndicator时ViewPager也能联动(在Item的点击事件中调用ViewPager.setCurrentItem(index)切换ViewPager的页面);

整个流程的基本思想就是这样的,可以看的出,TabPageIndicator就是通过获取PagerAdapter中的getPageTitle动态TextView来实现的。
既然我们找到了局限性,那么我们就可以自己实现一个新的TabPageIndicator了。

自定义TabPageIndicatorEx

对于我们自定义的TabPageIndicator,需要实现的是在TabPageIndicator中动态添加用户自定义的子View。既然TabPageIndicator可以通过PagerAdapter获取,那么我们也采用这种方式,只是PagerAdapter本身没有提供这种操作,那就重写一下PagerAdapter,增加一个这样的方法,代码如下:

public abstract class PagerAdapterEx extends PagerAdapter {

    public abstract View getTabView(int position);
}

为了能够同时兼容Fragment, 这里我们继承FragmentPagerAdapter(FragmentPagerAdapter继承自PagerAdapter),那么PagerAdapterEx的最终代码是这样的:

public abstract class PagerAdapterEx extends FragmentPagerAdapter {

    public PagerAdapterEx(FragmentManager fm) {
        super(fm);
    }

    public abstract View getTabView(int position);
}

当然,TabPageIndicator也要进行修改。这里我们按照上面的实现流程实现一个TabPageIndicatorEx, 核心代码如下:

public class TabPageIndicatorEx extends HorizontalScrollView implements ViewPager.OnPageChangeListener {

    ...

    // 获取与之关联的ViewPager,并加载数据
    public void setViewPager(ViewPager view) {
        if (view != null) {
            pagerAdapter = (PagerAdapterEx) view.getAdapter();
            if (pagerAdapter == null) {
                throw new IllegalStateException("ViewPager does not have adapter instance.");
            }
            this.viewPager = view;
            viewPager.addOnPageChangeListener(this);
            notifyDataSetChanged(pagerAdapter);
        } else {
            throw new IllegalStateException("ViewPager is null.");
        }
    }

    // 动态添加Item的过程
    public void notifyDataSetChanged(PagerAdapterEx pagerAdapterEx) {
        if (itemLayout.getChildCount() > 0) {
            itemLayout.removeAllViews();
            tabLayout.removeAllViews();
            removeAllViews();
        }
        if (pagerAdapterEx != null) {
            final int count = pagerAdapterEx.getCount();
            if (count > 0) {
                itemLayout.setLayoutParams(new ViewGroup.LayoutParams(WRAP_CONTENT, MATCH_PARENT));
                // 根据pagerAdapterEx,动态添加子View
                for (int i = 0; i < count; i++) {
                    View itemView = pagerAdapterEx.getTabView(i);
                    final int index = i;
                    itemView.setOnClickListener(new OnClickListener() {
                        @Override
                        public void onClick(View view) {
                                // Item点击时切换ViewPager
                            viewPager.setCurrentItem(index, false);
                            if (itemListener != null) {
                                itemListener.onItemClick(index);
                            }
                        }
                    });
                    itemLayout.addView(itemView, new LinearLayout.LayoutParams((int) mItemWidth, MATCH_PARENT));
                }
                tabLayout.addView(itemLayout);
                // 添加底部的指示器
                indicatorText = new TextView(context);
                indicatorText.setPadding((int) mIndicatorPadding, 0, (int) mIndicatorPadding, 0);
                indicatorText.setBackgroundColor(mIndicatorColor);
                LinearLayout.LayoutParams params = new LinearLayout.LayoutParams((int) (mItemWidth - mIndicatorPadding * 2), (int) mIndicatorHeight);
                params.topMargin = (int) -mIndicatorHeight;
                params.leftMargin = (int) mIndicatorPadding;
                params.rightMargin = (int) mIndicatorPadding;
                indicatorText.setLayoutParams(params);
                tabLayout.addView(indicatorText);

                addView(tabLayout);
            }

            if (currentItem != -1) {
                setCurrentItem(currentItem);
            } else {
                setCurrentItem(0);
            }
        }
    }

    // ViewPager切换后调整TabPageIndicatorEx的位置
     @Override
    public void onPageSelected(int position) {
        indicatorLeft = (int) (position * mItemWidth + mIndicatorPadding);
        // 调整TabPageIndicatorEx的位置
        animateToTab(position);
        // 设置TabPageIndicatorEx中与当前页面匹配的子View的选中状态
        setSelected(position);
        currentItem = position;
        if (listener != null) {
            listener.onPageSelected(position);
        }
    }
    ...

}

至此,一个多功能的TabPageIndicator就实现了,对于需要自定义Indicator布局的需求,只需要在实现ViewPager的Adapter的时候,只需要继承PagerAdaperEx,并在getTabView()自定义子View即可实现。

经典示例

现在市场上的大多数APP,采用的都是经典的四分法,即首页由一个Activity+4个Fragment构成,底部是相应的四个菜单,一般可通过LinearLayout或ButtonGroup来实现,有了这个TabPageIndicatorEx,我们就可以使用TabPageIndicator+Fragment的形式快速实现。

PS: TabPageIndicatorEx默认情况下,底部的指示器是显示状态,如果用于做首页的这种布局,应将TabPageIndicatorEx的indicatorHeight属性设置为0dp。具体的可参见源码及示例代码。

源码及示例代码

TabPageIndicatorEx源码

,