Android自定義View實(shí)現(xiàn)星星評(píng)分效果
前言
在前面的學(xué)習(xí)中,我們基本了解了一些 Canvas 的繪制,那么這一章我們一起復(fù)習(xí)一下圖片的繪制幾種方式,和事件的簡單交互方式。
我們從易到難,作為基礎(chǔ)的進(jìn)階控件,我們從最簡單的交互開始,那就自定義一個(gè)星星評(píng)分的控件吧。
一個(gè) App 必不可少的評(píng)論系統(tǒng)打分的控件,可以展示評(píng)分,可以點(diǎn)擊評(píng)分,可以滑動(dòng)評(píng)分。它的實(shí)現(xiàn)總體上可以分為以下的步驟:
- 強(qiáng)制測(cè)量大小為我們指定的大小
- 先繪制Drawable未評(píng)分的圖片
- 在繪制Bitmap已評(píng)分的圖片
- 在onTouch中點(diǎn)擊和移動(dòng)的事件中動(dòng)態(tài)計(jì)算當(dāng)前的評(píng)分,進(jìn)而刷新布局
- 回調(diào)的處理與屬性的抽取
思路我們已經(jīng)有了,下面一步一步的來實(shí)現(xiàn)吧。
話不多說,Let's go
1、測(cè)量與圖片的繪制
我們需要繪制幾個(gè)星星,那么我們必須要設(shè)置的幾個(gè)屬性:
當(dāng)前的評(píng)分值,總共有幾個(gè)星星,每一個(gè)星星的間距和大小,選中和未選中的Drawable圖片:
private int mStarDistance = 0;
private int mStarCount = 5;
private int mStarSize = 20; //每一個(gè)星星的寬度和高度是一致的
private float mScoreNum = 0.0F; //當(dāng)前的評(píng)分值
private Drawable mStarScoredDrawable; //已經(jīng)評(píng)分的星星圖片
private Drawable mStarUnscoredDrawable; //還未評(píng)分的星星圖片
private void init(Context context, AttributeSet attrs) {
mScoreNum = 2.1f;
mStarSize = context.getResources().getDimensionPixelSize(R.dimen.d_20dp);
mStarDistance = context.getResources().getDimensionPixelSize(R.dimen.d_5dp);
mStarScoredDrawable = context.getResources().getDrawable(R.drawable.iv_normal_star_yellow);
mStarUnscoredDrawable = context.getResources().getDrawable(R.drawable.iv_normal_star_gray);
}
測(cè)量布局的時(shí)候,我們就不能根據(jù)xml設(shè)置的 match_parent 或 wrap_content 來設(shè)置寬高,我們需要根據(jù)星星的大小與間距來動(dòng)態(tài)的計(jì)算,所以不管xml中如何設(shè)置,我們都強(qiáng)制性的使用我們自己的測(cè)量。
星星的數(shù)量 * 星星的寬度再加上中間的間距 * 數(shù)量-1,就是我們的控件寬度,控件高度則是星星的高度。
具體的確定測(cè)量我們?cè)偕弦黄呀?jīng)詳細(xì)的復(fù)習(xí)過了,這里直接貼代碼:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(mStarSize * mStarCount + mStarDistance * (mStarCount - 1), mStarSize);
}這樣就可以得到對(duì)應(yīng)的測(cè)量寬高 (加一個(gè)背景方便看效果):

如何繪制星星?直接繪制Drawable即可,默認(rèn)的Drawable的繪制為:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
for (int i = 0; i < mStarCount; i++) {
mStarUnscoredDrawable.setBounds((mStarDistance + mStarSize) * i, 0, (mStarDistance + mStarSize) * i + mStarSize, mStarSize);
mStarUnscoredDrawable.draw(canvas);
}
}如果有5個(gè)星星圖片,那么就為每一個(gè)星星定好位置:

那么已經(jīng)選中的圖片也需要使用這種方法繪制嗎?
計(jì)算當(dāng)前的評(píng)分,然后計(jì)算計(jì)算需要繪制多少星星,那么就是這樣做:
int score = (int) Math.ceil(mScoreNum);
for (int i = 0; i < score; i++) {
mStarScoredDrawable.setBounds((mStarDistance + mStarSize) * i, 0, (mStarDistance + mStarSize) * i + mStarSize, mStarSize);
mStarScoredDrawable.draw(canvas);
}
可是這么做不符合我們的要求啊 ,我們是需要是可以顯示評(píng)分為2.5之類值,那么我們?cè)趺茨芾L制半顆星呢?Drawable.draw(canvas) 的方式滿足不了,那我們可以使用 BitmapShader 的方式來繪制。
初始化一個(gè) BitmapShader 設(shè)置給 Paint 畫筆,通過畫筆就可以畫出對(duì)應(yīng)的形狀。
比如此時(shí)的場景,我們?nèi)绻胫划?.5個(gè)星星,那么我們就可以
paint = new Paint();
paint.setAntiAlias(true);
paint.setShader(new BitmapShader(drawableToBitmap(mStarScoredDrawable), BitmapShader.TileMode.CLAMP, BitmapShader.TileMode.CLAMP));
@Override
protected void onDraw(Canvas canvas) {
for (int i = 0; i < mStarCount; i++) {
mStarUnscoredDrawable.setBounds((mStarDistance + mStarSize) * i, 0, (mStarDistance + mStarSize) * i + mStarSize, mStarSize);
mStarUnscoredDrawable.draw(canvas);
}
canvas.drawRect(0, 0, mStarSize * mScoreNum, mStarSize, paint);
}
那么如果是大于一個(gè)星星之后的小數(shù)點(diǎn)就可以用公式計(jì)算
if (mScoreNum > 1) {
canvas.drawRect(0, 0, mStarSize, mStarSize, paint);
if (mScoreNum - (int) (mScoreNum) == 0) {
//如果評(píng)分是3.0之類的整數(shù),那么直接按正常的rect繪制
for (int i = 1; i < mScoreNum; i++) {
canvas.translate(mStarDistance + mStarSize, 0);
canvas.drawRect(0, 0, mStarSize, mStarSize, paint);
}
} else {
//如果是小數(shù)例如3.5,先繪制之前的3個(gè),再繪制后面的0.5
for (int i = 1; i < mScoreNum - 1; i++) {
canvas.translate(mStarDistance + mStarSize, 0);
canvas.drawRect(0, 0, mStarSize, mStarSize, paint);
}
canvas.translate(mStarDistance + mStarSize, 0);
canvas.drawRect(0, 0, mStarSize * (Math.round((mScoreNum - (int) (mScoreNum)) * 10) * 1.0f / 10), mStarSize, paint);
}
} else {
canvas.drawRect(0, 0, mStarSize * mScoreNum, mStarSize, paint);
}效果:

關(guān)于 BitmapShader 的其他用法,可以翻看我之前的自定義圓角圓形View,和自定義圓角容器的文章,里面都有用到過,主要是方便一些圖片的裁剪和縮放等。
2、事件的交互與計(jì)算
這里并沒有涉及到什么事件嵌套,攔截之類的復(fù)雜處理,只需要處理自身的 onTouch 即可。而我們需要處理的就是按下的時(shí)候和移動(dòng)的時(shí)候評(píng)分值的變化。
在onDraw方法中,我們使用 mScoreNum 變量來繪制的已評(píng)分的 Bitmap 繪制。所以這里我們只需要在 onTouch 中計(jì)算出對(duì)應(yīng)的 mScoreNum 值,讓其重繪即可。
@Override
public boolean onTouchEvent(MotionEvent event) {
//x軸的寬度做一下最大最小的限制
int x = (int) event.getX();
if (x < 0) {
x = 0;
}
if (x > mMeasuredWidth) {
x = mMeasuredWidth;
}
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_MOVE: {
mScoreNum = x * 1.0f / (mMeasuredWidth * 1.0f / mStarCount);
invalidate();
break;
}
case MotionEvent.ACTION_UP: {
break;
}
}
return super.onTouchEvent(event);
}計(jì)算出一顆星的長度,然后計(jì)算當(dāng)前x軸的長度,就可以計(jì)算出當(dāng)前有幾顆星,我們默認(rèn)處理的是 float 類型。就可以根據(jù)計(jì)算出的 mScoreNum 值來得到對(duì)應(yīng)的動(dòng)畫效果:

