今天来写这个小控件:
可能有同学要说了,这不就是ViewPagerIndicator么,去github上找xxxx。
嗯,这确实也是个解决方案,但是开头我说了,尽量不用第三方UI库,而且就朴朴里面的这个Indicator,功能很简单,也基本不存在什么太大的扩展性需求,自己写能节约一些性能。很多时候,我们只需要第三方库的一个功能,但是引用的库却包含很多我们用不到的功能,这就凭空增加了安装包的体积。
哪怕在kotlin工程里,自定义控件我一般也用java写,以便复用到java工程,这个也不例外。
操作朴朴app我们发现,这个indicator没有复杂的效果,只是在放图片的viewpager翻页的时候指示器改变状态,所以没必要继承ViewGroup,我们直接继承view,自己去画里面的内容。
首先在styleable里定义几个属性:
<declare-styleable name="SimpleIndicator">
<!-- 指示器每个子项的宽度 -->
<attr name="indicatorItemWidth" format="dimension|reference"/>
<!-- 指示器每个子项间的间隔 -->
<attr name="indicatorItemPadding" format="dimension|reference"/>
<!-- 指示器每个子项的圆角半径 -->
<attr name="indicatorItemRadius" format="dimension|reference"/>
<!-- 指示器选中项的颜色 -->
<attr name="indicatorItemSelectedColor" format="color|reference"/>
<!-- 指示器未选中项的颜色 -->
<attr name="indicatorItemNormalColor" format="color|reference"/>
</declare-styleable>
在这里,子项的高度直接取外层高度就行了,因为这个控件并不需要padding。
为了让这个控件宽度能够使用wrap_content指定,必须重写它的onMeasure方法,根据我们自定义的子项宽度和间距来计算总宽度,MeasureSpec的用法网上一搜一大片,不再赘述:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
//宽度使用wrap_content的时候
if(widthMode == MeasureSpec.AT_MOST){
float width = getPaddingLeft() + getPaddingRight() + itemWidth * size + (size - 1) * itemPadding;
if(width < widthSize)
widthSize = (int)width;
else
throw new IllegalStateException("您设置的子项目宽度和间距超过了可用宽度");
}
//高度必须指定为具体值
if(heightMode != MeasureSpec.EXACTLY){
throw new IllegalStateException("indicator的高度必须指定为具体数值");
}
setMeasuredDimension(widthSize, heightSize);
}
然后是子项绘制位置的初始化:
private void initItemRectangles(){
itemRectangles = new RectF[size];
int height = getMeasuredHeight();
float itemDist = itemWidth + itemPadding;
for(int i = 0; i < size; i++){
float left = i * itemDist;
float right = left + itemWidth;
itemRectangles[i] = new RectF(left, 0, right, height);
}
}
这个初始化应该在onDraw里调用并且只调用一次以避免多余的计算,大家可能注意到了drawXXXX这类方法也有不用RectF的重载,直接以上下左右坐标为入参,但是如果你那样用,每次onDraw的时候都会重新计算一次,作为硬件的老板,我们要学会为这些可爱的员工减负啊。
上面的size哪来的?当然是跟viewPager关联的时候取到的了:
public void setupWithViewPager(ViewPager viewPager){
PagerAdapter adapter = viewPager.getAdapter();
if(adapter == null)
throw new IllegalStateException("this viewPager has not set adapter yet");
this.size = adapter.getCount();
viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
@Override
public void onPageScrolled(int i, float v, int i1) {
}
@Override
public void onPageSelected(int i) {
selectedIndex = i;
invalidate();
}
@Override
public void onPageScrollStateChanged(int i) {
}
});
}
最后是onDraw方法:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if(itemRectangles == null)
initItemRectangles();
for(int i = 0; i < itemRectangles.length; i++){
drawItem(canvas, itemRectangles[i], i == selectedIndex);
}
}
protected void drawItem(Canvas canvas, RectF rect, boolean selected){
paint.setColor(selected ? selectedColor : normalColor);
canvas.drawRoundRect(rect, itemRadius, itemRadius, paint);
}
这里把drawItem抽成单独的方法,而且是protected,一般这种Indicator的不同只是图形不同,这样如果你需要简单扩展的话,改这个方法就可以了。
到这里,自定义小控件就写完了,上一张效果图:
本项目地址:仿写朴朴app