当前位置: 首页 > 工具软件 > Indicator > 使用案例 >

android 自定义Indicator

翟英达
2023-12-01
/**
 * @author on 2018/11/12.
 * Describe: 自定义指示器
 *               1.支持指示器与文字等长效果 ******;
 *               2.支持指示器与指示器文本等长
 *               3.支持选中调整文字的大小
 *               4.链式调用 支持各种颜色的修改
 *               5.支持三角指示器以及下划线指示器效果
 *               和TabLayout效果相类似
 * 注意点:
 * 确保一定在ViewPagerAdapter中返回了:标题字符串,否则 <addTextFromViewPager/> 会报空指针异常
 */
public class NiceViewPagerIndicator extends HorizontalScrollView {

    public  enum IndicatorType{
        /**
         * 与标题等长
         */
        EQUAL_TAB,
        /**
         * 与文字等长
         */
        EQUAL_TEXT,
        /**
         * 指示器绝对长度
         */
        ABSOLUTE_LENGTH;
    }

    public enum IndicatorShape{
        /**
         * 线性
         */
        LINEAR,
        /**
         * 三角
         */
        TRIANGLE
    }

    /**
     * 记录当前指示器的左右的X轴坐标
     */
    private float mLineLeft;
    private float mLineRight;

    private  NicePageChangeListener mNicePageChangeListener;
    private ViewPager mViewPager;
    private Context mContext;
    private IndicatorType mIndicatorType = IndicatorType.EQUAL_TEXT;
    private IndicatorShape mIndicatorShape = IndicatorShape.LINEAR;

    /**
     * 控件的高度
     */
    private int mIndicatorHeight;
    private int mIndicatorWidth;

    /**
     * 三角指示器------
     */
    private Path mTrianglePath;
    private int mTriangleWidth = 14;
    private int mTriangleHeight = 6;
    private Paint mTrianglePaint;



    /**
     * ScrollView下的唯一子布局
     * 承载容纳文本控件的作用 -----
     */
    private LinearLayout mLinearContainer;
    /**
     * 滑动过程中的选中操作指示器Index
     */
    private int mSelectedIndex;
    /**
     * 滑动结束后停留的指示器Index
     */
    private int mCurrentIndex;
    /**
     * ViewPager 滑动参数 0-1
     */
    private float mCurrentPositionOffset;
    /**
     * 标示tab的个数
     */
    private int mTabCount;

    /**
     * 默认控制器水平padding
     */
    private static final int DEFAULT_TAB_HNL_PADDING = 20;
    /**
     * 水平方向上的文本控件Padding
     */
    private int mTabHorizontalPadding = DEFAULT_TAB_HNL_PADDING;

    /**
     *  根据控件的多少设定是否需要等分:
     *  false ----- 设置标题平分(针对标题较 少)  [标题不可滑动]
     *  true  ----- 设置标题按一定的Padding铺满整个Linear 适应较多的标题 [标题可能可以滑动]
     */

    private boolean isExpand = true;
    private LinearLayout.LayoutParams wrapTabLayoutParams;
    private LinearLayout.LayoutParams expandTabLayoutParams;
    /*
     * 指示器(被选中的tab下的短横线)
     * true:indicator与文字等长;false:indicator与整个tab等长
     */

    private int mNormalTextColor = Color.BLACK;
    private int mSelectedTextColor = Color.RED;
    private int mNormalTextSize = 30;
    private int mSelectedTextSize = 35;
    /**
     * 标识 HorizontalScrollView滑动的x点
     */
    private int lastScrollX = 0;

    /**
     * 用于测量文字长度的画笔
     */
    private Paint mMeasureTextPaint;

    private Paint mIndicatorPaint;
    private int mIndicatorColor = Color.GREEN;

    private int mIndicatorStrokeWidth = 10;

    /**
     * 在ABSOLUTE_LENGTH 指示器类型下,默认的指示器长度
     */
    private int mIndicatorLength = 40;

    /*
     * scrollView整体滚动的偏移量,dp
     */
    private int mScrollOffset = 100;


    /*----------------- 使用者可根据需要进行参数的修正 契合自己项目的需求-------------------*/

