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编写)