RatingStarView
Android自定義的評分控件,類似RatingBar那樣的,使用星星圖標(full、half、empty)作為rating值的“評分/打分控件”。
效果圖
圖1:
RatingStarView控件支持的特性:
- 半顆星支持(實際支持任意小數)
- 填充色、底色、描邊色
- 指定高度,寬度自適應
- 拐角弧度、描邊寬度、星星間距
- 肥胖指數(star thickness),越胖越可愛
- 點擊評分(不支持半顆星)
實現思路
下面是RatingStarView的實現設計。
如何畫一顆星
Star標準坐標
可以在抽象的xOy坐標系中計算得到一個star的“標準坐標”。這個坐標可以作為后續有關坐標計算(偏移和縮放)的基準。
圖2:
以上面的圖為例,這里其中心點O為原點。
這里為了描述方便,稱A,B,C,D,E為5個外點(Outer Corner Vertex),a,b,c,d,e 五個點為內點(Inner Corner Vertex)。
這里坐標值的選取完全出于計算方便來考慮,實現方式畢竟很多,大家可以選取其它坐標方式,比如原點O的位置在其它處,或者星星的范圍由高度、寬度表示等。
5個外頂點
A拐點的坐標為(0,1),其它幾個點的坐標根據幾何公式是可以固定下來的。為了簡化計算,可以將這幾個值作為常量保存,之后的其它值的計算基于它們。下面代碼為了程序上的便利E點坐標x,y值是起始元素:
private static final float[] starVertexes = new float[]{
-0.9511f, 0.3090f, // E (left)
0.0000f, 1.0000f, // A (top vertex)
0.9511f, 0.3090f, // B (right)
0.5878f, -0.8090f, // C (bottom right)
-0.5878f, -0.8090f, // D (bottom left)
};
使用常量簡化五角星坐標計算時的cos、sin操作。因為幾何上這些點的坐標是固定的。之后可以通過簡單的+-*/操作來變換坐標系,以及star的大小。
常量也不會保持得太多,比如a,b,c,d,e的計算是根據A,B,C,D,E來的。
5個內拐點
這里為star引入“胖度系數(star thickness)”的說法,用來控制星星的可愛程度。
很明顯,胖度是由a,b,c,d,e五個內點的位置決定的。
但在計算上,這里采取另一種方式:
設置變量thickness
來表示肥胖系數,5個內點的位置由原點O和此內點臨近的兩個外點計算得到。
還是上面的圖2,
AE的中點是P,那么e肯定在OP上,如果取OP上的其它點,作為EPA這樣的多邊形路徑(其它五個內點類似)就可以打造出不同肥胖度的星星了。
這里因為原點O是星星的中心,在標準坐標系下,根據胖度系數thickness,結合ABCDE這幾個外點,就可以計算出abcde這幾個內點了,而且當thickness不同時,星星胖度不同。
根據thickness和ABCDE計算abcde的過程必須是在“標準坐標系”下,也就是X+軸向右,Y+向上,而且O原點是星星中心!!
坐標轉換
每一個要顯示的star由一個StarModel
類來表示,它持有一個星星的坐標信息并完成相應的計算。
其代碼是整個RatingStarView關于坐標部分的核心,完整代碼見下面的源碼地址。
拐點(頂點)表示
星星的頂點可以用一個PointF進行表示,不過這里為了方便將多個點作為一個鏈表使用,定義了下面的VertexF
來保存頂點數據:
class VertexF {
public VertexF() {
}
public VertexF(float x, float y) {
this.x = x;
this.y = y;
}
public float x;
public float y;
public VertexF next;
}
10個拐點的計算
StarModel類使用靜態的數組保存ABCDE五個外點的標準坐標系下的坐標初始值。
因為thickness系數必須在標準坐標系下計算,這里選擇StarModel的構造函數中接受thickness參數,而且初始化中完成所有10個拐點的計算。
轉換到Android坐標系
手機設備下,Android的Y+是向下的,所以需要一個adjustCoordinate()的方法來完成星星坐標系的轉換。
同時它還將星星的x,y都變為正數——這樣它才是可見的。
注意Android中,childView繪制自身內容時,其使用的x,y坐標單位是pixel,而且是相對其父ViewGroup的相對坐標。
偏移和縮放
RatingStarView在顯示若干個star時,需要可以控制其位置和大小。
所以StarModel在標準坐標系轉換完為Android下坐標系后(在父布局中的相對坐標),還需要可以被偏移和縮放。
偏移
只需要對10個拐點坐標進行+、-操作即可。
有關Star的大小這里使用height來衡量,因為繪制肯定是完整的星星,這樣height和width是有一個比例的。選取height或width作為其大小衡量本身都可以。
縮放
首先以star的height作為衡量,那么在標準坐標系進行轉換后可以認為star是具備一個默認的縮放系數的:就是它的高度AD(或AC)線段的垂直距離。
之后要為star設置新的高度時(也就是改變其大小范圍——外接矩形邊框outerRect),根據高度的變化進行乘除運算即可——要注意的是坐標問題,這個留給寫代碼時思考。:)
自定義View實現
以上是關于坐標和坐標相關的計算,主要由StarModel
類完成,它持有要顯示的每一個star的數據。
繪制的功能由RatingStarView實現,它繼承了View類:
public class RatingStarView extends View;
View大小測量
自定義控件第一步解決自身大小的測量問題。
前面提到了star的大小由其height決定。
為了同時顯示多個star,而且考慮文章開頭宣稱的那些特性,RatingStarView如何測量自身大小的邏輯也就確定了。
要注意,View測量時的一般準則是需要遵循的:MATCH_PARENT這樣的不限定大小的情況——此時還是優先確定height。
在onMeasure()中:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
float width;
int height; // must have height
if (heightMode == MeasureSpec.EXACTLY) {
height = heightSize;
} else {
height = DEFAULT_STAR_HEIGHT;
if (heightMode == MeasureSpec.AT_MOST) {
height = Math.min(height, heightSize);
}
}
float starHeight = height - getPaddingBottom() - getPaddingTop();
if (widthMode == MeasureSpec.EXACTLY) {
// Parent has told us how big to be. So be it.
width = widthSize;
} else {
// get the perfect width
width = getPaddingLeft() + getPaddingRight();
if (starNum > 0) {
if (starHeight > 0) {
width += starMargin * (starNum - 1);
width += StarModel.getStarWidth(starHeight) * starNum;
}
}
if (widthMode == MeasureSpec.AT_MOST) {
width = Math.min(widthSize, width);
}
}
int widthInt = (int) (width);
if (widthInt < width) {
widthInt++;
}
setMeasuredDimension(widthInt, height);
}
計算時原則是先確定View的height,作為star的高度。
考慮padding,starMargin(星星間距)。
因為是float值相關計算,測量最終大小應該取“向上”的整數。
布局
RatingStarView不是ViewGroup,它不需要布局childView。
但需要根據自身大小確定要顯示的各個star的坐標數據。
在onSizeChanged()中監聽View大小變化,并計算要顯示的star(多個)的坐標數據,也就是private ArrayList<StarModel> starList
:
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if (h != oldh) {
calcStars();
}
}
/**
* Create all stars data, according to the contentWidth/contentHeight.
*/
private void calcStars() {
int paddingLeft = getPaddingLeft();
int paddingTop = getPaddingTop();
int paddingRight = getPaddingRight();
int paddingBottom = getPaddingBottom();
int contentWidth = getWidth() - paddingLeft - paddingRight;
int contentHeight = getHeight() - paddingTop - paddingBottom;
int left = paddingLeft;
int top = paddingTop;
// according to the View's height , make star height.
int starHeight = contentHeight;
if (contentHeight > contentWidth) {
starHeight = contentWidth;
}
if (starHeight <= 0) return;
float startWidth = StarModel.getStarWidth(starHeight);
// starCount * startWidth + (starCount - 1) * starMargin = contentWidth
int starCount = (int) ((contentWidth + starMargin) / (startWidth + starMargin));
if (starCount > starNum) {
starCount = starNum;
}
starList = new ArrayList<>(starCount);
for (int i = 0; i < starCount; i++) {
StarModel star = new StarModel(starThicknessFactor);
starList.add(star);
star.setDrawingOuterRect(left, top, starHeight);
left += startWidth + 0.5f + starMargin;
}
...
}
繪制
繪制五角星
Canvas.drawPath()
可以用來繪制若干個點組成的閉合path。
方法原型:
/**
* Draw the specified path using the specified paint. The path will be
* filled or framed based on the Style in the paint.
*
* @param path The path to be drawn
* @param paint The paint used to draw the path
*/
public void drawPath(@NonNull Path path, @NonNull Paint paint)
但為了繪制“圓角五角星”,需要設置paint的“路徑效果”:
/**
* Set or clear the patheffect object.
* <p />
* Pass null to clear any previous patheffect.
* As a convenience, the parameter passed is also returned.
*
* @param effect May be null. The patheffect to be installed in the paint
* @return effect
*/
public PathEffect setPathEffect(PathEffect effect)
這里設置public class CornerPathEffect extends PathEffect
即可。
滿星和空星
paint可設置其Style。
fullStar:Paint.Style.FILL_AND_STROKE
emptyStar:Paint.Style.STROKE
半星
Canvas支持圖層操作。
可以在第一層繪制空星。
然后在新的圖層中繪制滿星——并利用canvas.clipRect(clip);
來裁剪出一半星星。因為
clipRect是一個矩形,所以其實可以繪制任意小數的星星——只不過0.5(半星)最好看。
Property和Attributes
良好的控件需要支持java代碼和xml中創建及設置它的各個方面。
RatingStarView支持:
- 設置Star數量
- 設置Rating (float值,一個star做1看待)
- 設置thickness
- 其它的間距、大小、寬度、顏色等
它是開源的,你可以自行修改和擴展
事件
RatingStarView支持點擊評分,不支持半星——半星這種是許多用戶評分后的均值。
在onTouchEvent()中記錄點擊的x,y坐標:
@Override
public boolean onTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
clickedX = event.getX();
clickedY = event.getY();
}
return super.onTouchEvent(event);
}
RatingStarView自己實現View.OnClickListener,監聽自身點擊。
在onClick()回調中根據顯示的starList,以及自身大小來改變Rating.
默認它只用來展示評分(只讀),可以通過enableSelectRating屬性開啟點擊評分。
快速使用
gradle依賴
見這里
"https://jitpack.io/#everhad/AndroidRatingStar/v1.0.1".
Add the JitPack repository to your build file
allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}
Add the dependency
dependencies {
compile 'com.github.everhad:AndroidRatingStar:v1.0.1'
}
在 'layout.xml'中
<com.idlestar.ratingstar.RatingStarView
app:cornerRadius="4dp"
app:starMargin="12dp"
app:strokeWidth="2px"
app:strokeColor="#457DD7"
app:starForegroundColor="#DB6958"
app:starBackgroundColor="#E8E8E8"
app:starNum="5"
app:rating="1"
app:enableSelectRating="true"
app:starThickness="0.7"
android:layout_marginTop="8dp"
app:drawStrokeForEmptyStar="false"
app:drawStrokeForHalfStar="true"
android:paddingTop="2dp"
android:paddingLeft="0dp"
android:paddingRight="0dp"
android:background="#fff"
android:layout_width="wrap_content"
android:layout_height="40dp" />
在java代碼中
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
RatingStarView rsv_rating = (RatingStarView) findViewById(R.id.rsv_rating);
rsv_rating.setRating(1.5f);
}
源碼
完整代碼見這里:
https://github.com/everhad/AndroidRatingStar
希望它能節約你的時間(去和UI要各種icon定制RatingBar)
(本文使用Atom編寫)
文章列表