    /**
     * 设置指示器的高度
     * @param indicatorHeightDp dp为单位
     * @return
     */
    public NiceViewPagerIndicator setIndicatorHeight(int indicatorHeightDp){
        this.mIndicatorStrokeWidth = indicatorHeightDp;
        return this;
    }

    /**
     * 是否与文字等长 --------
     * @return
     */
    public NiceViewPagerIndicator setIndicatorLengthType(IndicatorType mIndicatorType){
        this.mIndicatorType = mIndicatorType;
        return this;
    }


    public NiceViewPagerIndicator setIndicatorShapeType(IndicatorShape indicatorShapeType){
        this.mIndicatorShape = indicatorShapeType;
        return this;
    }

    /**
     * 设置文本指示器的水品左右水品padding
     */
    public NiceViewPagerIndicator setIndicatorHorlPadding(int tabHorizontalPadding){
        this.mTabHorizontalPadding = tabHorizontalPadding;
        return this;
    }



    /**
     * 设置是否平分布局
     * @param isExpand boolean
     * @return
     */
    public NiceViewPagerIndicator setIsExpand(boolean isExpand){
        this.isExpand = isExpand;
        return this;
    }

    public NiceViewPagerIndicator setIndicatorColor(int indicatorColor){
        this.mIndicatorColor = indicatorColor;
        return this;
    }

    /**
     * 设置未选中的颜色
     * @param colorID
     * @return
     */
    public NiceViewPagerIndicator setNormalTextColor(int colorID){
        this.mNormalTextColor = colorID;
        return this;
    }

    /**
     * 设置选中的颜色
     * @param colorID
     * @return
     */
    public NiceViewPagerIndicator setSelectedTextColor(int colorID){
        this.mSelectedTextColor = colorID;
        return this;
    }

    /**
     * 设置为选中的文字的颜色
     * @param textNormalSp
     * @return
     */
    public NiceViewPagerIndicator setNormalTextSize(int textNormalSp){
        this.mNormalTextColor = textNormalSp;
        return this;
    }

    /**
     * 设置已经选中的文字的颜色
     * @param textSelectedSp
     * @return
     */
    public NiceViewPagerIndicator setSelectedTextSize(int textSelectedSp){
        this.mSelectedTextSize = textSelectedSp;
        return this;
    }

    /**
     * 设置指示器的长度
     * @param indicatorLength
     * @return
     */
    public NiceViewPagerIndicator setIndicatorLength(int indicatorLength){
        this.mIndicatorLength = indicatorLength;
        return this;
    }


    /**
     * ----------------------------------构造方法开始---------------------------------
     */


    public NiceViewPagerIndicator(Context context) {
        this(context,null);
    }

    public NiceViewPagerIndicator(Context context, AttributeSet attrs) {
        this(context,attrs,0);
    }

    public NiceViewPagerIndicator(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        // 初始化ViewPager监听
        mNicePageChangeListener = new NicePageChangeListener();
        mContext = context;
        setFillViewport(true);
        // 允许ViewGroup执行onDraw()执行绘制操作 ---
        setWillNotDraw(false);
    }

    /**
     * ----------------------------------构造方法结束---------------------------------
     */


    /**
     * 属性初始化完成了绑定ViewPager 并执行各项初始化操作
     * @param viewPager
     */
    public void setUpViewPager(@NonNull ViewPager viewPager){
        this.mViewPager = viewPager;
        // 在adapter为空时抛出异常 解决后面报的空指针检查
        if (mViewPager.getAdapter() == null) {
            throw new IllegalStateException("ViewPager does not have adapter instance.");
        }
        viewPager.addOnPageChangeListener(mNicePageChangeListener);
        init();
        initViews();
    }

    /**
     * 进行初始化
     */
    private void init() {
        float density = getResources().getDisplayMetrics().density;
        mSelectedTextSize = (int)(mSelectedTextSize * density);
        mNormalTextSize = (int)(mNormalTextSize * density);
        mTabHorizontalPadding = (int)(mTabHorizontalPadding * density);
        mScrollOffset = (int)(mScrollOffset * density);
        mIndicatorLength =(int)(mIndicatorLength * density);
        mTriangleHeight = (int)(mTriangleHeight * density);
        mTriangleWidth = (int)(mTriangleWidth * density);
        addOnlyContainerChild();
        defTextIndicatorParams();
        initMeasureTextPaints();
        initIndicatorPaints();
        initTrianglePaint();
    }

