PagerSlidingTabStrip,酷炫的ViewPager的滑动导航,使用及解析

本文介绍如何在GitHub上找到一个用于实现简单且丰富样式的ViewPager导航栏的自定义控件。通过XML布局文件定义控件,并在Activity中初始化和配置。了解其工作原理及关键属性,包括tab样式、指示器、分隔线等的定制。详细解析如何通过viewpager的滚动监听实时更新导航栏,以及如何在初始化时加载和配置自定义属性。

最近在github发现了一个酷炫的UI,一个viewpager的导航栏,不仅使用简单,而且还能定义各种样式,使用风格如下图。

这里写图片描述这里写图片描述

使用方法

  • 列表内容

我们只需要在布局文件定义这个自定义控件

<com.astuetz.PagerSlidingTabStrip
    android:id="@+id/tabs"
    android:layout_width="match_parent"
    android:layout_height="48dip" 
    //控件的自定义属性,可不写
    app1:pstsTextAllCaps=""//tab文本是否全部大写
    app1:pstsShouldExpand=""//tab是设置weight1还是设置为wrap_content
    app1:pstsTabBackground=""
    app1:pstsScrollOffset=""//tab刚开始多长不滚动,当为0时,viewpager一滚动它就跟着滚动
    app1:pstsTabPaddingLeftRight=""
    app1:pstsDividerPadding=""
    app1:pstsUnderlineHeight=""
    app1:pstsIndicatorHeight=""
    app1:pstsDividerColor=""
    app1:pstsUnderlineColor=""
    app1:pstsIndicatorColor=""/>
  • 然后当我们在activity初始化的时候,只需要调用如下就可以了

 PagerSlidingTabStrip tabs = (PagerSlidingTabStrip) findViewById(R.id.tabs);
 tabs.setViewPager(pager);//pager是viewpager对象
  • 注意,PagerSlidingTabStrip 调用了viewpager的OnPageChangeListener()的监听方法,我们要是也想实现它的话可以选择调用setOnPageChangeListener来实现viewpager监听器
 tabs.setOnPageChangeListener(mPageChangeListener);

解析

下面我们来看一下他是怎么实现这么些功能的,其实也就是一个类PagerSlidingTabStrip ,他实现了HorizontalScrollView,具体原理就是

  1. tab部分使用scrollview并且在OnPageChangeListener监听器里面实时滑动更新
  2. 指示器部分在ondraw()中画一个rect,并且在OnPageChangeListener监听器里面实时滑动更新

    知道了原理之后分析起来就简单了


PagerSlidingTabStrip 控件的初始化

