文章出處

效果圖

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編寫)


不含病毒。www.avast.com
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 AutoPoster 的頭像
    AutoPoster

    互聯網 - 大數據

    AutoPoster 發表在 痞客邦 留言(0) 人氣()