自定義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頁面指示器就講完了,如果你覺得本文對你有幫助,不要忘了爲博主點贊哦。(^__^) 嘻嘻……

項目源碼下載

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章