Android自定义view实现评分控件

首先贴上源码地址:https://github.com/CB-ysx/CBRatingBar

最近需要做一个星星的评分控件(可以调整进度,进度颜色渐变)

如图:

image
image

一开始想到的是用系统自带的RatingBar做,但发现了一个问题,实现颜色渐变有点复杂,而且有好几个页面都用了这个,总不能都这样写吧。刚好这段时间看了HenCoder写的Android自定义view系列文章,于是就想自己尝试下实现一个评分控件,可以实现图案的替换,渐变颜色,进度背景,图案个数,大小等参数的自己控制,经过几天的折腾,终于完成了这个控件CBRatingBar

先上效果图:

image

image

image

image

gif效果图:

image

如何使用:

Gradle

  • 在项目的build.gradle中添加如下代码:
    allprojects {
        repositories {
            maven { url 'https://jitpack.io' }
        }
    }
  • 在项目的build.gradle中引入该库:
    dependencies {
        compile 'com.github.CB-ysx:CBRatingBar:2.0.1'
    }

使用方法

  • xml
    <com.cb.ratingbar.CBRatingBar
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
    <com.cb.ratingbar.CBRatingBar
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:starSize="20dp"
        app:starCount="5"
        app:starSpace="10dp"
        app:starStrokeWidth="1dp"
        app:starCanTouch="true"
        app:starMaxProgress="120"
        app:starProgress="60"
        app:starShowStroke="true"
        app:starUseGradient="true"
        app:starStartColor="#0000ff"
        app:starEndColor="#00ff00"
        app:starCoverColor="#ff0000"
        app:starFillColor="#666666"
        app:starPointCount="5"
        app:starStrokeColor="#0f0f0f"
        app:pathData="@string/bird"
        app:pathDataId="@string/bird"/>
  • java
cbRatingBar.setStarSize(20) //大小
        .setStarCount(5) //数量
        .setStarSpace(10) //间距
        .setStarPointCount(5) //角数(n角星)
        .setShowStroke(true) //是否显示边框
        .setStarStrokeColor(Color.parseColor("#00ff00")) //边框颜色
        .setStarStrokeWidth(5) //边框大小
        .setStarFillColor(Color.parseColor("#00ff00")) //填充的背景颜色
        .setStarCoverColor(Color.parseColor("#00ff00")) //填充的进度颜色
        .setStarMaxProgress(120) //最大进度
        .setStarProgress(50) //当前显示的进度
        .setUseGradient(true) //是否使用渐变填充(如果使用则coverColor无效)
        .setStartColor(Color.parseColor("#000000")) //渐变的起点颜色
        .setEndColor(Color.parseColor("#ffffff")) //渐变的终点颜色
        .setCanTouch(true) //是否可以点击
        .setPathData(getResources().getString(R.string.pig))//传入path的数据
        .setPathDataId(R.string.pig)//传入path数据id
        .setDefaultPath()//设置使用默认path
        .setPath(path)//传入path
        .setOnStarTouchListener(new CBRatingBar.OnStarTouchListener() { //点击监听
            @Override
            public void onStarTouch(int touchCount) {
                Toast.makeText(MainActivity.this, "点击第" + touchCount + "个星星", Toast.LENGTH_SHORT).show();
            }
        });

说明

pathData为svg文件中的path数据,如下:

<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
    "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns:xlink="http://www.w3.org/1999/xlink" style="" class="icon" height="16" p-id="2384" t="1506306007922"
     version="1.1" viewBox="0 0 1137 1024" width="17.765625" xmlns="http://www.w3.org/2000/svg">
    <defs>
        <style type="text/css"></style>
    </defs>
    <path
        d="M800.653373 60.04085c-92.533023 0-174.90483 42.957261-228.601405 109.913074-53.696576-66.955813-136.068383-109.913074-228.601406-109.913074-161.838292 0-293.037297 131.199004-293.037297 293.037297 0 34.485875 6.284982 67.452386 17.216998 98.202847 82.149461 249.939217 470.047002 495.868793 504.429116 517.265896 34.374702-21.397103 422.272244-267.326679 504.421705-517.265896 10.932015-30.750461 17.216997-63.716972 17.216997-98.202847-0.007412-161.838292-131.206416-293.037297-293.044708-293.037297z"
        fill="#E24B44" p-id="2385">
    </path>
</svg>

以上path中"M800.653373 ... -293.037297z"这部分数据就是要提交给控件的pathData。


如何实现

为了有更好的扩展性,这我用了path数据来绘制图案,而非单独实现绘制星星,当然,一开始确实只是实现了绘制星星,而且是没有圆角效果的星星(设计图有圆角,先凑合着用吧,哈哈)。于是在网上找到了星星的绘制方法:

/**
     * 获取星星的path
     *
     * @return
     */
    private Path getStarPath(int dx) {
        Path path = new Path();
        float radius;
        if (starPointCount % 2 == 0) {
            radius = starSize * (cos(360.0 / starPointCount / 2) / 2 - sin(360.0 / starPointCount / 2) * sin(90 -
                    360.0 / starPointCount) / cos(90 - 360.0 / starPointCount));
        } else {
            radius = starSize * sin(360.0 / starPointCount / 4) / 2 / sin(180 - 360.0 / starPointCount / 2 - 360.0 /
                    starPointCount / 4);
        }
        for (int i = 0; i < starPointCount; ++i) {
            if (i == 0) {
                path.moveTo(starSize * cos(360 / starPointCount * i) / 2, starSize * sin(360 / starPointCount * i) /
                        2 + dx);
            } else {
                path.lineTo(starSize * cos(360.0 / starPointCount * i) / 2, starSize * sin(360.0 / starPointCount *
                        i) / 2 + dx);
            }
            path.lineTo(radius * cos(360.0 / starPointCount * i + 360.0 / starPointCount / 2), radius * sin(360.0 /
                    starPointCount * i + 360.0 / starPointCount / 2) + dx);
        }
        path.close();

        return path;
    }

