仿写朴朴app(二)

2019-03-25  本文已影响0人  怀念小兔

今天来写这个小控件:


不起眼的小控件

可能有同学要说了,这不就是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
上一篇 下一篇

猜你喜欢

热点阅读