指示器在APP应用中最为常见,例如新手引导页,滑动页,轮播图等等,在这些界面的指示器中圆形指示器最为常见。圆形指示器在网上一搜也很多,但是很多用着都不太方便,耦合性太强,一些设置往往不符合要求,使用起来比较麻烦。在之前也写过一个自定义控件,也是指示器,这个指示器其也很优美,但是代码耦合性比较强,有兴趣的同学可以看看,点击打开链接。鉴于此准备打造一个万能的轻量级圆形指示器,一行代码就可以实现指示器,同时也可以设置一些常用的属性来制定自己的需求。效果图如下:
使用
在使用的时候在xml中引入布局路径,在Activity中直接和ViewPager进行绑定,使用下面的一行代码即可实现上图的指示器功能:
((CircleIndicatorView)findViewById(R.id.indicator)).setUpWithViewPager(mViewPager);
实现原理
既然是自定义控件,那么离不开自定义控件的几个核心方法:onMeasure,onLayout,onDraw,指示器的自定义也是围绕着这些方法而展开的。首先自定义一些属性,来设置指示器的属性,如半径,间距,颜色等;其次自定义一个类,继承自View,在View中加载相关属性,进行初始化操作;然后我在重写一些View的核心方法,进行指示器参数设置和绘制;最后把View和当前的ViewPager进行绑定,使其联动。接下来介绍具体实现步骤。
一、自定义属性
在圆形指示器中常见的属性有默认颜色,选中颜色,半径,间距。因就定义这四大属性。在res/values文件夹下创建attrs.xml文件夹,创建CircleIndicatorView属性。
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="CircleIndicatorView">
<attr name="indicatorRadius" format="dimension"/>
<attr name="indicatorDistance" format="dimension"/>
<attr name="indicatorColor" format="color"/>
<attr name="indicatorSelectColor" format="color"/>
</declare-styleable>
</resources>
二、View的初始化
1、写一个类CircleIndicatorView继承自View,重写他的构造方法:
public CircleIndicatorView(Context context) {
this(context, null);
}
public CircleIndicatorView(Context context, AttributeSet attrs) {
this(context, attrs, -1);
}
public CircleIndicatorView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
getAttr(context, attrs);
init();
}
2、初始化自定义属性
这里初始化属性需要借助于Context context, AttributeSet attrs两个参数,AttributeSet里面封装了自定义属性的一些内容。
private void getAttr(Context context, AttributeSet attrs) {
TypedArray attributes = context.obtainStyledAttributes(attrs, R.styleable.CircleIndicatorView);
radius = attributes.getDimension(R.styleable.CircleIndicatorView_indicatorRadius, DisplayUtils.dpToPx(5));
distance = attributes.getDimension(R.styleable.CircleIndicatorView_indicatorDistance, DisplayUtils.dpToPx(8));
defaultColor = attributes.getColor(R.styleable.CircleIndicatorView_indicatorColor, defaultColor);
defaultSelectColor = attributes.getColor(R.styleable.CircleIndicatorView_indicatorSelectColor, defaultSelectColor);
attributes.recycle(); //进行变量缓存,每次加载从缓存数据里面读取
}
obtainStyledAttributes:是用来加载自定义属性的布局文件,返回的是TypedArray类型的对象
getDimension:用来获取自定义的属性的大小尺寸,参数一为定义的属性名称,参数二为默认设置的尺寸大小
getColor:获取自定义颜色类型的数据,参数一为定义的属性名称,参数二为默认设置的颜色
3、初始化画笔
初始化自定义属性之后,接下来需要初始化画笔了,设置画笔的一些参数,用来绘制圆形指示器的每个小圆点
private void init() {
//创建画笔
paint = new Paint();
//设置抗锯齿
paint.setAntiAlias(true);
//设置绘制防抖
paint.setDither(true);
//设置绘制模式:内部填充:FILL:填充内部,FILL_AND_STROKE:填充内部和描边,STROKE:描边
paint.setStyle(Paint.Style.FILL);
//创建集合添加指示器
indicatorList = new ArrayList<>();
}
每个属性的含义都有详细的说明,这里不用多解释。有人可能会疑问问什么不在这里设置颜色呢?如果在这里设置颜色该设置成选中的还是默认的颜色?所以这里设置颜色并不合适,应该在后面选中的状态逻辑判断中,动态设置颜色。
三、onMeasure
onMeasuer方法是自定义指示器的核心方法之一,他的主要作用是测量指示器的每个小圆点的绘制位置,因此需要重写onMeasure方法:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
1、获取指示器的总宽度。
指示器的宽度是指:每个小圆点的宽度x小圆点的个数+两个小圆点之间距离x(小圆点的个数-1)
int width = (int) (radius * count * 2 + distance * (count - 1));
2、获取指示器的高度
指示器的高度:小圆点的半径x2
int height = (int) radius * 2;
3、设置绘制区域的大小
setMeasuredDimension是设置测量宽高数据的一个方法,接收两个参数分别是宽高。在父类的onMeasure里面可以看到他最终也是调用setMeasuredDimension方法来设置测量区域的大小的。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
4、封装指示器小圆点的参数
确定好绘制区域的大小,接下来就需要绘制指示器的每个小圆点,每个小圆点绘制需要一个参考位置,因此封装一个circle对象,里面包含绘制点的宽高
public static class circle {
public float x; // 圆心x坐标
public float y; // 圆心y 坐标
}
5、遍历小圆点的个数,确定绘制位置在遍历小圆点个数前需要清理掉indicatorList指示器集合,数据为空;在 遍历每个小圆点的位置之后,需要把每个小圆点参数对象添加到集合里面,以便于绘制小圆点时候每次从中取出。再遍历小圆点个数时候需要确定每个小圆点的坐标值。对于x方向上,随着小圆点的增加每次都是变化的,y轴方法,都是固定的,在y轴水平方向上不变。计算方式如下:
x轴:上一次绘制的距离+半径x2+小圆点之间的距离
x += radius * 2 + distance;
y轴:y周的值为固定的,不受每次x周小圆点的增加而变化,等于半径的长度
circle.y = getMeasuredHeight() / 2;
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = (int) (radius * count * 2 + distance * (count - 1));
int height = (int) radius * 2;
setMeasuredDimension(width, height);
indicatorList.clear();
float x = 0;
for (int i = 0; i < count; i++) {
circle circle = new circle();
if (i == 0) {
x = radius;
} else {
x += radius * 2 + distance;
}
circle.x = x;
circle.y = getMeasuredHeight() / 2;
indicatorList.add(circle);
}
}
四、onDraw
完成了测量之后,接下来进行绘制过程,和onMeasure过程一样,需要重写onDraw方法,进行指示器的绘制:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
}
在onDraw方法里面提供一个canvas类,我们可以借助于这个画布进行小圆点的绘制,绘制方法如下:
canvas.drawCircle(x, y, radius, paint);
x:表示x轴位置
y:表示y周位置
radius:表是绘制的半径
paint:表示绘制的颜色
这里我们只需要遍历上一步测量的小圆点的集合indicatorList,然后从中拿去每一个元素的x,y坐标值进行图像绘制。在初始化画笔的时候说到不能再那里设置颜色,现在可以在onDraw方法里面对画笔进行设置颜色值,这样就很灵活。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
for (int i = 0; i < indicatorList.size(); i++) {
circle circle = indicatorList.get(i);
float x = circle.x;
float y = circle.y;
if (position == i) {
paint.setColor(defaultSelectColor);
} else {
paint.setColor(defaultColor);
}
canvas.drawCircle(x, y, radius, paint);
}
}
五、指示器和ViewPager进行关联
经过以上四个步骤就可以定义出一个指示器了,但是它还是零散的,不能和ViewPager进行联动。如何做到联动?这里让定义的当前类CircleIndicatorView实现ViewPager.OnPageChangeListene接口。然后在setSelectPosition方法里面设置选中的位置,接着进行重绘,这样就会重新调用onDraw方法,在onDraw可以动态设置当前位置选中时候的颜色以及其他位置的颜色。
@Override
public void onPageSelected(int position) {
setSelectPosition(position);
}
public void setSelectPosition(int selectPosition) {
this.position = selectPosition;
invalidate();
}
还有最后一步就是对ViewPager进行初始化的操作:
public void setUpWithViewPager(ViewPager viewPager) {
if (viewPager == null) {
return;
}
viewPager.removeOnPageChangeListener(this);
viewPager.addOnPageChangeListener(this);
int count = viewPager.getAdapter().getCount();
setCount(count);
}
public void setCount(int count) {
this.count = count;
}
这样指示器Indictor就和ViewPager进行了完全的绑定。
六、其他属性设置
1、XML中属性配置
app:indicatorSelectColor="#f00";//设置选中颜色
app:indicatorColor="#fff" ; //设置默认颜色
app:indicatorDistance="8dp";//设置小圆点间间距
app:indicatorRadius="6dp" ; //设置小圆点半径
2、代码中属性配置
在刚刚开始时候自定义了一些属性,他可以直接写在XML布局文件中,这里为了灵活期间,在设定一些方法,用于在代码里面进行控制圆形指示器的属性,如下:
/**
* 设置小圆点数量
* @param count
*/
public void setCount(int count) {
this.count = count;
}
/**
* 设置选中指示器的颜色
* @param selectColor
*/
public void setSelectColor(int selectColor) {
this.defaultSelectColor = selectColor;
}
/**
* 设置指示器默认颜色
* @param defaultColor
*/
public void setDefaultColor(int defaultColor) {
this.defaultColor = defaultColor;
}
/**
* 设置选中的位置
* @param selectPosition
*/
public void setSelectPosition(int selectPosition) {
this.position = selectPosition;
invalidate();
}
/**
* 设置Indicator 半径
* @param radius
*/
public void setRadius(int radius) {
this.radius = radius;
}
/**
* 设置小圆点之间的距离
* @param distance
*/
public void setDistance(int distance) {
this.distance = distance;
}