如何开发一个评分控件(一):UIBezierPath的操作

评分控件在APP中非常常用,一般常见的形状为星型或者心型(❤)。一般我们比较常用的方法是一个一个图片或者按钮去堆砌,但是这样应付简单整数可以,比如4.7分这样非整数就并不好显示。本文将实现一个能复用的评分控件,不仅仅可以准确地显示非整数的评分,也可以灵活地自定义不同的参数,甚至设置各种不同的形状。

背景

我们主要是使用UIBezierPath对象来实现星星或者心(❤)的形状,我们自己用代码去写不同的形状就比较复杂了,这里我们会使用PaintCode来减少工作量。PaintCode的具体操作就不多做介绍了,大家可以参考下面的文章:

PaintCode 教程一
PaintCode 教程二
使用paitcode来完成applewatch刻度盘动画效果
使用paintcode 制作一个星星评分视图

其实在上面的文章已经有星星评分控件的一种实现方法了,不过存在着不能使用非纯色或者透明色背景颜色的缺点,而且也没有进行封装。我们会通过UIBezierPath对象的运算操作来改善这个问题。

UIBezierPath

我们首先将PaintCode生成代码复制到自定义UIView的- (void)drawRect:(CGRect)rect方法中,代码如下:

- (void)drawRect:(CGRect)rect {
    UIBezierPath* starPath = [UIBezierPath bezierPath];
    [starPath moveToPoint: CGPointMake(56, 13)];
    [starPath addLineToPoint: CGPointMake(64.82, 25.86)];
    [starPath addLineToPoint: CGPointMake(79.78, 30.27)];
    [starPath addLineToPoint: CGPointMake(70.27, 42.64)];
    [starPath addLineToPoint: CGPointMake(70.69, 58.23)];
    [starPath addLineToPoint: CGPointMake(56, 53)];
    [starPath addLineToPoint: CGPointMake(41.31, 58.23)];
    [starPath addLineToPoint: CGPointMake(41.73, 42.64)];
    [starPath addLineToPoint: CGPointMake(32.22, 30.27)];
    [starPath addLineToPoint: CGPointMake(47.18, 25.86)];
    [starPath closePath];
    
    [UIColor.grayColor setFill];
    [starPath fill];
}

代码生成的界面也很简单,就是一个灰色的星星。

灰色星星.png

星星已经得到了,现在我们想要只显示半颗星星,这样就可以实现我们想要的0.5分的效果了。那如何做呢?
办法当然有了,我们可以获取UIBezierPath对象的位置和尺寸,然后拿一个方块去遮住它的半边,那不就可以了。继续在自定义UIView的- (void)drawRect:(CGRect)rect方法后面添加代码:

    //获取path的尺寸和位置
    CGRect pathBounds = starPath.bounds;
    pathBounds.size.width = pathBounds.size.width * 0.5;
    UIBezierPath *rectPath = [UIBezierPath bezierPathWithRect:pathBounds];
    [UIColor.redColor setFill];
    [rectPath fill];
遮住的星星.png

如果把颜色改成白色的,那么半个星星已经达到了。

组合与裁剪

下面我们将通过一系列的魔法来实现星星,半边是红色的,半边是灰色的,这个魔法就是UIBezierPath对象的组合和裁剪方法。使用- (void)appendPath:(UIBezierPath *)*bezierPath*可以添加其他的UIBezierPath对象,而设置usesEvenOddFillRule就可以决定最终的路径是交集(Intersection)或是并集(Union),从而达到我们想要的裁剪效果。

好了,我们继续在自定义UIView的- (void)drawRect:(CGRect)rect方法后面添加代码:

    [starPath appendPath:rectPath];
    [starPath setUsesEvenOddFillRule:YES];
    [starPath addClip];
    [[UIColor yellowColor] setFill];
    [starPath fill];
第一次组合.png

从上面的效果可以看出来,usesEvenOddFillRule的效果是将两个路径组合在一起并去除掉交集,所以这次的路径并不包括左边的半颗星星。我们如果用现在的路径继续去和rectPath去组合,就能得到右边的半颗星星了。

我们在drawRect方法中添加代码:

    [starPath appendPath:rectPath];
    [starPath setUsesEvenOddFillRule:YES];
    [starPath addClip];
    [[UIColor blueColor] setFill];
    [starPath fill];
第二次组合.png

经过第二次的组合操作, 我们可以看到效果已经非常接近想要的效果,我们现在可以试着把组合过程的红色和黄色代码都去掉,效果已经出来了啊。

去掉组合颜色.png

算是已经达成目标了,我们刚开始的灰色替换成红色,把蓝色替换成灰色,就成了想要的效果了。我们也可以尝试把rectPath的宽度比例修改成其他比例,效果如下:

宽度比例为0.3.png

宽度比例为0.5.png

宽度比例为0.8.png

好啦,单个星星的比例已经搞定了,此时的自定义的UIView也可以其他背景颜色,都不会影响我们的星星形状了。下面就是最终的代码:

- (void)drawRect:(CGRect)rect {
    UIBezierPath *starPath = [UIBezierPath bezierPath];
    [starPath moveToPoint: CGPointMake(56, 13)];
    [starPath addLineToPoint: CGPointMake(64.82, 25.86)];
    [starPath addLineToPoint: CGPointMake(79.78, 30.27)];
    [starPath addLineToPoint: CGPointMake(70.27, 42.64)];
    [starPath addLineToPoint: CGPointMake(70.69, 58.23)];
    [starPath addLineToPoint: CGPointMake(56, 53)];
    [starPath addLineToPoint: CGPointMake(41.31, 58.23)];
    [starPath addLineToPoint: CGPointMake(41.73, 42.64)];
    [starPath addLineToPoint: CGPointMake(32.22, 30.27)];
    [starPath addLineToPoint: CGPointMake(47.18, 25.86)];
    [starPath closePath];
    
    [[UIColor redColor] setFill];
    [starPath fill];
    
    //获取path的尺寸和位置
    CGRect pathBounds = starPath.bounds;
    pathBounds.size.width = pathBounds.size.width * 0.8;
    UIBezierPath *rectPath = [UIBezierPath bezierPathWithRect:pathBounds];
    
    [starPath appendPath:rectPath];
    [starPath setUsesEvenOddFillRule:YES];
    [starPath addClip];

    [starPath appendPath:rectPath];
    [starPath setUsesEvenOddFillRule:YES];
    [starPath addClip];
    [[UIColor grayColor] setFill];
    [starPath fill];
}

平移和缩放

通过上一步的操作我们已经解决了星星不同比例的问题,但是还存在一个很大的问题,PaintCode生成的UIBezierPath是固定大小的,而我们所使用的控件需要通过frame或者constraint(约束)来改变大小的。不过UIBezierPath对象是矢量的,也就是说可以通过平移和缩放来适配UIView的大小。

这一次我们将通过复制PaintCode生成的UIBezierPath对象,然后通过平移和缩放操作来改变Path的位置,最后组合成5颗星星,而且大小可以随着UIView的改变而改变。

依然通过自定义UIView的- (void)drawRect:(CGRect)rect方法中添加一个灰色的星星:

- (void)drawRect:(CGRect)rect {
    UIBezierPath* starPath = [UIBezierPath bezierPath];
    [starPath moveToPoint: CGPointMake(56, 13)];
    [starPath addLineToPoint: CGPointMake(64.82, 25.86)];
    [starPath addLineToPoint: CGPointMake(79.78, 30.27)];
    [starPath addLineToPoint: CGPointMake(70.27, 42.64)];
    [starPath addLineToPoint: CGPointMake(70.69, 58.23)];
    [starPath addLineToPoint: CGPointMake(56, 53)];
    [starPath addLineToPoint: CGPointMake(41.31, 58.23)];
    [starPath addLineToPoint: CGPointMake(41.73, 42.64)];
    [starPath addLineToPoint: CGPointMake(32.22, 30.27)];
    [starPath addLineToPoint: CGPointMake(47.18, 25.86)];
    [starPath closePath];
    
    [UIColor.grayColor setFill];
    [starPath fill];
}

不过我们为了显示明显,讲UIView的背景颜色设置为橙色了。

带背景颜色的灰色星星.png

我们是通过UIBezierPath对象的- (void)applyTransform:(CGAffineTransform)*transform*方法来进行形变的,在drawRect方法中继续添加形变代码:

    //复制平移操作
    CGRect pathBounds = starPath.bounds;
    UIBezierPath *totalPath = [UIBezierPath bezierPath];
    for (int i = 0; i < 5; i ++) {
        UIBezierPath *copyPath = [UIBezierPath bezierPath];
        [copyPath appendPath:starPath];
        [copyPath applyTransform:CGAffineTransformMakeTranslation((pathBounds.size.width + 5.0)*i,0)];
        [totalPath appendPath:copyPath];
    }

    [[UIColor greenColor] setFill];
    [totalPath fill];

通过复制和平移动作,我们就得到了五个绿色的星星啦,并且把路径存在totalPath中。

复制平移为五颗星星.png

虽然我们得到了五颗星星,但是它还是大小固定的,而且星星的位置不是居中的,我们将通过缩放操作来适配大小。继续在复制平移的操作后面添加代码:

    //缩放操作    
    CGRect totalPathRect = totalPath.bounds;
    CGFloat scale;
    if (totalPathRect.size.width / totalPathRect.size.height >= rect.size.width / rect.size.height) {
        scale = rect.size.width / totalPathRect.size.width;
    }else {
        scale = rect.size.height / totalPathRect.size.height;
    }
    [totalPath applyTransform:CGAffineTransformMakeScale(scale,scale)];
缩放操作之后的星星.png

缩放操作之后的星星位置不对,我们需要通过平移对位置进行修正,同时把刚开始填充的灰色去掉。

    //修正origin的x,y值, 使path位置居中
    totalPathRect = totalPath.bounds;
    CGFloat x = (rect.size.width - totalPathRect.size.width) / 2.0;
    x = x - totalPathRect.origin.x;
    CGFloat y = (rect.size.height - totalPathRect.size.height) / 2.0;
    y = y - totalPathRect.origin.y;
    [totalPath applyTransform:CGAffineTransformMakeTranslation(x, y)];

修正后的效果如下:

修正位置的效果.png

本文的代码可以在https://github.com/aijun198600/AJScoreViewExample查看。

下一篇文章我们将实现星星控件的封装,其中会使用CALayer的mask遮罩来实现控件。
如果需要使用评分控件的话可以直接去https://github.com/aijun198600/AJScoreView下载使用,其中使用drawRect实现的方法在drawRect分支上。

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

推荐阅读更多精彩内容