    private void initTrianglePaint() {
        mTrianglePaint = new Paint();
        mTrianglePaint.setAntiAlias(true);
        mTrianglePaint.setDither(true);
        mTrianglePaint.setColor(Color.WHITE);
        mTrianglePaint.setStyle(Paint.Style.FILL);
    }

    private void initMeasureTextPaints() {
        /*
         * 文字画笔
         */
        mMeasureTextPaint = new Paint();
        mMeasureTextPaint.setAntiAlias(true);
        mMeasureTextPaint.setStrokeWidth(mIndicatorStrokeWidth);
        mMeasureTextPaint.setTextSize(mSelectedTextSize);
    }

    private void initIndicatorPaints() {
        mIndicatorPaint = new Paint();
        mIndicatorPaint.setColor(mIndicatorColor);
        mIndicatorPaint.setStrokeCap(Paint.Cap.ROUND);
        mIndicatorPaint.setStrokeWidth(mIndicatorStrokeWidth);
        mIndicatorPaint.setAntiAlias(true);
        mIndicatorPaint.setDither(true);
        mIndicatorPaint.setStyle(Paint.Style.FILL);
    }


    /**
     * 添加Horizontal 唯一子布局
     */
    private void addOnlyContainerChild() {
        mLinearContainer = new LinearLayout(mContext);
        mLinearContainer.setOrientation(LinearLayout.HORIZONTAL);
        mLinearContainer.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.MATCH_PARENT));
        // 将唯一子布局添加上去
        addView(mLinearContainer);
    }

    /**
     * 创建两个Tab的LayoutParams,wrapTabLayoutParams     ---  为宽度包裹内容,控件较多
     *                          expandTabLayoutParams   ---  宽度等分父控件剩余空间,控件较少
     */
    private void defTextIndicatorParams() {
        //宽度包裹内容
        wrapTabLayoutParams = new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
        //宽度等分 这里用途会更加广泛一点----
        expandTabLayoutParams = new LinearLayout.LayoutParams(0, LayoutParams.MATCH_PARENT, 1.0f);
    }

    private void initViews() {
        mSelectedIndex = mViewPager.getCurrentItem();
        mCurrentIndex = mViewPager.getCurrentItem();
        if (mViewPager.getAdapter() != null) {
            mTabCount = mViewPager.getAdapter().getCount();
        }
        addTextFromViewPager();

        //滚动scrollView
        getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
                    getViewTreeObserver().removeGlobalOnLayoutListener(this);
                } else {
                    getViewTreeObserver().removeOnGlobalLayoutListener(this);
                }
                // 绘制好了完成滑动------
                scrollToChild(mCurrentIndex, 0);
            }
        });
    }

    /**
     * 将从绑定关系的ViewPager的文本属性,添加到linear_container布局中去
     *
     * 跳过编译条件,满足下面的条件
     */
    @SuppressWarnings("ConstantConditions")
    private void addTextFromViewPager(){
        mLinearContainer.removeAllViews();
        for (int i=0 ;i< mTabCount ;i++){
            addTextTab(i, mViewPager.getAdapter().getPageTitle(i).toString());
        }
        updateTextTabStyle();
    }

    /**
     * 按照选中的更新标题式样
     */
    private void updateTextTabStyle() {
        for (int i=0;i<mTabCount;i++){
            TextView textView = (TextView) mLinearContainer.getChildAt(i);
            if ( i == mSelectedIndex){
                // 以px传递
                textView.setTextSize(TypedValue.COMPLEX_UNIT_PX,mSelectedTextSize);
                textView.setTextColor(mSelectedTextColor);
            }else {
                textView.setTextSize(TypedValue.COMPLEX_UNIT_PX,mNormalTextSize);
                textView.setTextColor(mNormalTextColor);
            }
        }
    }

    /**
     * 添加文本Tab
     * @param i
     * @param pageTitle
     */
    private void addTextTab(int i, String pageTitle) {
        TextView textTab = new TextView(mContext);
        textTab.setGravity(Gravity.CENTER);
        textTab.setText(pageTitle);
        // 暂时定为红色
        textTab.setTextColor(mNormalTextColor);
        textTab.setTextSize(mNormalTextSize);
        // 设置左右的Padding
        textTab.setPadding(mTabHorizontalPadding,0,mTabHorizontalPadding,0);
        textTab.setOnClickListener(new TextClickSelectListener(i));
        // 设置文本控件的布局params方式
        mLinearContainer.addView(textTab,isExpand?expandTabLayoutParams:wrapTabLayoutParams);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mIndicatorHeight = h;
        mIndicatorWidth = w;

    }

    /**
     * 绘制指示器 ----- 计算指示器的起点与中点进行绘制
     * @param canvas
     */
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (mTabCount == 0){
            return;
        }

        if (IndicatorShape.TRIANGLE == mIndicatorShape){
            initTriangleLocation();
            canvas.drawPath(mTrianglePath,mTrianglePaint);
            return;
        }

        // 获得控件的高度
        final int height = getHeight();
        float nextLeft;
        float nextRight;
        // 依据文字等长还是tab等长进行效果展示
        switch (mIndicatorType){
            case EQUAL_TAB:
                View currentTab = mLinearContainer.getChildAt(mCurrentIndex);
                mLineLeft = currentTab.getLeft();
                mLineRight = currentTab.getRight();
                if (mCurrentPositionOffset > 0f && mCurrentIndex < mTabCount - 1) {
                    View nextTab = mLinearContainer.getChildAt(mCurrentIndex + 1);
                    nextLeft = nextTab.getLeft();
                    nextRight = nextTab.getRight();
                    mLineLeft = mLineLeft + (nextLeft - mLineLeft) * mCurrentPositionOffset;
                    mLineRight = mLineRight + (nextRight - mLineRight) * mCurrentPositionOffset;
                }
                break;
            case EQUAL_TEXT:
                getTextLocation(mCurrentIndex);
                mLineLeft = textLocation.left;
                mLineRight = textLocation.right;
                if (mCurrentPositionOffset > 0f && mCurrentIndex < mTabCount - 1) {
                    getTextLocation(mCurrentIndex + 1);
                    nextLeft = textLocation.left;
                    nextRight = textLocation.right;
                    mLineLeft = mLineLeft + (nextLeft - mLineLeft) * mCurrentPositionOffset;
                    mLineRight = mLineRight + (nextRight - mLineRight) * mCurrentPositionOffset;
                }
                break;
            case ABSOLUTE_LENGTH:
                getLinearLayoutWithLength(mCurrentIndex);
                mLineLeft = textLocation.left;
                mLineRight = textLocation.right;
                if (mCurrentPositionOffset > 0f && mCurrentIndex < mTabCount-1){
                    getLinearLayoutWithLength(mCurrentIndex + 1);
                    nextLeft = textLocation.left;
                    nextRight = textLocation.right;
                    mLineLeft = mLineLeft + (nextLeft - mLineLeft) * mCurrentPositionOffset;
                    mLineRight = mLineRight + (nextRight - mLineRight) * mCurrentPositionOffset;
                }
                break;
                default:break;
        }
        canvas.drawRect(mLineLeft, height - mIndicatorPaint.getStrokeWidth()/2, mLineRight, height, mIndicatorPaint);
    }


    /*
     * ----------------- 准备绘制白色的三角形指示器 ---------
     */

    private void initTriangleLocation() {
        // 这里有问题嘛
        float centerX = getTriangleCenterX(mCurrentIndex);
        if (mCurrentIndex < mTabCount-1){
            float nextCenterX = getTriangleCenterX(mCurrentIndex + 1);
            centerX = centerX + (nextCenterX - centerX) * mCurrentPositionOffset;
        }
        if (mTrianglePath == null){
            mTrianglePath = new Path();
        }
        mTrianglePath.reset();
        mTrianglePath.moveTo(centerX - mTriangleWidth/2,mIndicatorHeight);
        mTrianglePath.lineTo(centerX + mTriangleWidth/2, mIndicatorHeight);
        mTrianglePath.lineTo(centerX, mIndicatorHeight - mTriangleHeight);
        mTrianglePath.close();
    }

    /**
     * 获得文本的中点
     * @param position
     */
    private float  getTriangleCenterX(int position){
        View child = mLinearContainer.getChildAt(position);
        int left = child.getLeft();
        int width = child.getWidth();
        return left + width/2f;
    }

    /*
     * ------------ 结束绘制白色的三角形指示器 ---------
     */



    /**
     * 获得指定tab中,文字的left和right,线性指示器
     */
    @SuppressWarnings("ConstantConditions")
    private void getTextLocation(int position) {
        View tab = mLinearContainer.getChildAt(position);
        String tabText = mViewPager.getAdapter().getPageTitle(position).toString();
        float textWidth = mMeasureTextPaint.measureText(tabText);
        int tabWidth = tab.getWidth();
        textLocation.left = tab.getLeft() + (int) ((tabWidth - textWidth) / 2);
        textLocation.right = tab.getRight() - (int) ((tabWidth - textWidth) / 2);
    }


    /**
     * 根据定长绘制指示器
     * @param position
     */
    private void getLinearLayoutWithLength(int position){
        View child = mLinearContainer.getChildAt(position);
        int childLeft = child.getLeft();
        int childWidth = child.getWidth();
        textLocation.left = (int)(childLeft + childWidth / 2f - mIndicatorLength/2f);
        textLocation.right = (int)(childLeft + childWidth / 2f + mIndicatorLength/2f);
    }

    private LeftRight textLocation = new LeftRight();

    class LeftRight {
        int left, right;
    }


    /**
     * 滑动HorizontalScrollView 滚动方法
     * @param position          标示下标ID
     * @param currentTextLength 当前文本长度
     * 比较关键 ---- 执行scrollView的滑动效果
     *
     *  这是很清除的,当水平方向滑动,值为正向左滑动
     */
    private void scrollToChild(int position, int currentTextLength) {
        // 如果文本长度为空,或者平分模式则返回
        if (mTabCount == 0 || isExpand){
            return;
        }
        //getLeft():tab相对于父控件,即tabsContainer的left
        View child = mLinearContainer.getChildAt(position);
        int newScrollX = child.getLeft() + currentTextLength ;

        //附加一个偏移量,防止当前选中的tab太偏左
        //可以去掉看看是什么效果

        // 相当于在原来基础上向右滑 mScrollOffset的距离,就达到了太左或者太有可以往中间靠的效果
        if (position > 0 || currentTextLength > 0) {
            newScrollX -= mScrollOffset;
        }

        if (newScrollX != lastScrollX) {
            lastScrollX = newScrollX;
            scrollTo(newScrollX, 0);
        }
    }


    /**
     * 重写onClickListener,为了能够安全的将参数传递进来
     */
    private class TextClickSelectListener implements View.OnClickListener{
        private  int selectedIndex;
        TextClickSelectListener(int selectedIndex) {
            this.selectedIndex = selectedIndex;
        }
        @Override
        public void onClick(View v) {
            // 设置文本的选中----
            if (mViewPager != null){
                mViewPager.setCurrentItem(selectedIndex);
            }
        }
    }


    /**
     * 重写ViewPager的onPageChangeListener 目的在于监听滑动状态,进行绑定
     */
    private class NicePageChangeListener implements ViewPager.OnPageChangeListener{

        @Override
        public void onPageScrolled(int i, float v, int i1) {
            mCurrentIndex = i;
            mCurrentPositionOffset = v;
            //HorizontalScrollView滚动
            scrollToChild(i, (int) (v * mLinearContainer.getChildAt(i).getWidth()));
            //invalidate后onDraw会被调用,绘制指示器
            invalidate();
        }

        @Override
        public void onPageSelected(int i) {
            mSelectedIndex = i;
            updateTextTabStyle();
        }

        @Override
        public void onPageScrollStateChanged(int i) {

        }
    }

}

使用方法:

   indicator = (NiceViewPagerIndicator) findViewById(R.id.niceIndicator2);
        indicator.setIndicatorLengthType(NiceViewPagerIndicator.IndicatorType.EQUAL_TEXT)
                .setIndicatorShapeType(NiceViewPagerIndicator.IndicatorShape.LINEAR)
                .setIndicatorColor(Color.BLUE);
        indicator.setUpViewPager(mUsbViewPager);

        mUsbViewPager.addOnPageChangeListener(this);
        ```
 类似资料: