自定义ViewPager页面指示器(导航索引)


Android开发中,我们经常会用到viewpager这种滑动的控件,来让用户有更好的体验,一般的viewpager都会有一个绑定的页面指示器,那么这个页面指示器是怎么做的呢,方法比较简单,我们可以自己定义一个view来实现它。

项目源码下载

1、效果图:

这里写图片描述

这里写图片描述

这里写图片描述

2、分析代码:

自定义的IndicatorView.java:

/**
 * @author  xpd3581 
 * @date 2016-10-30 
 * @version 1.0
 */
package com.xingpidong.demo_viewpager_indicator;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
import android.support.v4.view.PagerAdapter;
import android.support.v4.view.ViewPager;
import android.support.v4.view.ViewPager.OnPageChangeListener;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.View;

import com.example.demo_viewpager_indicator.R;

/**
 * IndicatorView 2016-10-30
 */
public class IndicatorView extends View {

    // 指示器图标,这里是一个 drawable,包含两种状态,
    // 选中和非选中状态
    private Drawable mIndicator;

    // 指示器图标的大小,根据图标的宽和高来确定,选取较大者
    private int mIndicatorSize;

    // 整个指示器控件的宽度
    private int mWidth;

    /* 图标加空格在家 padding 的宽度 */
    private int mContextWidth;

    // 指示器图标的个数,就是当前ViwPager 的 item 个数
    private int mCount;
    /* 每个指示器之间的间隔大小 */
    private int mMargin;
    /* 当前 view 的 item,主要作用,是用于判断当前指示器的选中情况 */
    private int mSelectItem;
    /* 指示器根据ViewPager 滑动的偏移量 */
    private float mOffset;
    /* 指示器是否实时刷新 */
    private boolean mSmooth;

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

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

    public IndicatorView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        // 通过 TypedArray 获取自定义属性
        TypedArray typedArray = getResources().obtainAttributes(attrs,
                R.styleable.IndicatorView);
        // 获取自定义属性的个数
        int N = typedArray.getIndexCount();
        for (int i = 0; i < N; i++) {
            int attr = typedArray.getIndex(i);
            switch (attr) {
            case R.styleable.IndicatorView_indicator_icon:
                // 通过自定义属性拿到指示器
                mIndicator = typedArray.getDrawable(attr);
                break;
            case R.styleable.IndicatorView_indicator_margin:
                float defaultMargin = (int) TypedValue.applyDimension(
                        TypedValue.COMPLEX_UNIT_DIP, 5, getResources()
                                .getDisplayMetrics());
                mMargin = (int) typedArray.getDimension(attr, defaultMargin);
                break;
            case R.styleable.IndicatorView_indicator_smooth:
                mSmooth = typedArray.getBoolean(attr, false);
                break;
            }
        }
        // 使用完成之后记得回收
        typedArray.recycle();
        initIndicator();
    }

    private void initIndicator() {
        // 获取指示器的大小值。一般情况下是正方形的,也是时,你的美工手抖了一下,切出一个长方形来了,
        // 不用怕,这里做了处理不会变形的
        mIndicatorSize = Math.max(mIndicator.getIntrinsicWidth(),
                mIndicator.getIntrinsicHeight());
        /* 设置指示器的边框 */
        mIndicator.setBounds(0, 0, mIndicator.getIntrinsicWidth(),
                mIndicator.getIntrinsicWidth());
    }

    /**
     * 测量View 的大小
     * 
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(measureWidth(widthMeasureSpec),
                measureHeight(heightMeasureSpec));
    }

    /**
     * 测量宽度,计算当前View 的宽度
     * 
     * @param widthMeasureSpec
     * @return
     */
    private int measureWidth(int widthMeasureSpec) {
        int mode = MeasureSpec.getMode(widthMeasureSpec);
        int size = MeasureSpec.getSize(widthMeasureSpec);
        int width;
        int desired = getPaddingLeft() + getPaddingRight() + mIndicatorSize
                * mCount + mMargin * (mCount - 1);
        mContextWidth = desired;
        if (mode == MeasureSpec.EXACTLY) {
            width = Math.max(desired, size);
        } else {
            if (mode == MeasureSpec.AT_MOST) {
                width = Math.min(desired, size);
            } else {
                width = desired;
            }
        }
        mWidth = width;
        return width;
    }

    private int measureHeight(int heightMeasureSpec) {
        int mode = MeasureSpec.getMode(heightMeasureSpec);
        int size = MeasureSpec.getSize(heightMeasureSpec);
        int height;
        if (mode == MeasureSpec.EXACTLY) {
            height = size;
        } else {
            int desired = getPaddingTop() + getPaddingBottom() + mIndicatorSize;
            if (mode == MeasureSpec.AT_MOST) {
                height = Math.min(desired, size);
            } else {
                height = desired;
            }
        }

        return height;
    }

    /**
     * 绘制指示器
     * 
     * @param canvas
     */
    @Override
    protected void onDraw(Canvas canvas) {

        /*
         * 首先得保存画布的当前状态,如果位置行这个方法 等一下的 restore()将会失效,canvas 不知道恢复到什么状态 所以这个
         * save、restore 都是成对出现的,这样就很好理解了。
         */
        canvas.save();
        /*
         * 这里开始就是计算需要绘制的位置, 如果不好理解,请按照我说的做,拿起 附近的纸和笔,在纸上绘制一下,然后 你就一目了然了,
         */
        int left = mWidth / 2 - mContextWidth / 2 + getPaddingLeft();
        canvas.translate(left, getPaddingTop());
        for (int i = 0; i < mCount; i++) {
            /*
             * 这里也需要解释一下, 因为我们额 drawable 是一个selector 文件 所以我们需要设置他的状态,也就是 state
             * 来获取相应的图片。 这里是获取未选中的图片
             */
            mIndicator.setState(EMPTY_STATE_SET);
            /* 绘制 drawable */
            mIndicator.draw(canvas);
            /* 每绘制一个指示器,向右移动一次 */
            canvas.translate(mIndicatorSize + mMargin, 0);
        }
        /*
         * 恢复画布的所有设置,也不是所有的啦, 根据 google 说法,就是matrix/clip 只能恢复到最后调用 save 方法的位置。
         */
        canvas.restore();
        /* 这里又开始计算绘制的位置了 */
        float leftDraw = (mIndicatorSize + mMargin) * (mSelectItem + mOffset);
        /*
         * 计算完了,又来了,平移,为什么要平移两次呢? 也是为了好理解。
         */
        canvas.translate(left, getPaddingTop());
        canvas.translate(leftDraw, 0);
        /*
         * 把Drawable 的状态设为已选中状态 这样获取到的Drawable 就是已选中 的那张图片。
         */
        mIndicator.setState(SELECTED_STATE_SET);
        /* 这里又开始绘图了 */
        mIndicator.draw(canvas);

    }

    /**
     * 此ViewPager 一定是先设置了Adapter, 并且Adapter 需要所有数据,后续还不能 修改数据
     * 
     * @param viewPager
     */
    public void setViewPager(ViewPager viewPager,
            OnPageChangeListener mPageChangeListener) {
        if (viewPager == null) {
            return;
        }
        PagerAdapter pagerAdapter = (PagerAdapter) viewPager.getAdapter();
        if (pagerAdapter == null) {
            throw new RuntimeException("pagerAdapter = null");
        }
        mCount = pagerAdapter.getCount();
        viewPager.setOnPageChangeListener(mPageChangeListener);
        mSelectItem = viewPager.getCurrentItem();

        invalidate();
    }

    public Drawable getmIndicator() {
        return mIndicator;
    }

    public void setmIndicator(Drawable mIndicator) {
        this.mIndicator = mIndicator;
    }

    public int getmIndicatorSize() {
        return mIndicatorSize;
    }

    public void setmIndicatorSize(int mIndicatorSize) {
        this.mIndicatorSize = mIndicatorSize;
    }

    public int getmWidth() {
        return mWidth;
    }

    public void setmWidth(int mWidth) {
        this.mWidth = mWidth;
    }

    public int getmContextWidth() {
        return mContextWidth;
    }

    public void setmContextWidth(int mContextWidth) {
        this.mContextWidth = mContextWidth;
    }

    public int getmCount() {
        return mCount;
    }

    public void setmCount(int mCount) {
        this.mCount = mCount;
    }

    public int getmMargin() {
        return mMargin;
    }

    public void setmMargin(int mMargin) {
        this.mMargin = mMargin;
    }

    public int getmSelectItem() {
        return mSelectItem;
    }

    public void setmSelectItem(int mSelectItem) {
        this.mSelectItem = mSelectItem;
    }

    public float getmOffset() {
        return mOffset;
    }

    public void setmOffset(float mOffset) {
        this.mOffset = mOffset;
    }

    public boolean ismSmooth() {
        return mSmooth;
    }

    public void setmSmooth(boolean mSmooth) {
        this.mSmooth = mSmooth;
    }

}

讲一下public IndicatorView(Context context, AttributeSet attrs, int
defStyleAttr)

这个构造函数,如果你想自定义属性,那么必须通过将xml文件和你的view绑定到一起,这个构造函数就是做的这样的事。

它是怎么做的呢,来看一下:

  • getResources().obtainAttributes(attrs,R.styleable.IndicatorView)

    这句话就引用了自定义属性集IndicatorView,那么它是怎么写的,存放在哪里?

看图:

这里写图片描述

res/values文件夹下面新建一个attrs的资源文件,Android编译器就可以根据attrs找到你的自定义属性,并用Xml的pullParser解析到java代码中从而供我们使用。

    <attr name="indicator_icon" format="integer" />
    <attr name="indicator_margin" format="dimension" />
    <attr name="indicator_smooth" format="boolean" />
  • 第一行的iconformat类型,指定的是integer类型,就是你项目当中R.java中的int值,这里定义icon是图标,也就可以为它指定一个drawable图片,可以是一张真实的图片,也可以是自己定义的shape的图形。

  • 第二行是dimension,就是间距、距离、尺寸的意思,他会去调用res/values下的dimens.xml文件,你可以在那里写好尺寸来调用。

  • 第三行是boolean,这个就不说了,就像开关键一样。

现在,我们的自定义IndicatorView和自定义属性都写完了,那么接下来就是去调用了:

来看activity_main.xml:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:customAttr="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#8888"
    tools:context="com.xingpidong.demo_viewpager_indicator.MainActivity" >

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_marginBottom="50dp"
        android:background="#ff0f"
        android:paddingBottom="20dp" >

        <android.support.v4.view.ViewPager
            android:id="@+id/mViewPager"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />

        <com.xingpidong.demo_viewpager_indicator.IndicatorView
            android:id="@+id/mIndicatorView1"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_alignParentBottom="true"
            android:layout_centerHorizontal="true"
            customAttr:indicator_icon="@drawable/indicator_selector"
            customAttr:indicator_margin="3dp"
            customAttr:indicator_smooth="true" />

        <com.xingpidong.demo_viewpager_indicator.IndicatorView
            android:id="@+id/mIndicatorView3"
            android:layout_width="match_parent"
            android:layout_marginTop="20dp"
            android:layout_height="wrap_content"
            android:layout_alignParentTop="true"
            android:layout_centerHorizontal="true"
            customAttr:indicator_icon="@drawable/my_indicator_selector"
            customAttr:indicator_margin="3dp"
            customAttr:indicator_smooth="true" />
    </RelativeLayout>

    <com.xingpidong.demo_viewpager_indicator.IndicatorView
        android:id="@+id/mIndicatorView2"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_centerHorizontal="true"
        android:layout_marginBottom="20dp"
        customAttr:indicator_icon="@drawable/indicator_selector"
        customAttr:indicator_margin="8dp"
        customAttr:indicator_smooth="false" />

</RelativeLayout>

其中的根布局中有一句话:

    xmlns:customAttr="http://schemas.android.com/apk/res-auto"

customAttr 是自己定义的,你写什么名字都可以,这个名字就是你对控件添加属性的前缀标签,就像 android:focusable="true" 当中的android 标签一样。

添加自定义属性:

customAttr:indicator_icon="@drawable/indicator_selector"
customAttr:indicator_margin="3dp"
customAttr:indicator_smooth="true"

使用起来就这么简单。

再看主界面的代码MainActivity.java:

package com.xingpidong.demo_viewpager_indicator;

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

import android.app.Activity;
import android.os.Bundle;
import android.support.v4.view.PagerAdapter;
import android.support.v4.view.ViewPager;
import android.support.v4.view.ViewPager.OnPageChangeListener;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import com.example.demo_viewpager_indicator.R;

/**
 * xpd 2016-11-3
 */
public class MainActivity extends Activity {

    private ViewPager mViewPager;
    private MyAdapter mPagerAdapter;
    private OnPageChangeListener mOnPageChangeListener;
    private IndicatorView mIndicatorView1;
    private IndicatorView mIndicatorView2;
    private IndicatorView mIndicatorView3;
    private List<View> mList;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mViewPager = (ViewPager) this.findViewById(R.id.mViewPager);
        mIndicatorView1 = (IndicatorView) this
                .findViewById(R.id.mIndicatorView1);
        mIndicatorView2 = (IndicatorView) this
                .findViewById(R.id.mIndicatorView2);
        mIndicatorView3 = (IndicatorView) this
                .findViewById(R.id.mIndicatorView3);

