天天看點

Android 自制ViewPager的訓示器PagerIndicator

1.實際效果

由于之前用到了一個比較老的類似的訓示器架構,發現它功能并不完善(發現顯示層是用圖檔來做的,感覺好呆闆的控件),而且UI很醜(使用藍色png圖檔填充的,為了搭配我的app色調,特意用ps把原圖檔素材全改為白色了[]~( ̄▽ ̄)~*),就如下面我之前做的App上展示的那樣。後來發現Bilibili用戶端的效果不錯,稍微思考一下發現原理不難,就嘗試着自己寫一個,終于成品出來了!

Android 自制ViewPager的訓示器PagerIndicator
Android 自制ViewPager的訓示器PagerIndicator
Android 自制ViewPager的訓示器PagerIndicator

從左到右依次是純粹菜單,Bilibili用戶端,本案例demo

2.原理機制

完成這個自定義ViewGroup時遇到了不少bug,不過主要是邏輯方面的bug,認真檢查後一一排除問題了。

制作類似的控件要解決的主要是滑動ViewPager時訓示器的顯示問題,這個需要很細緻清晰的邏輯,然後是對自定義View各個内部方法調用順序和機制的熟練程度。

整個PagerIndicator運作原理:根據傳入的ViewPager,調用其監聽方法,根據其狀态動态繪制标題下方的小橫條(矩形)。當使用者滑動頁面時,小橫條會按比例移動相應的距離;當滑動結束時,小橫條固定在指定的位置

下面貼出PagerIndicator源碼:

package chen.capton.custom;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Gravity;
import android.view.View;
import android.widget.HorizontalScrollView;
import android.widget.LinearLayout;
import android.widget.TextView;

import java.util.ArrayList;
import java.util.List;

import static android.content.ContentValues.TAG;

/**
 * Created by CAPTON on 2017/1/13.
 */

public class PagerIndicator extends HorizontalScrollView implements View.OnClickListener{
    private Context context;
    private int textColor;   //标題字型的顔色
    private int textCheckedColor; //選中時标題字型的顔色
    private int textSize;    //标題字型大小
    private int lineColor;  //橫條顔色
    private int lineHeight; //橫條厚度(高度)
    private float lineProportion; //橫條占比(寬度)0.0~1.0
    private ViewPager viewPager;  //儲存傳進來的ViewPager
    private List<TextView> textViewList=new ArrayList<>();//顯示各個标題的TextView集合
    private List<String> titleList;//标題字元串ArrayList
    private LinearLayout wrapper; //由于控件繼承自HorizontalView,其直系子View必須是唯一的LinearLayout。

    public ViewPager getViewPager() {return viewPager;}
    //傳回值為控件本體對象,友善使用者鍊式調用,下同
    public PagerIndicator setViewPager(ViewPager viewPager) {
        this.viewPager = viewPager;
        return this;
    }
    public List<String> getTitleList() {return titleList;}
    public PagerIndicator setTitleList(List<String> titleList) {
        this.titleList = titleList;
        return this;
    }
    public int getTextColor() {return textColor;}
    public PagerIndicator setTextColor(int textColor) {
        this.textColor = getResources().getColor(textColor);
        invalidate();
        return this;
    }
    public int getTextSize() {return textSize;}
    public PagerIndicator setTextSize(int textSize) {
        this.textSize =  DisplayUtil.sp2px(context,textSize);;
        invalidate();
        return this;
    }
    public int getTextCheckedColor() {return textCheckedColor;}
    public PagerIndicator setTextCheckedColor(int textCheckedColor) {
        this.textCheckedColor = getResources().getColor(textCheckedColor);
        invalidate();
        return this;
    }
    public int getLineColor() {return lineColor;}
    public PagerIndicator setLineColor(int lineColor) {
        this.lineColor = getResources().getColor(lineColor);
        paint.setColor(this.lineColor);
        invalidate();
        return this;
    }
    public int getLineHeight() {return lineHeight;}
    public PagerIndicator setLineHeight(int lineHeight) {
        this.lineHeight = DisplayUtil.dip2px(context,lineHeight);
        invalidate();
        return this;
    }
    public float getLineProportion() {return lineProportion;}
    public PagerIndicator setLineProportion(float lineProportion) {
        this.lineProportion = lineProportion;
        invalidate();
        return this;
    }