public PagerSlidingTabStrip(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);

        //这两行代码意义可以参考http://www.aiuxian.com/article/p-638915.html
        setFillViewport(true);
        setWillNotDraw(false);

        //定义tab容器
        tabsContainer = new LinearLayout(context);
        tabsContainer.setOrientation(LinearLayout.HORIZONTAL);
        tabsContainer.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
        addView(tabsContainer);

        ......


        TypedArray a = context.obtainStyledAttributes(attrs, ATTRS);

        ......

        // 这是在xml文件中设置的自定义属性,我们也可以在代码中设置它

        a = context.obtainStyledAttributes(attrs, R.styleable.PagerSlidingTabStrip);

        indicatorColor = a.getColor(R.styleable.PagerSlidingTabStrip_pstsIndicatorColor, indicatorColor);
        //tab下面的与viewpager分隔的一条线
        underlineColor = a.getColor(R.styleable.PagerSlidingTabStrip_pstsUnderlineColor, underlineColor);
        //tab之间的分隔线
        dividerColor = a.getColor(R.styleable.PagerSlidingTabStrip_pstsDividerColor, dividerColor);
        //指示器宽度
        indicatorHeight = a.getDimensionPixelSize(R.styleable.PagerSlidingTabStrip_pstsIndicatorHeight, indicatorHeight);
        //下划线的高度
        underlineHeight = a.getDimensionPixelSize(R.styleable.PagerSlidingTabStrip_pstsUnderlineHeight, underlineHeight);
        dividerPadding = a.getDimensionPixelSize(R.styleable.PagerSlidingTabStrip_pstsDividerPadding, dividerPadding);
        //设置tab的左右內边距
        tabPadding = a.getDimensionPixelSize(R.styleable.PagerSlidingTabStrip_pstsTabPaddingLeftRight, tabPadding);
        tabBackgroundResId = a.getResourceId(R.styleable.PagerSlidingTabStrip_pstsTabBackground, tabBackgroundResId);
        //设置tab布局,false时tab宽度为wrap_content,true时weight设为1
        shouldExpand = a.getBoolean(R.styleable.PagerSlidingTabStrip_pstsShouldExpand, shouldExpand);
        //scrollview根据viewpager滚动的多少而滚动,当scrollOffset设置为n时,scrollview会先让viewpager滚动n之后,再和它同步滚动。
        scrollOffset = a.getDimensionPixelSize(R.styleable.PagerSlidingTabStrip_pstsScrollOffset, scrollOffset);
        //是否全部大写
        textAllCaps = a.getBoolean(R.styleable.PagerSlidingTabStrip_pstsTextAllCaps, textAllCaps);

        a.recycle();

        //设置指示器画笔
        rectPaint = new Paint();
        rectPaint.setAntiAlias(true);
        rectPaint.setStyle(Style.FILL);

        //tab之间的分割线画笔
        dividerPaint = new Paint();
        dividerPaint.setAntiAlias(true);
        dividerPaint.setStrokeWidth(dividerWidth);

        //设置两种布局,当tab项过少时应该调用第二种满屏幕看起来才顺眼
        defaultTabLayoutParams = new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
        expandedTabLayoutParams = new LinearLayout.LayoutParams(0, LayoutParams.MATCH_PARENT, 1.0f);

        ......
    }

控件初始化完成之后我们会调用tabs.setViewPager(pager);下面我们就来分析一下它的实现.

public void setViewPager(ViewPager pager) {
        this.pager = pager;

        if (pager.getAdapter() == null) {
            throw new IllegalStateException("ViewPager does not have adapter instance.");
        }

        pager.setOnPageChangeListener(pageListener);

        notifyDataSetChanged();
    }

该方法主要有两行pager.setOnPageChangeListener(pageListener);和notifyDataSetChanged();
我们来逐个分析它的实现
首先是PageListener,他实现了viewpager的OnPageChangeListener,所以这个viewpager已经定义了监听器,我们注意不要在activity里面再次定义viewpager的监听器了
控件就是通过viewpager的滚动监听来同步更新tab和指示器的

private class PageListener implements OnPageChangeListener {

        //viewpager滑动中的时候会调用
        @Override
        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
            currentPosition = position;//tab的位置,第一张时为0,注意以0开始算的
            currentPositionOffset = positionOffset;//一个tab滑动距离的比例,为0到1之间
            //这个方法是更新tab的
            scrollToChild(position, (int) (positionOffset * tabsContainer.getChildAt(position).getWidth()));
            //滑动的时候要一直调用onDraw()重绘指示器,
            invalidate();
            //回调OnPageChangeListener
            if (delegatePageListener != null) {
                delegatePageListener.onPageScrolled(position, positionOffset, positionOffsetPixels);
            }
        }

        @Override
        public void onPageScrollStateChanged(int state) {
            //每次滑动完了的时候调用scrollToChild,再更新一次
            if (state == ViewPager.SCROLL_STATE_IDLE) {
                scrollToChild(pager.getCurrentItem(), 0);
            }
            //回调OnPageChangeListener
            if (delegatePageListener != null) {
                delegatePageListener.onPageScrollStateChanged(state);
            }
        }

        @Override
        public void onPageSelected(int position) {
            //回调OnPageChangeListener
            if (delegatePageListener != null) {
                delegatePageListener.onPageSelected(position);
            }
        }

    }

