在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" />
第一行的
icon
的format
类型,指定的是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_indicator
和gray_indicator
就是两张小图标了,是美工妹妹提供的,我们也可以自己用ps简单的制作一下。
好了,讲到这里,我们的ViewPager页面指示器就讲完了,如果你觉得本文对你有帮助,不要忘了为博主点赞哦。(^__^) 嘻嘻……