    public PagerIndicator(Context context) {
        this(context,null);
    }
    public PagerIndicator(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);}
    public PagerIndicator(Context context, AttributeSet attrs) {
        this(context, attrs,);
        this.context=context;
        TypedArray ta=context.obtainStyledAttributes(attrs,R.styleable.PagerIndicator);
        textColor=ta.getColor(R.styleable.PagerIndicator_textColor,getResources().getColor(R.color.pager_indicator_black)); //預設标題字型為黑色
        textCheckedColor=ta.getColor(R.styleable.PagerIndicator_textCheckedColor,getResources().getColor(android.R.color.white)); //預設選中标題為白色
        lineColor=ta.getColor(R.styleable.PagerIndicator_lineColor,getResources().getColor(android.R.color.white)); //預設橫線為白色
        textSize= (int) ta.getDimension(R.styleable.PagerIndicator_textSize,);    //預設14sp(44px)
        lineHeight= (int) ta.getDimension(R.styleable.PagerIndicator_lineHeight,); //預設6dp(18px)
        lineProportion=ta.getFloat(R.styleable.PagerIndicator_lineProportion,f); //預設選項寬度的1/3
        /*防止使用者輸入參數超出範圍造成顯示錯誤*/
        if(lineProportion>){
            lineProportion=;
        }
        if(lineProportion<){
            lineProportion=;
        }
        ta.recycle();
        wrapper=new LinearLayout(context);
        paint=new Paint();
        paint.setColor(lineColor);
    }

    boolean once;
    int width,heigth;//控件寬度,控件高度
    int childrenWidth=; //标題視圖的寬度總和
    List<Integer> childWidthList=new ArrayList<>();
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthSize=MeasureSpec.getSize(widthMeasureSpec);
        int heightSize=MeasureSpec.getSize(heightMeasureSpec);
        int heightMode=MeasureSpec.getMode(heightMeasureSpec);
        //當使用者設定本空間高度為“wrap_content”時,預設高度設定為40dp,其他情況則是實際高度
        heightSize=heightMode==MeasureSpec.AT_MOST?DisplayUtil.dip2px(context,):heightSize;
        width=widthSize;  
        heigth=heightSize; 
        while (!once) {
            LinearLayout.LayoutParams lp2 = new LinearLayout.LayoutParams(widthSize, heightSize);
            lp2.gravity=LinearLayout.HORIZONTAL;
            wrapper.setLayoutParams(lp2); 

            //根據傳進來的标題ArrayList動态添加子View(标題項)
            for (int i = ; i <titleList.size(); i++) {
                int childWidth=titleList.get(i).length()*;
                childWidthList.add(childWidth);
                TextView textView=new TextView(context);
                textView.setText(titleList.get(i));
                textView.setTextSize(DisplayUtil.px2sp(context,textSize));
                textView.setTextColor(textColor);
                textView.setGravity(Gravity.CENTER);
                LayoutParams lp3=new LayoutParams(childWidth,
                heightSize-DisplayUtil.px2dip(context,lineHeight));
                textView.setLayoutParams(lp3);
                 //設定Tag,用于辨別此TextView對象,當點選标題時,根據此Tag的i值,ViewPager将跳轉至頁面i。
                textView.setTag(i);
                textView.setOnClickListener(this);
                childrenWidth+=childWidth;
                textViewList.add(textView);
                 wrapper.addView(textView);
            }
            textViewList.get().setTextColor(textCheckedColor);
            addView(wrapper);
            once=true;
        }
        setMeasuredDimension(widthSize,heightSize);//測量控件本體
    }
    int tempPosition=;
    float goneWidth=;//儲存目前标題之前的所有标題寬度,通過計算,确定目前小橫條的位置
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        wrapper.layout(,,childrenWidth,heigth);//布局直系唯一子View,參數 r 必須等于其子View的寬度之和,如果等于父控件寬度,則當子View寬度大于父控件時,多出部分的子View沒法顯示,也沒有拖動效果。
        viewPager.setOnPageChangeListener(new ViewPager.OnPageChangeListener() {
            @Override
            public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
            /*
            *核心算法部分,當手指滑動螢幕時頁面跟着滑動,訓示器的小橫條跟着移動;停止滑動時,小橫條顯示在正确的位置上。(ps:這部分算法因人而異,我相信大家可以寫出更簡潔高效的代碼,就不多做注釋了)
            */
                if(position==tempPosition) {
                    left = childWidthList.get(position)*(-lineProportion)/
              +positionOffset*childWidthList.get(position)+goneWidth;
                    right = childWidthList.get(position)*(+lineProportion)/+
                    positionOffset*childWidthList.get(position)+goneWidth;
                    top = heigth - DisplayUtil.px2dip(context,lineHeight);
                    bottom = heigth;
                }else {
                    int sum=;
                    for (int i = ; i < position; i++) {
                        sum+=childWidthList.get(i);
                    }
                    goneWidth=sum;
                    if(position>tempPosition) {
                        left = childWidthList.get(position)*(-lineProportion)/+
                        goneWidth;
                        right = childWidthList.get(position)*(+lineProportion)/+
                        goneWidth;
                    }else{
                        left = childWidthList.get(position)*(-lineProportion)/+positionOffset*childWidthList.get(position)+goneWidth;
                        right = childWidthList.get(position)*(+lineProportion)/+positionOffset*childWidthList.get(position)+goneWidth;
                    }
                    /*
                    *這是當頁面很多,标題寬度很大(超過螢幕了)時,将標明的标題居中顯示的算法
                    */
                    float scrollCenter=goneWidth+childWidthList.get(position)/;
                    if (scrollCenter>=width/){
                        smoothScrollTo((int) (scrollCenter-width/),);
                    }else {
                        smoothScrollTo(,);
                    }
                    tempPosition=position;
                }
                invalidate();
            }
            @Override
            public void onPageSelected(int position) {
                for(TextView textview:textViewList){
                    textview.setTextColor(textColor);
                }
                textViewList.get(position).setTextColor(textCheckedColor);
                invalidate();
            }
            @Override
            public void onPageScrollStateChanged(int state) {}
        });
    }
    Paint paint;  //繪制小橫條的畫筆
    float left,top,right,bottom;//繪制小橫條(矩形)的四個邊界
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawRect(left,top,right,bottom,paint);
    }
    @Override
    public void onClick(View v) {
        if(v instanceof TextView){
        //根據之前動态初始化TextView時設定的Tag(int類型),跳轉頁面
            viewPager.setCurrentItem((Integer) v.getTag());
        }
    }
}
           

