android自定义控件(星级评分)
一、背景
視覺過來提了一個需求,要求完成一個星級評分控件,該控件中的星星的顏色需要實現漸變的效果,并且沒有漸變的規律,也就是說各個星星的顏色需要不一樣,效果如下:
二、問題分析
星星控件對應的控件是android.support.v7.widget.AppCompatRatingBar,利用這個控件可以實現星級評分效果,但是每個星星的顏色是一樣的,效果如下:
具體的實現代碼如下:
<android.support.v7.widget.AppCompatRatingBarandroid: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類
@Overrideprotected 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方法中,有繪制星星的邏輯
在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));}@Overrideprotected 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);}@Overrideprotected 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();}@Overrideprotected 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>
mc_ratingbar_big_full_light.xml
<?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>
mc_ratingbar_small_full_light.xml
<?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>
在arrays.xml中聲明默認的星星顏色
<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>
上面用到的資源有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中使用
<com.liunian.common.widget.MzRatingBarandroid: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.MzRatingBarandroid: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.MzRatingBarandroid: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.MzRatingBarandroid: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,指定一個顏色數組,顏色數組為各個星星的顏色。 也可以在代碼中直接調用接口。效果如下:
六、總結
1、在接到控件方面的需求時,首先我們要考慮利用現有的控件是否能夠達到要求,通過給原生控件定義樣式能夠滿足大部分的控件需求; 2、在原生控件不能滿足需求時,我們需要分析需求,選擇一個最接近需求的控件繼承,然后在該控件上擴展; 3、自定義控件需要大量的知識積累,才能做出完美的自定義控件。總結
以上是生活随笔為你收集整理的android自定义控件(星级评分)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python re库 正则表达式
- 下一篇: 如何运用Microsoft Office