一、背景
视觉过来提了一个需求,要求完成一个星级评分控件,该控件中的星星的颜色需要实现渐变的效果,并且没有渐变的规律,也就是说各个星星的颜色需要不一样,效果如下:
二、问题分析
星星控件对应的控件是android.support.v7.widget.AppCompatRatingBar,利用这个控件可以实现星级评分效果,但是每个星星的颜色是一样的,效果如下:
具体的实现代码如下:
<android.support.v7.widget.AppCompatRatingBar
android:id="@+id/popup_ratingbar"
android:layout_gravity="center_horizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/Widget.DeviceDefault.Light.RatingBar.Color.DodgerBlue"
android:numStars="5"
android:rating="3.5"
android:isIndicator="false" />
<style name="Widget.DeviceDefault.Light.RatingBar.Color.DodgerBlue" parent="android:style/Widget.DeviceDefault.Light.RatingBar">
<item name="android:progressDrawable">@drawable/mz_ratingbar_full_light_color_dodgerblue</item>
<item name="android:indeterminateDrawable">@drawable/mz_ratingbar_full_light_color_dodgerblue</item>
<item name="android:minHeight">29.3dip</item>
</style>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+android:id/background" android:drawable="@drawable/mz_btn_bigstar_off" />
<item android:id="@+android:id/secondaryProgress" android:drawable="@drawable/mz_btn_bigstar_off" />
<item android:id="@+android:id/progress" android:drawable="@drawable/mz_ratingbar_full_filled_light_color_dodgerblue" />
</layer-list>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true"
android:state_window_focused="true"
android:drawable="@drawable/mz_btn_bigstar_on_pressed_color_dodgerblue" />
<item android:drawable="@drawable/mz_btn_bigstar_on_color_dodgerblue" />
</selector>
mz_btn_bigstar_on_pressed_color_dodgerblue、mz_btn_bigstar_on_color_dodgerblue、mz_btn_bigstar_off是三种星星图片,分别对应星星的按下状态、正常状态和背景:
从上面的代码可以看到,AppCompatRatingBar提供了一个设置progressDrawable的接口,通过这个接口我们可以设置星星的样式,而我们在设置progressDrawable的时候,传入进去的只是一张星星的图片,那它是怎样做到绘制多个星星的呢?
首先,我们来看一下AppCompatRatingBar类的继承关系:
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中并没有绘制的逻辑,查看父类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、在原有控件基础上绘制一层渐变的矩形
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;
}
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<getNumStars(); i++) {
int starColor;
if (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<getNumStars(); i++) {
int starColor;
if (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声明属性的定义
<declare-styleable name="MzRatingBar">
<attr name="mcStarColors" format="reference" />
<attr name="mcStarDrawable" format="reference" />
</declare-styleable>
定义常用的style,这里定义了大星星和小星星两套style给应用使用
<style name="Widget.Common.MzRatingBar.Large" parent="android:style/Widget.DeviceDefault.Light.RatingBar">
<item name="android:progressDrawable">@drawable/mc_ratingbar_big_full_light</item>
<item name="android:indeterminateDrawable">@drawable/mc_ratingbar_big_full_light</item>
<item name="android:minHeight">29.3dip</item>
<item name="mcStarDrawable">@drawable/mz_btn_big_star_on</item>
<item name="mcStarColors">@array/mc_rating_bar_default_colors</item>
</style>
<style name="Widget.Common.MzRatingBar.Small" parent="android:style/Widget.DeviceDefault.Light.RatingBar">
<item name="android:progressDrawable">@drawable/mc_ratingbar_small_full_light</item>
<item name="android:indeterminateDrawable">@drawable/mc_ratingbar_small_full_light</item>
<item name="android:minHeight">14.7dip</item>
<item name="mcStarDrawable">@drawable/mz_btn_small_star_on</item>
<item name="mcStarColors">@array/mc_rating_bar_default_colors</item>
</style>
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@android:id/background" android:drawable="@drawable/mz_btn_big_star" />
<item android:id="@android:id/secondaryProgress" android:drawable="@drawable/mz_btn_big_star_secondary" />
<item android:id="@android:id/progress" android:drawable="@drawable/mz_btn_big_star" />
</layer-list>
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@android:id/background" android:drawable="@drawable/mz_btn_small_star" />
<item android:id="@android:id/secondaryProgress" android:drawable="@drawable/mz_btn_small_star_secondary" />
<item android:id="@android:id/progress" android:drawable="@drawable/mz_btn_small_star" />
</layer-list>
<array name="mc_rating_bar_default_colors" translatable="false">
<item>#ff961d</item>
<item>#fda21f</item>
<item>#fcb121</item>
<item>#fabd23</item>
<item>#f6c84b</item>
</array>
五、应用使用
1、在xml中使用
<com.liunian.common.widget.MzRatingBar
android:id="@+id/ratingbar1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
style="@style/Widget.Common.MzRatingBar.Large"
android:isIndicator="false"
android:numStars="5"
android:rating="3" />
<com.liunian.common.widget.MzRatingBar
android:id="@+id/ratingbar2"
android:layout_below="@id/ratingbar1"
android:layout_marginTop="20dp"
android:layout_centerHorizontal="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/Widget.Common.MzRatingBar.Small"
android:isIndicator="false"
android:numStars="5"
android:rating="3" />
2、自定义星星的颜色
<com.liunian.common.widget.MzRatingBar
android:id="@+id/ratingbar1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
style="@style/Widget.Common.MzRatingBar.Large"
app:mcStarColors="@array/rating_bar_colors"
android:isIndicator="false"
android:numStars="5"
android:rating="3" />
<com.liunian.common.widget.MzRatingBar
android:id="@+id/ratingbar2"
android:layout_below="@id/ratingbar1"
android:layout_marginTop="20dp"
android:layout_centerHorizontal="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/Widget.Common.MzRatingBar.Small"
app:mcStarColors="@array/rating_bar_colors"
android:isIndicator="false"
android:numStars="5"
android:rating="3" />
<array name="rating_bar_colors" translatable="false">
<item>#43b56b</item>
<item>#44aff9</item>
<item>#f56455</item>
<item>#5ecddf</item>
<item>#f6b944</item>
</array>
通过设置mcStarColors,指定一个颜色数组,颜色数组为各个星星的颜色。 也可以在代码中直接调用接口。效果如下: