[BOT]自定义ViewPagerStripIndicator

效果图

app中下面这样的控件很常见,像默认的TabHost表现上不够灵活,下面就简单写一个可以结合ViewPager切换内容显示,提供底部“滑动条”指示所显示页签的效果。

这里控件应对的场景是“水平等长度”的若干标题,标题不可滚动。

控件设计

下面是要实现的控件TabIndicator的组成部分:

  1. 底部指示器:也就是蓝色滑动条,记为Indicator。
  2. 分割线,宽度固定为1px的线条,可以不显示。记为Divider。
  3. 页签标题:记为TabView。
  4. 最底部的边框线,高度固定1px,就是给整个View的bottom部分一个分割线。

整体思路

整个TabIndicator是一个LinearLayout的子类,它包含水平方向的TabView——用来显示页签标题。 分割线、底部的指示器、底部的水平边框线都直接在TabIndicator.onDraw()中绘制。

方式很多,这里尽可能使用更少的View实现目标。当然标题文本可以不使用TextView自己绘制。如果需要按下标签时的背景切换效果,使用TextView更好些,而且文本换行,大小等也好控 制。

TabIndicator的设置

TabIndicator作为一个ViewGroup,它需要绘制内容的话就需要设置属性setWillNotDraw(false);以保证它的onDraw()被执行。

要知道childView绘制会覆盖ViewGroup本身的内容,所以这里的思路是利用paddingBottom为要绘制的底部Indicator和BorderLine预留空间。

在其构造方法中:

public TabIndicator(Context context, AttributeSet attrs) {
    ...
    setWillNotDraw(false);
    setGravity(Gravity.CENTER_VERTICAL);
    setPadding(0, 0, 0, mIndicatorHeight);
}

标签标题:TabView

将要显示的标题使用TextView进行显示,为了让水平方向等分宽度,childView设置weight为1。 然后为了显示容器绘制的Divider,俩个TabView之间需要预留空间,使用marginRight即可。

private void buildTabStrip() {
    removeAllViews();

    PagerAdapter adapter = mViewPager.getAdapter();
    TabClickListener tabClickListener = new TabClickListener();

    int tabCount = adapter.getCount();
    int dividerWidth = (int) mDividerWidth;
    for (int i = 0; i < tabCount; i++) {
        LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT);
        params.weight = 1;

        if (dividerWidth > 0) {
            if (i != 0) {
                // use marginRight to make space for divider line.
                params.setMargins(dividerWidth, 0, 0, 0);
            }
        }

        TextView tabTitleView = createTabTitleView(params);
        tabTitleView.setText(adapter.getPageTitle(i));
        tabTitleView.setOnClickListener(tabClickListener);

        addView(tabTitleView);
    }
}

private TextView createTabTitleView(LinearLayout.LayoutParams params) {
    TextView textView = new TextView(getContext());
    textView.setGravity(Gravity.CENTER);
    textView.setBackgroundColor(Color.WHITE);
    textView.setLayoutParams(params);
    return textView;
}

代码中params.weight、params.setMargins()的调用完成了上述操作。 要显示的TabView的个数是根据ViewPager关联的PagerAdapter.getCount()决定的,这里明确 一点:此处的TabIndicator不会像ActionBar自带Tabs视图那样水平滚动,它是一个等宽的页签指示器控件,适合2-6个TabView这样的场景,如果需求不是这样的,这里仅仅是一个思路。

TabClickListener用来监听各个TabView的点击,然后将ViewPager切换到对应位置:

private class TabClickListener implements View.OnClickListener {
  @Override
  public void onClick(View v) {
      for (int i = 0; i < getChildCount(); i++) {
          if (v == getChildAt(i)) {
              mViewPager.setCurrentItem(i);
              return;
          }
      }
  }
}

底部边界线

具体的绘制操作在onDraw()中进行。 边界线就是一条紧贴TabIndicator底部bottom的一个线条,canvas.drawLine()可以完成。 只需要注意一点:绘制的BorderLine的位置必须在TabIndicator的区域内,所以这里应该让 line的y坐标是TabIndicator本身的y减去1。

protected void onDraw(Canvas canvas) {
  ...

  canvas.drawLine(getLeft(), tabHostHeight - 1, getRight(), tabHostHeight - 1, mBottomLinePaint);
}

分割线:Divider

Divider需要在每两个TabView的中间进行绘制,在创建各个TabView时,已经使用marginRight预留了它的显示位置。其高度会在上下各减去一定的值int mDividerPadding,为了美观:

protected void onDraw(Canvas canvas) {
  ...

  if (mEnableDivider && mDividerWidth > 0 && tabCount > 1) {
      View tab = getChildAt(0);

      if (mDividerPadding > tab.getHeight()) {
          mDividerPadding = tab.getHeight() / 2.0f;
      }

      float startY = tab.getY() + mDividerPadding;
      float stopY = tab.getY() + tab.getHeight() - mDividerPadding;

      mDividerPaint.setStrokeWidth(mDividerWidth);
      float halfDividerWidth = mDividerWidth / 2.0f;

      for (int i = 0; i < tabCount - 1; i++) {
          tab = getChildAt(i);

          canvas.drawLine(tab.getRight() + halfDividerWidth,
                  startY, tab.getRight() + halfDividerWidth,
                  stopY,
                  mDividerPaint);
      }
  }
}

同样是一个canvas.drawLine()指令进行绘制,其参数的计算代码是最好的解释。

底部指示器:滑动条