然后我们继续查看scrollToChild方法是如何滑动scrollview(tab)的

    /**
     * 将tab根据viewpager的滚动进度而滚动
     * @param position 当前viewpager页码,第一张为0
     * @param offset viewpager滚动的比例*tab项的长度=tab项应该滚动的距离
     */
    private void scrollToChild(int position, int offset) {

        if (tabCount == 0) {
            return;
        }
        //scrollview应该滚动的长度
        int newScrollX = tabsContainer.getChildAt(position).getLeft() + offset;
        if (position > 0 || offset > 0) {
            //scrollview根据viewpager滚动的多少而滚动,当scrollOffset设置为n时,scrollview会先让viewpager滚动n之后,再和它同步滚动。
            //比如设置100dp的时候,viewpager要滑动了100dp距离,scrollview才开始滚动
            newScrollX -= scrollOffset;
        }
        //scrollview滑动
        scrollTo(newScrollX, 0);

    }

在onPageScrolled的时候频繁调用invalidate();来重绘,那就来看看重绘的究竟是什么东西

protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        if (isInEditMode() || tabCount == 0) {
            return;
        }
        //获取控件高度
        final int height = getHeight();

        //设置指示器画笔颜色
        rectPaint.setColor(indicatorColor);

        //获取当前页对应的tab的信息
        View currentTab = tabsContainer.getChildAt(currentPosition);
        float lineLeft = currentTab.getLeft();
        float lineRight = currentTab.getRight();

        //当开始滑动且有下一个tab时调用
        if (currentPositionOffset > 0f && currentPosition < tabCount - 1) {
            //获取要滑动到的那个tab的信息
            View nextTab = tabsContainer.getChildAt(currentPosition + 1);
            final float nextTabLeft = nextTab.getLeft();
            final float nextTabRight = nextTab.getRight();

            //实时更新新的指示器的左右坐标,由于每个tab长度可能不同,这个算法可以根据tab项的大小来实时放大缩小到最后和tab长度一样大
            lineLeft = (currentPositionOffset * nextTabLeft + (1f - currentPositionOffset) * lineLeft);
            lineRight = (currentPositionOffset * nextTabRight + (1f - currentPositionOffset) * lineRight);
        }
        //画指示器
        canvas.drawRect(lineLeft, height - indicatorHeight, lineRight, height, rectPaint);

        // draw underline
        //画下划线
        rectPaint.setColor(underlineColor);
        canvas.drawRect(0, height - underlineHeight, tabsContainer.getWidth(), height, rectPaint);

        // draw divider
        //画tab间的分割线
        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);
        }
    }

好了,已经通过滑动监听来实现实时更新了,接下来就是初始化tab信息了,就是在刚才还有没说的notifyDataSetChanged()中。

public void notifyDataSetChanged() {
        //清空所以组件
        tabsContainer.removeAllViews();
        //获取所有tab数
        tabCount = pager.getAdapter().getCount();

        for (int i = 0; i < tabCount; i++) {

            if (pager.getAdapter() instanceof IconTabProvider) {
                //pageAdapter实现IconTabProvider接口时,tab用图片
                addIconTab(i, ((IconTabProvider) pager.getAdapter()).getPageIconResId(i));
            } else {
                //设置tab的文本
                addTextTab(i, pager.getAdapter().getPageTitle(i).toString());
            }

        }
        //主要是设置tab文本风格,就不介绍了
        updateTabStyles();
        //初始化tab
        getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() {

            @SuppressWarnings("deprecation")
            @SuppressLint("NewApi")
            @Override
            public void onGlobalLayout() {

                if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
                    getViewTreeObserver().removeGlobalOnLayoutListener(this);
                } else {
                    getViewTreeObserver().removeOnGlobalLayoutListener(this);
                }

                currentPosition = pager.getCurrentItem();
                scrollToChild(currentPosition, 0);
            }
        });

    }

总算是研究完了,其实只要理解了实现原理,读起来就不费劲了。。。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值