android自定义控件(星级评分)

一、背景

视觉过来提了一个需求,要求完成一个星级评分控件,该控件中的星星的颜色需要实现渐变的效果,并且没有渐变的规律,也就是说各个星星的颜色需要不一样,效果如下:


android自定义控件(星级评分)_第1张图片


二、问题分析


星星控件对应的控件是android.support.v7.widget.AppCompatRatingBar,利用这个控件可以实现星级评分效果,但是每个星星的颜色是一样的,效果如下:




具体的实现代码如下:

    


style的定义Widget.DeviceDefault.Light.RatingBar.Color.DodgerBlue:


mz_ratingbar_full_light_color_dodgerblue.xml


    
    
    


mz_ratingbar_full_filled_light_color_dodgerblue.xml



    

    


mz_btn_bigstar_on_pressed_color_dodgerblue、mz_btn_bigstar_on_color_dodgerblue、mz_btn_bigstar_off是三种星星图片,分别对应星星的按下状态、正常状态和背景:




从上面的代码可以看到,AppCompatRatingBar提供了一个设置progressDrawable的接口,通过这个接口我们可以设置星星的样式,而我们在设置progressDrawable的时候,传入进去的只是一张星星的图片,那它是怎样做到绘制多个星星的呢?

首先,我们来看一下AppCompatRatingBar类的继承关系:


android自定义控件(星级评分)_第2张图片


AppCompatRatingBar类


    @Override
    protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        Bitmap sampleTile = mAppCompatProgressBarHelper.getSampleTime();
        if (sampleTile != null) {
            // 根据星星个数计算控件的宽度
            final int width = sampleTile.getWidth() * getNumStars();
            setMeasuredDimension(ViewCompat.resolveSizeAndState(width, widthMeasureSpec, 0),
                    getMeasuredHeight());
        }
    }


AppCompatRatingBar会根据星星的个数来计算控件的宽度,也就是控件的宽度等于:单个星星的宽度 X 星星个数。
在AppCompatRatingBar中并没有绘制的逻辑,查看父类RatingBar的代码,也没用重写onDraw方法,继续在父类中查找,在ProgressBar的onDraw方法中,有绘制星星的逻辑


    @Override
    protected synchronized void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        drawTrack(canvas);
    }

    void drawTrack(Canvas canvas) {
        final Drawable d = mCurrentDrawable;
        if (d != null) {
            ...
            d.draw(canvas);
            ...
        }
    }


在ProgressBar的构造函数中,会对progressDrawable进行处理

    public ProgressBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        ...
        if (progressDrawable != null) {
            // Calling setProgressDrawable can set mMaxHeight, so make sure the
            // corresponding XML attribute for mMaxHeight is read after calling
            // this method.
            if (needsTileify(progressDrawable)) {
                setProgressDrawableTiled(progressDrawable);
            } else {
                setProgressDrawable(progressDrawable);
            }
        }
        ...
    }

    public void setProgressDrawableTiled(Drawable d) {
        if (d != null) {
            d = tileify(d, false);
        }

        setProgressDrawable(d);
    }

    private Drawable tileify(Drawable drawable, boolean clip) {
        ...
        if (drawable instanceof BitmapDrawable) {
            ...
            final BitmapDrawable clone = (BitmapDrawable) bitmap.getConstantState().newDrawable();、
            // 横向重复平铺
            clone.setTileModeXY(Shader.TileMode.REPEAT, Shader.TileMode.CLAMP);

            if (clip) {
                return new ClipDrawable(clone, Gravity.LEFT, ClipDrawable.HORIZONTAL);
            } else {
                return clone;
            }
        }
        ...
    }

在ProgressBar的构造函数中,会根据prgressDrawable生成一个新的drawable,这个drawable横向是prgressDrawable的平铺效果,然后再把新的drawable设为progressDrawable。所以,我们只给控件的背景只设置了一个星星的图片,但是它会根据设置的星星个数计算控件的宽度,然后再对星星drawable进行横向平铺直到填满控件。

现在我们各个星星的颜色要求不一样,而现在控件的绘制逻辑是只传入一张星星的图片,然后将星星图片横向平铺知道撑满控件,现有的控件已经无法满足星星颜色渐变的需求了。这是,我们可以考虑自定义控件。


三、解决办法


既然现有的控件已经无法满足需求,我们就要考虑自定义控件来满足需求。分析过后尝试了以下几种方法:

1、在原有控件基础上绘制一层渐变的矩形

首先我们自定义控件MzAppCompatRatingBar,继承于AppCompatRatingBar,重新它的onDraw方法,在原有控件的基础上绘制一层渐变的矩形,代码如下:
    protected synchronized void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Drawable progressDrawable = getProgressDrawable();
        if (progressDrawable != null) {
            canvas.save();
            mPaint.setShader(new LinearGradient((float) progressDrawable.getBounds().left, (float) progressDrawable.getBounds().top,
                    (float) progressDrawable.getBounds().right, (float) progressDrawable.getBounds().bottom, Color.argb(0, 255, 255, 255), Color.argb(150, 255, 255, 255), Shader.TileMode.REPEAT));
            canvas.drawPaint(mPaint);
            canvas.restore();
        }
    }

实现效果:



这种方式其实就是在原有控件的基础上盖了一层渐变的矩形,两者效果的叠加就会产生一种渐变的效果,如果背景色改变了,渐变的颜色也需要改变,与背景色一致,否则就会看到星星控件有背景色,比如:



这种方式实现的效果,整个控件是从左到右颜色逐渐渐变的,而按照视觉的要求,每个星星的颜色是固定的,并且也没有渐变的规律,所以使用这种方式是不可以的。

2、在原有控件基础上绘制自己的星星


既然原有的控件无法定制每个星星的颜色,那我们是否可以考虑自己绘制各个星星呢?我们可以在原有的控件基础上绘制五个颜色不一样的星星,然后根据滑动的进度来设置裁剪区域,只绘制进度左边的内容,代码如下:

    // 加载五张星星的图片
    private void createStarDrawables(int[] starColors) {
        mStarDrawables = new ArrayList<>();
        mStarDrawables.add(getResources().getDrawable(R.drawable.mz_btn_bigstar_on_pressed_color_limegreen));
        mStarDrawables.add(getResources().getDrawable(R.drawable.mz_btn_bigstar_on_pressed_color_grey));
        mStarDrawables.add(getResources().getDrawable(R.drawable.mz_btn_bigstar_on_pressed_color_firebrick));
        mStarDrawables.add(getResources().getDrawable(R.drawable.mz_btn_bigstar_on_pressed_color_coral));
        mStarDrawables.add(getResources().getDrawable(R.drawable.mz_btn_bigstar_on_pressed_color_seagreen));
    }


    @Override
    protected synchronized void onDraw(Canvas canvas) {
        super.onDraw(canvas); // 先绘制原来的控件
    // 在原来控件的基础上根据进度绘制颜色不一样的星星
        Drawable progressDrawable = getProgressDrawable();
        if (progressDrawable != null) {
            canvas.save();
            // 获得进度位置
            final int pogressPos = getProgressPos();
            // 根据进度位置设置裁剪区域
            canvas.clipRect(0, 0, pogressPos, getHeight());
            int drawableLeft = getPaddingLeft();
            int drawableTop = getPaddingTop();
            // 绘制五张星星
            for (Drawable drawable : mStarDrawables) {
                drawable.setBounds(drawableLeft, drawableTop, drawableLeft + drawable.getIntrinsicWidth(), drawableTop + drawable.getIntrinsicHeight());
                drawableLeft += drawable.getIntrinsicWidth();
                drawable.draw(canvas);
            }
            canvas.restore();
        }
    }


    /**
     * 获取进度所对应的位置
     * @return
     */
    private int getProgressPos() {
        int available = getWidth() - getPaddingLeft() - getPaddingRight();
        final int progressPos = (int) (getScale() * available + 0.5f) + getPaddingLeft();
        return progressPos;
    }


    /**
     * 获得当前滑动进度的百分比
     * @return
     */
    private float getScale() {
        final int max = getMax(); // 最大进度
        return max > 0 ? getProgress() / (float) max : 0;
    }

显示效果如下:



ok,这个正式我们想要的效果,通过在原有控件上盖上自己绘制的星星,我们可以实现视觉的需求。

3、通过滤镜效果绘制不同颜色的星星


通过方法二,已经可以实现视觉提出的需求,但是,这种方法有一个缺点:每种颜色的星星都需要提供图片,增加了公共资源的大小。

那么,有什么改进的办法呢?在android中,可以通过setColorFilter来改变drawable的颜色,那么我们是否可以只提供一张纯色的星星图片,然后通过setColorFilter来动态改变星星的颜色呢?答案是可以的,改进后的代码如下:

    private void init() {
        createStarDrawables(new int[]{0xFFFF9602, 0xFFFFA300, 0xFFFEB100, 0xFFF9BE00, 0xFFF9BE00});
    }

    private void createStarDrawables(int[] starColors) {
        mStarColors = starColors;
        mstarDrawable = getResources().getDrawable(R.drawable.mz_btn_bigstar_test, null);

    }

    @Override
    protected synchronized void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (mStarDrawable != null && mStarColors != null) {
            // 在原来RatingBar的基础上在绘制一层颜色不一样的星星
            canvas.save();
            final int pogressPos = getProgressPos();
            canvas.clipRect(0, 0, pogressPos, getHeight());
            int drawableLeft = getPaddingLeft();
            int drawableTop = getPaddingTop();
            for (int i=0; i= mStarColors.length) {
                    starColor = mStarColors[mStarColors.length - 1];
                } else {
                    starColor = mStarColors[i];
                }
                mStarDrawable.setColorFilter(starColor, PorterDuff.Mode.SRC_IN); // 对drawable进行染色处理
                mStarDrawable.setBounds(drawableLeft, drawableTop, drawableLeft + mStarDrawable.getIntrinsicWidth(), drawableTop + mStarDrawable.getIntrinsicHeight());
                drawableLeft += mStarDrawable.getIntrinsicWidth();
                mStarDrawable.draw(canvas);
            }
            canvas.restore();
        }
    }

其中mz_btn_bigstar_test是一张纯色的星星图片,如下:




绘制效果如下:


通过利用滤镜来改变drawable的颜色,我们完美解决了需要多张图片资源的情况。


四、代码整理

对于自定义控件,在完成了基本的功能后,我们还需要整理代码,定义属性和样式给应用使用,分析应该提供哪些接口给应用。比如在星级评分控件中,应用应该可以自定义星星的图片和颜色,所有我们需要定义属性给应用自定义 星星的图片和颜色,并在代码中提供相应的接口。

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.widget.RatingBar;
import com.liunian.common.R;

public class MzRatingBar extends RatingBar {
    private Drawable mStarDrawable;
    private int[] mStarColors;

    public MzRatingBar(Context context) {
        this(context, null);
    }

    public MzRatingBar(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MzRatingBar(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.MzRatingBar, defStyleAttr, 0);
        int colorArrayId = a.getResourceId(R.styleable.MzRatingBar_mcStarColors, R.array.mc_rating_bar_default_colors);
        mStarColors = getResources().getIntArray(colorArrayId);
        mStarDrawable = a.getDrawable(R.styleable.MzRatingBar_mcStarDrawable);
        if (mStarDrawable == null) {
            mStarDrawable = getResources().getDrawable(R.drawable.mz_btn_big_star_on);
        }
        a.recycle();
    }

    @Override
    protected synchronized void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (mStarDrawable != null && mStarColors != null) {
            // 在原来RatingBar的基础上在绘制一层颜色不一样的星星
            canvas.save();
            final int pogressPos = getProgressPos();
            canvas.clipRect(0, 0, pogressPos, getHeight());
            int drawableLeft = getPaddingLeft();
            int drawableTop = getPaddingTop();
            for (int i=0; i= mStarColors.length) {
                    starColor = mStarColors[mStarColors.length - 1];
                } else {
                    starColor = mStarColors[i];
                }
                mStarDrawable.setColorFilter(starColor, PorterDuff.Mode.SRC_IN);
                mStarDrawable.setBounds(drawableLeft, drawableTop, drawableLeft + mStarDrawable.getIntrinsicWidth(), drawableTop + mStarDrawable.getIntrinsicHeight());
                drawableLeft += mStarDrawable.getIntrinsicWidth();
                mStarDrawable.draw(canvas);
            }
            canvas.restore();
        }
    }

    /**
     * 设置各个星星的颜色
     * @param starColors
     */
    public void setStarColors(int[] starColors) {
        if (starColors != null) {
            mStarColors = starColors;
        }
    }

    /**
     * 获取进度所对应的位置
     * @return
     */
    private int getProgressPos() {
        int available = getWidth() - getPaddingLeft() - getPaddingRight();
        final int progressPos = (int) (getScale() * available + 0.5f) + getPaddingLeft();
        return progressPos;
    }

    private float getScale() {
        final int max = getMax();
        return max > 0 ? getProgress() / (float) max : 0;
    }
}

在attrs.xml声明属性的定义
    
        
        
    

定义常用的style,这里定义了大星星和小星星两套style给应用使用

    
    


mc_ratingbar_big_full_light.xml




    
    
    

mc_ratingbar_small_full_light.xml




    
    
    


在arrays.xml中声明默认的星星颜色

    
        #ff961d
        #fda21f
        #fcb121
        #fabd23
        #f6c84b
    


上面用到的资源有mz_btn_big_star_on、mz_btn_big_star_on、mz_btn_big_star_secondary、mz_btn_small_star_on、mz_btn_small_star_on、mz_btn_small_star_secondary,如下:




五、应用使用

定义好控件后,应用就可以使用了,具体用法如下:

1、在xml中使用


    

    


效果如下:

android自定义控件(星级评分)_第3张图片

2、自定义星星的颜色


    

    

    
        #43b56b
        #44aff9
        #f56455
        #5ecddf
        #f6b944
    

通过设置mcStarColors,指定一个颜色数组,颜色数组为各个星星的颜色。 也可以在代码中直接调用接口。效果如下:
android自定义控件(星级评分)_第4张图片


六、总结

1、在接到控件方面的需求时,首先我们要考虑利用现有的控件是否能够达到要求,通过给原生控件定义样式能够满足大部分的控件需求;
2、在原生控件不能满足需求时,我们需要分析需求,选择一个最接近需求的控件继承,然后在该控件上扩展;
3、自定义控件需要大量的知识积累,才能做出完美的自定义控件。


你可能感兴趣的:(android)