3. 回調(diào)處理與自定義屬性抽取
到此效果的實(shí)現(xiàn)算是結(jié)束了,但是我們還有一些收尾工作沒做,如何監(jiān)聽進(jìn)度的回調(diào),如何控制整數(shù)與浮點(diǎn)數(shù)的顯示,是否支持觸摸等等。然后對(duì)其做一些自定義屬性的抽取,就可以在應(yīng)用中比較廣泛的使用了。
自定義屬性:
private int mStarDistance = 5;
private int mStarCount = 5;
private int mStarSize = 20; //每一個(gè)星星的寬度和高度是一致的
private float mScoreNum = 0.0F; //當(dāng)前的評(píng)分值
private Drawable mStarScoredDrawable; //已經(jīng)評(píng)分的星星圖片
private Drawable mStarUnscoredDrawable; //還未評(píng)分的星星圖片
private boolean isOnlyIntegerScore = false; //默認(rèn)顯示小數(shù)類型
private boolean isCanTouch = true; //默認(rèn)支持控件的點(diǎn)擊
private OnStarChangeListener onStarChangeListener;
自定義屬性的賦值與初始化操作:
private void init(Context context, AttributeSet attrs) {
setClickable(true);
TypedArray mTypedArray = context.obtainStyledAttributes(attrs, R.styleable.StarScoreView);
this.mStarDistance = mTypedArray.getDimensionPixelSize(R.styleable.StarScoreView_starDistance, 0);
this.mStarSize = mTypedArray.getDimensionPixelSize(R.styleable.StarScoreView_starSize, 20);
this.mStarCount = mTypedArray.getInteger(R.styleable.StarScoreView_starCount, 5);
this.mStarUnscoredDrawable = mTypedArray.getDrawable(R.styleable.StarScoreView_starUnscoredDrawable);
this.mStarScoredDrawable = mTypedArray.getDrawable(R.styleable.StarScoreView_starScoredDrawable);
this.isOnlyIntegerScore = mTypedArray.getBoolean(R.styleable.StarScoreView_starIsTouchEnable, true);
this.isOnlyIntegerScore = mTypedArray.getBoolean(R.styleable.StarScoreView_starIsOnlyIntegerScore, false);
mTypedArray.recycle();
paint = new Paint();
paint.setAntiAlias(true);
paint.setShader(new BitmapShader(drawableToBitmap(mStarScoredDrawable), BitmapShader.TileMode.CLAMP, BitmapShader.TileMode.CLAMP));
}自定義屬性的定義xml文件:
<!-- 評(píng)分星星控件 -->
<declare-styleable name="StarScoreView">
<!--星星間距-->
<attr name="starDistance" format="dimension" />
<!--星星大小-->
<attr name="starSize" format="dimension" />
<!--星星個(gè)數(shù)-->
<attr name="starCount" format="integer" />
<!--星星已評(píng)分圖片-->
<attr name="starScoredDrawable" format="reference" />
<!--星星未評(píng)分圖片-->
<attr name="starUnscoredDrawable" format="reference" />
<!--是否可以點(diǎn)擊-->
<attr name="starIsTouchEnable" format="boolean" />
<!--是否顯示整數(shù)-->
<attr name="starIsOnlyIntegerScore" format="boolean" />
</declare-styleable>
在OnTouch的時(shí)候就可以判斷是否能觸摸
@Override
public boolean onTouchEvent(MotionEvent event) {
if (isCanTouch) {
//x軸的寬度做一下最大最小的限制
int x = (int) event.getX();
if (x < 0) {
x = 0;
}
if (x > mMeasuredWidth) {
x = mMeasuredWidth;
}
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_MOVE: {
setStarMark(x * 1.0f / (getMeasuredWidth() * 1.0f / mStarCount));
break;
}
case MotionEvent.ACTION_UP: {
break;
}
}
return super.onTouchEvent(event);
} else {
//如果設(shè)置不能點(diǎn)擊,直接不觸發(fā)事件
return false;
}
}而 setStarMark 則是設(shè)置入口的方法,內(nèi)部判斷是否支持小數(shù)點(diǎn)和設(shè)置對(duì)于的監(jiān)聽,并調(diào)用重繪。
public void setStarMark(float mark) {
if (isOnlyIntegerScore) {
mScoreNum = (int) Math.ceil(mark);
} else {
mScoreNum = Math.round(mark * 10) * 1.0f / 10;
}
if (this.onStarChangeListener != null) {
this.onStarChangeListener.onStarChange(mScoreNum); //調(diào)用監(jiān)聽接口
}
invalidate();
}一個(gè)簡單的圖片繪制和事件觸摸的控件就完成啦,使用起來也是超級(jí)方便。
<com.guadou.kt_demo.demo.demo18_customview.star.StarScoreView
android:id="@+id/star_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="@dimen/d_40dp"
android:background="#f1f1f1"
app:starCount="5"
app:starDistance="@dimen/d_5dp"
app:starIsOnlyIntegerScore="false"
app:starIsTouchEnable="true"
app:starScoredDrawable="@drawable/iv_normal_star_yellow"
app:starSize="@dimen/d_35dp"
app:starUnscoredDrawable="@drawable/iv_normal_star_gray" />
Activity中可以設(shè)置評(píng)分和設(shè)置監(jiān)聽:
override fun init() {
val starView = findViewById<StarScoreView>(R.id.star_view)
starView.setOnStarChangeListener {
YYLogUtils.w("當(dāng)前選中的Star:$it")
}
findViewById<View>(R.id.set_progress).click {
starView.setStarMark(3.5f)
}
}效果:

后記
整個(gè)流程走下來是不是很簡單呢,此控件不止用于星星類型的評(píng)分,任何圖片資源都可以使用,現(xiàn)在我們思路打開擴(kuò)展一下,相似的場景和效果我們可以實(shí)現(xiàn)一些圖片進(jìn)度,觸摸進(jìn)度條,圓環(huán)的SeekBar,等等類似的控制都是相似的思路。
到此這篇關(guān)于Android自定義View實(shí)現(xiàn)星星評(píng)分效果的文章就介紹到這了,更多相關(guān)Android自定義View星星評(píng)分內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Android不顯示開機(jī)向?qū)Ш烷_機(jī)氣泡問題
這篇文章主要介紹了Android不顯示開機(jī)向?qū)Ш烷_機(jī)氣泡問題,需要的朋友可以參考下2019-05-05
Android 實(shí)現(xiàn)會(huì)旋轉(zhuǎn)的餅狀統(tǒng)計(jì)圖實(shí)例代碼
這篇文章主要介紹了Android 實(shí)現(xiàn)會(huì)旋轉(zhuǎn)的餅狀統(tǒng)計(jì)圖實(shí)例代碼的相關(guān)資料,這里附有實(shí)例代碼及實(shí)現(xiàn)效果圖,需要的朋友可以參考下2016-12-12
Android?縮放動(dòng)畫?ScaleAnimation的使用小結(jié)
ScaleAnimation即縮放動(dòng)畫,應(yīng)用場景特別多,比如常見的隱藏菜單點(diǎn)擊顯示,這篇文章主要介紹了Android?縮放動(dòng)畫?ScaleAnimation的使用小結(jié),需要的朋友可以參考下2024-03-03
Android自定義View實(shí)現(xiàn)支付寶支付成功-極速get花式Path炫酷動(dòng)畫
這篇文章主要介紹了Android自定義View實(shí)現(xiàn)支付寶支付成功-極速get花式Path炫酷動(dòng)畫的相關(guān)資料,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2017-01-01
Android自定義短信倒計(jì)時(shí)view流程分析
倒計(jì)時(shí)實(shí)現(xiàn)有三種方式 而這個(gè)自定義view是通過handler實(shí)現(xiàn)的。本文通過實(shí)例代碼給大家介紹Android自定義短信倒計(jì)時(shí)view流程,,需要的朋友可以參考下2020-03-03
Android 下載網(wǎng)絡(luò)圖片并顯示到本地
本文主要介紹了Android實(shí)現(xiàn)下載網(wǎng)絡(luò)圖片并顯示到本地功能的示例代碼。具有很好的參考價(jià)值,下面跟著小編一起來看下吧2017-03-03
Android如何動(dòng)態(tài)調(diào)整應(yīng)用字體大小詳解
這篇文章主要給大家介紹了關(guān)于Android如何動(dòng)態(tài)調(diào)整應(yīng)用字體大小的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2018-05-05

