前言
其实指示器的自定义控件太多了,但是需求时刻在变,总有不满足的时候,所以就得自己来绘制
因为博主遇到了横线形式的指示器,所以特地分享一下,同时也教一下不会自定义的童鞋
效果图
可以看到可以和ViewPager一起联动,下面就写出实现的过程
首先我们需要弄明白几个点
1. 绘制每一个指示器通过canvas的绘制圆角矩形就行
2. 每一个指示器Item都需要一个Rect对象来描述绘制的位置
3. 和ViewPager联动需要监听ViewPager的滑动事件就能实现
大致实现流程
1. 测量出自身的宽高
2. 根据各个数值算出每一个指示器的Item的位置
3. 根据位置绘制出所有的指示器Item
代码实现
准备:绘制此控件需要的变量属性
/**
* 指示器的宽度,单位是dp
*/
private int indicatorWidth = 10;
/**
* 指示器的高度,单位是dp
*/
private int indicatorHeight = 4;
/**
* 被选中的指示器的宽度,单位是dp
*/
private int selectedIndicatorWidth = 20;
/**
* 被选中的指示器的高度,单位是dp
*/
private int selectedIndicatorHeight = 4;
/**
* 每一个指示器之间的间距,单位是dp
*/
private int indicatorHorizontalSpace = 2;
/**
* 被选中的指示器的颜色
*/
private int selectedIndicatorColor = Color.parseColor("#ED5C55");
/**
* 没被选中的指示器的颜色
*/
private int unSelectedIndicatorColor = Color.WHITE;
/**
* 指示器的个数
*/
private int indicatorCount = 0;
/**
* 当前选中的指示器下标
*/
private int indicatorIndex = 0;
/**
* 偏移的比例,只有>0的,因为ViewPager中如果从0滑动到1,那么这个值就是这么变化的:
* 0->0.999->0
* 下标变化:
* 0->0->1
* 如果从1滑动到0,那么这个值就是这么变化的:
* 0->0.999->0
* 下标变化:
* 1->0->0
*/
private float offSet = 0f;
上述变量中都不需要解释,就最后一个offSet需要解释一下,因为我们肉眼看上去是和ViewPager联动的,但是对于此控件来说,是不断的绘制产生的效果,所以这个变量是记录从
index -> index + 1 或者 index -> index - 1 的时候的进度,所以才能在ViewPager联动的时候改变此值来改变计算出来的位置,下面会用到
2.测量自身的宽高
由于我们此控件基本全部都是包裹的效果,所以这里只处理包裹的效果,无论你写wrap_content或者match_parent都是包裹作用,是一样的,使用的时候注意了哦,支持内边距!
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// calculate width of no selected index
int width = indicatorCount * indicatorWidth + (indicatorCount - 1) * indicatorHorizontalSpace;
// get the bigger height
int height = Math.max(indicatorHeight, selectedIndicatorHeight);
// if have selected indicator
if (indicatorIndex >= 0 && indicatorIndex < indicatorCount) {
width += selectedIndicatorWidth - indicatorWidth;
}
// add the padding
width += getPaddingLeft() + getPaddingRight();
height += getPaddingTop() + getPaddingBottom();
// only have wrap
// save the width and height
setMeasuredDimension(width, height);
}
测量自己的宽和高,因为允许没选中和选中的指示器有不同的高度,所以在高度方面是采取两者的较大值,因为在效果上我们可以看见选中的那个明显比较高
另外测量的时候,我们绘制的区域算出来之后,加上了内边距的值,这也就支持了内边距的设置效果
3.根据所有可影响的属性值计算出绘制的坐标
/**
* 记录所有指示器的位置信息,包括选中的那个
*/
private void calculateRectFs() {
if (rectFs == null || rectFs.length != indicatorCount) {
rectFs = new RectF[indicatorCount];
}
int startLeft = getPaddingLeft();
int startTop = getPaddingTop();
int startBottom = getHeight() - getPaddingBottom();
//除去内边距可绘制的区域的高度
int drawHeight = getHeight() - getPaddingTop() - getPaddingBottom();
/* 绘制的时候,因为绘制的指示器有高度不同的情况
所以如果高度小一点的,那么就有一个绘制的top起点就有一个dy需要加上去
才能保证绘制所有的指示器都在中间*/
int dy;
int currentIndicatorWidth = indicatorWidth;
// 循环计算
for (int i = 0; i < indicatorCount; i++) {
//如果是选中的那个指示器
if (i == indicatorIndex) {
dy = (drawHeight - selectedIndicatorHeight) / 2;
currentIndicatorWidth = selectedIndicatorWidth;
} else {
dy = (drawHeight - indicatorHeight) / 2;
currentIndicatorWidth = indicatorWidth;
}
// 创建矩形,填入数据
RectF rectF = new RectF(startLeft, startTop + dy,
startLeft + currentIndicatorWidth, startBottom - dy);
startLeft += currentIndicatorWidth + indicatorHorizontalSpace;
rectFs[i] = rectF;
}
// 这是就是根据偏移的百分比,在正常基础上左右偏移坐标
// 但是这里不针对选择的下标是最后一个的情况
if (indicatorIndex != indicatorCount - 1) {
// 拿到选中那个位置信息和选中的下一个的位置信息
RectF rectFSelected = rectFs[indicatorIndex];
RectF next = rectFs[indicatorIndex + 1];
// 计算两者的偏移量
float selectedOffsetPx = (next.right - rectFSelected.right) * offSet;
float nextOffsetPx = (rectFSelected.left - next.left) * offSet;
// 在原来的基础上加上偏移量
rectFSelected.left += selectedOffsetPx;
rectFSelected.right += selectedOffsetPx;
next.left += nextOffsetPx;
next.right += nextOffsetPx;
}
}
在测量的时候我们在自己绘制的宽高基础上加上了内边距
所以这里计算的时候,也需要考虑内边距的问题,所以绘制的时候起点不是0
int startLeft = getPaddingLeft();
int startTop = getPaddingTop();
int startBottom = getHeight() - getPaddingBottom();
也就是这三句代码,然后接下来的代码都写了注释应该是没问题的,总得一句话就是根据所有可影响的属性计算出每一个指示器Item的绘制区域
4.根据计算出来的坐标数据,绘制出效果
@Override
protected void onDraw(Canvas c) {
super.onDraw(c);
calculateRectFs();
//绘制的起点
Paint p = new Paint();
p.setColor(Color.GREEN);
p.setAntiAlias(true);// 设置画笔的锯齿效果
if (rectFs == null) {
return;
}
p.setColor(unSelectedIndicatorColor);
for (int i = 0; i < rectFs.length; i++) {
//不绘制选中的
if (indicatorIndex == i) {
continue;
}
//拿到位置信息
RectF r = rectFs[i];
//计算出半径
float radius = Math.min(Math.abs((r.bottom - r.top) / 2), Math.abs((r.right - r.left) / 2));
//绘制圆角矩形
c.drawRoundRect(r, radius, radius, p);
}
if (indicatorIndex >= 0 && indicatorIndex < indicatorCount) {
p.setColor(selectedIndicatorColor);
//拿到位置信息
RectF r = rectFs[indicatorIndex];
//计算出半径
float radius = Math.min(Math.abs((r.bottom - r.top) / 2), Math.abs((r.right - r.left) / 2));
//绘制圆角矩形
c.drawRoundRect(r, radius, radius, p);
}
}
这段代码就很简单了,在每次绘制之前都调用计算的方法,然后根据计算出来的数据直接循环绘制
这里有一点注意了,我们展示的效果中,选中的那个指示器Item是永远在上层的,这也就是绘制的时候必须在最后绘制,先绘制出其他非选中的那些Item
到这里,整个自定义控件就完工了,下面是和ViewPager的滑动进行绑定
这里的代码就是实现了ViewPager的滑动监听接口,然后在滑动的时候不断的改变下标和偏移的百分比,然后重新绘制,那么还差最后一点就是和ViewPager的绑定操作
提供一个方法出来设置ViewPager
/**
* 和ViewPager的滑动事件绑定
* 此方法必须在ViewPager设置适配器之后
*
* @param vp
*/
public void setUpViewPager(@NonNull ViewPager vp) {
PagerAdapter adapter = vp.getAdapter();
if (adapter != null) {
indicatorCount = adapter.getCount();
}
indicatorIndex = vp.getCurrentItem();
vp.removeOnPageChangeListener(this);
vp.addOnPageChangeListener(this);
}
代码很简单,就不解释了,这样就完成了所有的工作了
源码下载
如果想直接用,直接依赖
在主配置build文件中配置
allprojects {
repositories {
jcenter()
//就是添加上这句
maven { url 'https://jitpack.io' }
}
}
//在项目模块中直接依赖就能使用了
compile ‘com.github.xiaojinzi123:widget:v1.1.3.2’
就可以使用了,贴出一个完整用法
<com.move.widget.XIndicator
android:id="@+id/indicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@+id/vp"
android:layout_centerHorizontal="true"
android:layout_marginBottom="10dp"
app:indicatorHeight="6dp"
app:indicatorSpace="6dp"
app:indicatorWidth="20dp"
app:index="1"
app:count="4"
app:selectedIndicatorHeight="8dp"
app:selectedIndicatorWidth="32dp" />