在ViewGroup中,onMeasure,onLayout,onDraw三個方法依次調用,其中onDraw方法在使用者觸摸或者拖動控件時會調用多次,當調用invalidate方法時,onDraw也會被調用。是以有了我們重繪控件的契機,即調用傳進來的ViewPager對象的setOnPageChangeListener(new ViewPager.OnPageChangeListener() {})方法,通過在添加的OnPageChangeListener監聽器中根據ViewPager的滑動狀态來同步重繪我們的PagerIndicator控件的小橫條,例如:

viewPager.setOnPageChangeListener(new ViewPager.OnPageChangeListener() {
            @Override
            public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
            /*
            *核心算法部分,當手指滑動螢幕時頁面跟着滑動,訓示器的小橫條跟着移動;停止滑動時,小橫條顯示在正确的位置上。(ps:這部分算法因人而異,我相信大家可以寫出更簡潔高效的代碼,就不多做注釋了)
            */
                if(position==tempPosition) {
                    left = childWidthList.get(position)*(-lineProportion)/
              +positionOffset*childWidthList.get(position)+goneWidth;
                    right = childWidthList.get(position)*(+lineProportion)/+
                    positionOffset*childWidthList.get(position)+goneWidth;
                    top = heigth - DisplayUtil.px2dip(context,lineHeight);
                    bottom = heigth; 
                }else {
                    int sum=;
                    for (int i = ; i < position; i++) {
                        sum+=childWidthList.get(i);
                    }
                    goneWidth=sum;
                    if(position>tempPosition) {
                        left = childWidthList.get(position)*(-lineProportion)/+
                        goneWidth;
                        right = childWidthList.get(position)*(+lineProportion)/+
                        goneWidth;
                    }else{
                        left = childWidthList.get(position)*(-lineProportion)/+positionOffset*childWidthList.get(position)+goneWidth;
                        right = childWidthList.get(position)*(+lineProportion)/+positionOffset*childWidthList.get(position)+goneWidth;
                    }
                    /*
                    *這是當頁面很多,标題寬度很大(超過螢幕了)時,将標明的标題居中顯示的算法
                    */
                    float scrollCenter=goneWidth+childWidthList.get(position)/;
                    if (scrollCenter>=width/){
                        smoothScrollTo((int) (scrollCenter-width/),);
                    }else {
                        smoothScrollTo(,);
                    } 
                    tempPosition=position;
                }
                invalidate();
            }
            @Override
            public void onPageSelected(int position) {
                for(TextView textview:textViewList){
                    textview.setTextColor(textColor);
                }
                textViewList.get(position).setTextColor(textCheckedColor);
                invalidate();
            }
            @Override
            public void onPageScrollStateChanged(int state) {}
        });
    }
    Paint paint;  //繪制小橫條的畫筆
    float left,top,right,bottom;//繪制小橫條(矩形)的四個邊界
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawRect(left,top,right,bottom,paint);
    }
    @Override
    public void onClick(View v) {
        if(v instanceof TextView){
        //根據之前動态初始化TextView時設定的Tag(int類型),跳轉頁面
            viewPager.setCurrentItem((Integer) v.getTag());
        }
    }
}
           

3.具體用法

詳情請見 5.相關檔案

4.作者的話

對于自定義View與自定義ViewGroup我還不是特别熟悉,很多沒見過的用法和函數都沒深入去涉獵,例如專注界面繪制的Canvas,Paint,Drawable等等。希望看到我文章的大觸們多多指教,對于那些看不懂這片代碼的同學,建議先去掌握好自定義View(ViewGroup)的基礎再來吐槽吧。

5.相關檔案

GitHub連結:https://github.com/Ccapton/pagerIndicator