        setUpPageChangeListener();

        setDatas();

        setUpViewPager();
    }

    /**
     * new 一个pagechangelistener
     */
    public void setUpPageChangeListener() {
        mOnPageChangeListener = new OnPageChangeListener() {

            @Override
            public void onPageSelected(int pageIndex) {

                mIndicatorView1.setmSelectItem(pageIndex);
                mIndicatorView1.invalidate();

                mIndicatorView2.setmSelectItem(pageIndex);
                mIndicatorView2.invalidate();

                mIndicatorView3.setmSelectItem(pageIndex);
                mIndicatorView3.invalidate();
            }

            @Override
            public void onPageScrolled(int position, float positionOffset,
                    int positionOffsetPixels) {
                if (mIndicatorView1.ismSmooth()) {
                    mIndicatorView1.setmSelectItem(position);
                    mIndicatorView1.setmOffset(positionOffset);
                    mIndicatorView1.invalidate();
                }

                if (mIndicatorView2.ismSmooth()) {
                    mIndicatorView2.setmSelectItem(position);
                    mIndicatorView2.setmOffset(positionOffset);
                    mIndicatorView2.invalidate();
                }

                if (mIndicatorView3.ismSmooth()) {
                    mIndicatorView3.setmSelectItem(position);
                    mIndicatorView3.setmOffset(positionOffset);
                    mIndicatorView3.invalidate();
                }
            }

            @Override
            public void onPageScrollStateChanged(int arg0) {

            }
        };
    }

    /**
     * 初始化数据集合
     */
    private void setDatas() {
        mList = new ArrayList<View>();
        for (int i = 0; i < 5; i++) {
            TextView view = new TextView(this);
            view.setText("这是第 " + (i + 1) + " 页!");
            view.setGravity(Gravity.CENTER);
            mList.add(view);
        }
    }

    /**
     * viewpager设置adapter,和页面指示器
     */
    private void setUpViewPager() {
        mPagerAdapter = new MyAdapter(mList);
        mViewPager.setAdapter(mPagerAdapter);

        mIndicatorView1.setViewPager(mViewPager, mOnPageChangeListener);
        mIndicatorView2.setViewPager(mViewPager, mOnPageChangeListener);
        mIndicatorView3.setViewPager(mViewPager, mOnPageChangeListener);
    }

    class MyAdapter extends PagerAdapter {

        List<View> mDatas;

        public MyAdapter(List<View> list) {
            this.mDatas = list;
        }

        @Override
        public int getCount() {
            return null == mDatas ? 0 : mDatas.size();
        }

        @Override
        public boolean isViewFromObject(View arg0, Object arg1) {
            return arg0 == arg1;
        }

        @Override
        public void destroyItem(ViewGroup container, int position, Object object) {
            container.removeView(mDatas.get(position));
        }

        @Override
        public Object instantiateItem(ViewGroup container, int position) {
            container.addView(mDatas.get(position));
            return mDatas.get(position);
        }

    }

}

indicator_selector.xml:

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android" >
    <item android:state_selected="true" android:drawable="@drawable/white_indicator"></item>
    <item android:state_selected="false" android:drawable="@drawable/gray_indicator"></item>
</selector>

my_indicator_selector.xml:

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android" >
    <item android:state_selected="true" android:drawable="@drawable/my_selected_circle"></item>
    <item android:state_selected="false" android:drawable="@drawable/my_unselected_circle"></item>

</selector>

my_selected_circle.xml:

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" >

    <solid android:color="#fff0" />

    <corners android:radius="5dp" />

    <size android:width="10dp" android:height="10dp"/>

    <stroke android:color="#fef2"/>

</shape>

my_unselected_circle.xml:

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" >

<solid android:color="#000" />

<corners android:radius="5dp" />

<size android:width="10dp" android:height="10dp"/>

</shape>

剩下的两个,white_indicatorgray_indicator就是两张小图标了,是美工妹妹提供的,我们也可以自己用ps简单的制作一下。

好了,讲到这里,我们的ViewPager页面指示器就讲完了,如果你觉得本文对你有帮助,不要忘了为博主点赞哦。(^__^) 嘻嘻……

项目源码下载

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值