zoukankan      html  css  js  c++  java
  • [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

    (本文使用Atom编写)

  • 相关阅读:
    how to write a paper
    attention mechanism思考
    OSX-KVM 安装备忘指南: 在KVM虚拟机中运行macOSX Big Sur / Catalina
    Unity CacheServer6.x版本 安装配置说明
    Linux常用命令2
    Ubuntu 18.04 + apache2.4 配置https证书(SSL)笔记
    Linux常用命令随笔
    软件测试-8 集成测试
    软件测试-7 在实际测试时的一些想法
    软件测试-6 白盒测试
  • 原文地址:https://www.cnblogs.com/everhad/p/6246807.html
Copyright © 2011-2022 走看看