Android自定義的評分控件,相似RatingBar那樣的,使用星星圖標(full、half、empty)做爲rating值的「評分/打分控件」。java
圖1:
RatingStarView控件支持的特性:android
下面是RatingStarView的實現設計。git
能夠在抽象的xOy座標系中計算獲得一個star的「標準座標」。這個座標能夠做爲後續有關座標計算(偏移和縮放)的基準。
圖2:github
以上面的圖爲例,這裏其中心點O爲原點。
這裏爲了描述方便,稱A,B,C,D,E爲5個外點(Outer Corner Vertex),a,b,c,d,e 五個點爲內點(Inner Corner Vertex)。canvas
這裏座標值的選取徹底出於計算方便來考慮,實現方式畢竟不少,你們能夠選取其它座標方式,好比原點O的位置在其它處,或者星星的範圍由高度、寬度表示等。數組
A拐點的座標爲(0,1),其它幾個點的座標根據幾何公式是能夠固定下來的。爲了簡化計算,能夠將這幾個值做爲常量保存,以後的其它值的計算基於它們。下面代碼爲了程序上的便利E點座標x,y值是起始元素:app
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來的。maven
這裏爲star引入「胖度係數(star thickness)」的說法,用來控制星星的可愛程度。
很明顯,胖度是由a,b,c,d,e五個內點的位置決定的。
但在計算上,這裏採起另外一種方式:ide
設置變量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; }
StarModel類使用靜態的數組保存ABCDE五個外點的標準座標系下的座標初始值。
由於thickness係數必須在標準座標系下計算,這裏選擇StarModel的構造函數中接受thickness參數,並且初始化中完成全部10個拐點的計算。
手機設備下,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),根據高度的變化進行乘除運算便可——要注意的是座標問題,這個留給寫代碼時思考。:)
以上是關於座標和座標相關的計算,主要由StarModel
類完成,它持有要顯示的每個star的數據。
繪製的功能由RatingStarView實現,它繼承了View類:
public class RatingStarView extends 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(半星)最好看。
良好的控件須要支持java代碼和xml中建立及設置它的各個方面。
RatingStarView支持:
它是開源的,你能夠自行修改和擴展
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屬性開啓點擊評分。
見這裏
"https://jitpack.io/#everhad/AndroidRatingStar/v1.0.1".
allprojects { repositories { ... maven { url 'https://jitpack.io' } } }
dependencies { compile 'com.github.everhad:AndroidRatingStar:v1.0.1' }
<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" />
@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) 個人博客即將搬運同步至騰訊雲+社區,邀請你們一同入駐:https://cloud.tencent.com/developer/support-plan (本文使用Atom編寫)