滚动条是有厚度的,所以使用canvas.drawRect()来进行绘制,方法需要绘制的矩形的四个坐标。 top、bottom是固定的。 left、right需要根据ViewPager的拖动进行确定: 假设从n滑动到n+1,那么计算出两个childView之间的水平距离,然后监听ViewPager的切换进度得到offset即可。

监听ViewPager的拖动使用OnPageChangeListener接口,这里为需要的交互规则定义了它的实现类:

private class PageChangeListener extends ViewPager.SimpleOnPageChangeListener {
    private int mScrollState;

    @Override
    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
        int tabCount = getChildCount();
        if ((tabCount == 0) || (position < 0) || (position >= tabCount)) {
            return;
        }

        onViewPagerPageChanged(position, positionOffset);

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

    @Override
    public void onPageScrollStateChanged(int state) {
        mScrollState = state;

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

    @Override
    public void onPageSelected(int position) {
        // this is called before the onPageScrolled progress finished.
        // do not conflict with drag or setting-scroll.
        // ViewPager.setCurrentItem(index, animating) may need this?
        if (mScrollState == ViewPager.SCROLL_STATE_IDLE) {
            onViewPagerPageChanged(position, 0f);
        }

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

为了让使用TabIndicator的代码可以继续监听ViewPager页面切换的事件,mOuterPageListener 用来保存外部代码提供的监听器。

回调方法onPageScrolled()用来通知ViewPager的拖动进度,positionOffset就是当前页面和目标页面切换的进度:0~1的一个float值。

监听器调用onViewPagerPageChanged()来做处理:

public void onViewPagerPageChanged(int position, float positionOffset) {
   if (mSelectedPosition == position
           && mIndicatorOffset == positionOffset) return;

   mSelectedPosition = position;
   mIndicatorOffset = positionOffset;
   invalidate();
}

记录下位置mSelectedPosition和切换进度mIndicatorOffset,然后通知当前TabIndicator进行绘制即可。紧接着在onDraw()中:

protected void onDraw(Canvas canvas) {
  ...

  if (tabCount > 0) {
      int left = selectedTitle.getLeft();
      int right = selectedTitle.getRight();

      if (mIndicatorOffset > 0f && mSelectedPosition < (tabCount - 1)) {
          int offsetPixels = (int) (tabWidth * mIndicatorOffset);
          left += offsetPixels;
          right += offsetPixels;
      }

      canvas.drawRect(left, tabHostHeight - mIndicatorHeight, right,
              tabHostHeight, mIndicatorPaint);
  }
}

对offsetPixels的计算很简单——这里的TabView是等宽的!!! 如果不是等宽的TabView,那么它们之间的水平位置差就是偏移的基准量。

NOTE: 在PageChangeListener.onPageSelected()中的调用onViewPagerPageChanged(position, 0f)用来通知ViewPager发生的瞬间切换,这个在无动画的ViewPager.setCurrentItem()时会发生。------我没实验,这里为了以防万一。 记得对onViewPagerPageChanged()的调用为了不和onPageScrolled()中的调用冲突,它只在 ViewPager处在SCROLL_STATE_IDLE状态时进行。

小结

以上就是TabIndicator的所有内容,这类控件实在是可以很简单,更多的功能意味着更多的代码。 这里没有提供各种property/attrs的代码,保持关键代码的简单。

实际上不一定需要结合ViewPager,代码稍微修改,就可以满足一般的TabHost这类效果的需求。

源码在这里: https://github.com/everhad/ViewPagerTabIndicator

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏向治洪

CoordinatorLayout

CoordinatorLayout作为“super-powered FrameLayout”基本实现两个功能:  1、作为顶层布局  2、调度协调子布局 ...

175100
来自专栏移动开发

android 圆角图片的实现和封装

下面为主要源码,实现了 Picasso 中的 Transformation 接口。

32340
来自专栏Android小菜鸡

Touch事件实现View拖动

  Touch监听事件可以监听手指在屏幕上的行为,例如按下、滑动。抬起。根据这些事件,可以做出View任意推动的效果。

21210
来自专栏Android干货

安卓开发_九宫格布局

31830
来自专栏学海无涯

Android开发之ListView使用经验分享

在Android开发中,ListView是使用最广泛的组件之一,虽然谷歌推出了RecycleView,但是很多项目中依旧在使用ListView,本文将总结一下使...

33360
来自专栏Android中高级开发

Android开发之漫漫长途 番外篇——自定义View的各种姿势1

该文章是一个系列文章,是本人在Android开发的漫漫长途上的一点感想和记录,我会尽量按照先易后难的顺序进行编写该系列。该系列引用了《Android开发艺术探索...

11910
来自专栏三好码农的三亩自留地

教你搞定Android自定义ViewGroup

我们知道ViewGroup就是View的容器类,我们经常用的LinearLayout,RelativeLayout等都是ViewGroup的子类,因为ViewG...

9010
来自专栏分享达人秀

ImageView的属性和方法大全

通过前面几期的学习,TextView控件及其子控件基本学习完成,可以在Android屏幕上显示一些文字或者按钮,那么从本期开始来学习如何进行图片展示,这...

21490
来自专栏Android干货园

Android 自定义上面圆角下面直角的ImageView

版权声明:本文为博主原创文章,转载请标明出处。 https://blog.csdn.net/lyhhj/article/details/48...

96040
来自专栏郭霖

Android滑动菜单框架完全解析,教你如何一分钟实现滑动菜单特效

之前我向大家介绍了史上最简单的滑动菜单的实现方式,相信大家都还记得。如果忘记了其中的实现原理或者还没看过的朋友,请先去看一遍之前的文章 Android滑动菜单特...

35460

扫码关注云+社区

领取腾讯云代金券