其中dx可以先不用管,这是后面需要绘制多个星星用到的,这段代码就可以得到星星的path,至于为什么是这样计算,这我就没有去探究了(赶项目要紧),这段代码绘制出来的图案如下:

image

没错,只能绘制一个星星,那要如何绘制多个星星呢,这里就用到了dx了,dx是星星的偏移量(星星宽度+两个星星之间的间距),通过这个偏移量获取不同位置的星星path,再绘制到画布上就能实现多个星星了,代码如下:

canvas.translate(dx, dy);
canvas.rotate(-90);

int x = 0;
for (int i = 0; i < starCount; ++i) {
    Path path = getStarPath(x);
    canvas.drawPath(patpaint);
    x += (starSize + starSpace);
}

canvas.rotate(90);
canvas.translate(-dx, -dy);

starCount就是星星的个数
starSize就是星星的大小
starSpace就是两个星星之间的间距

此时绘制出来的星星是填充了颜色的星星,但是还没有进度条,更没有渐变的进度条效果。
绘制效果如图:

image

接下来就是实现进度条,先实现纯色的进度条,本来绘制进度很简单的,但是由于星星属于不太规则的图案(虽然已经很规则了,但相对于矩形、圆形这些来说还是不规则的,哈哈,不要吐槽我),加上考虑到扩展性(可能使用的是其他真正不规则的图案),这里用了另一种方法来填充不规则图形。
代码如下:

//将星星绘制到star上
Bitmap star = Bitmap.createBitmap(width, starSizeBitmap.Config.ARGB_8888);
Canvas starCanvas = neCanvas(star);
drawStar(starCanvas, starFillPaint);

//在star上填充颜色
Bitmap finalStar = Bitmap.createBitmap(width, starSizeBitmap.Config.ARGB_8888);
Canvas canvas = neCanvas(finalStar);
canvas.save();
canvas.clipRect(0, 0, dx, starSize);
canvas.drawRect(0, 0, widthstarSize, starCoverPaint);
canvas.restore();

canvas.drawBitmap(star, 0, 0, starPaint);

先创建一个宽为width(由星星个数及间距计算得出)高为starSize(单个星星的大小)的Bitmap,然后把星星以未选中时的背景色绘制到Bitmap上,这时得到的就是没有任何进度的图。接下来是绘制进度,我们需要再创建一个Bitmap,然后使用clipRect方法裁剪高度为starSize,宽度为dx(进度)的矩形局域,然后填充进度的颜色(可以是纯色也可以是渐变色),最后把绘制了星星的bitmap绘制到这个画布上,采用PorterDuffXfermode的画笔,即可得到填充了进度的星星效果,如图:

image

设置画笔:

starPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_ATOP));

关于画笔的设置,可参考HenCoder的这篇文章HenCoder Android 开发进阶: 自定义 View 1-2 Paint 详解,不得不说,这几篇文章都写得很不错,对我来说,收获真的很多。

这样就完成了CBRatingBar的第一个版本。用在app上,又方便效果也还不错,不足的是只有一个图案,没办法自定义,于是就有了第二个版本,开发2.0.0版本虽然只是加多了自定义path数据,但是这一过程也是挺坎坷的。

研究png图片提取path数据---无果;
研究svg图片中的path数据---有点希望;
于是开始学习svg语法,自己实现将svg中的pathData转为Android绘图中的path数据,到了画弧线这些命令就卡住了,还有网上看了一些svg数据,发现其实挺坑的,有的用‘,’分割,有的用空格,感觉我的算法并不能很好地识别,又纠结了一段时间。最后在github发现了RichPath这个库,发现了该库中有实现从svg提取path的算法,于是就拿过来用了,在此感谢tarek360提供的算法。

有了这个剩下的就简单多了,加多几个方法,传入pathData数据,将之前获取星星的path方法,改为可以从pathData中提取,这样就实现了可以自定义图案,具体代码如下:

/**
* 初始化path
*
* @return
*/
private void initPath() {
    if (pathData != null && !"".equals(pathData.trim().replace(" ", ""))) {
        mPath = PathParserCompat.createPathFromPathData(pathData);
        isSelfPath = true;
    } else if (pathDataId != -1) {
        mPath = PathParserCompat.createPathFromPathData(getResources().getString(pathDataId));
        isSelfPath = true;
    } else {
        isSelfPath = false;
    }
    if (isSelfPath) {
        resizePath(mPath, starSize, starSize);
    }
}

其中的pathData就是原始数据,PathParserCompat就是用来将数据转为path的。由于提取出来的path是svg本来的大小,所以需要将它缩减为我们设置的大小,也就是用resizePath这个方法:

public void resizePath(Path path, float width, float height) {
    RectF bounds = new RectF(0, 0, width, height);
    RectF src = new RectF();
    path.computeBounds(src, true);
    Matrix resizeMatrix = new Matrix();
    resizeMatrix.setRectToRect(src, bounds, Matrix.ScaleToFit.FILL);
    path.transform(resizeMatrix);
}

这样我们就可以很方便地使用我们自己的图案来绘制了。
最后再贴上源码地址:https://github.com/CB-ysx/CBRatingBar
欢迎star~

关于svg语法,可查看W3C School

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容