天天看点

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