iOS星级评价的两种实现方式

图片来自 Pixabay

如何在 iOS 应用上用自定义视图实现星级评分功能呢?最近研究学习了两个实现方法,现总结如下:

一、实现方式一

参考 Ray Wenderlich 系列教程:

UIView Tutorial for iOS: How To Make a Custom UIView in iOS 5: A 5 Star Rating View

1.Demo 示例

2.实现的功能:

  1. 可任意设置星星数量;
  2. 可以设置星星是否可编辑、星星的左边距、星星之间的间隔等;
  3. 整星评价和半星评价;
  4. 不仅仅是点击触摸屏幕评分,还可以实现手指按住屏幕滑动评分;
  5. 用 Delegate 方式实现回调;

3.视图层级

视图层级

视图的层级比较简单,一个自定义 UIView 子类对象,上面添加了 5 个星星图片对象,即 UIImageView 对象。

评分值修改时,我们需要遍历这5个 UIImageView 对象,并对他们的图片进行相应的更改。

4.实现原理:根据 rating 值遍历设置每个星星图案

讲解一下大概的实现思路,具体内容可以参考源码:GitHub

  1. 首先需要三张星星图片:空星图片、半星图片、全星图片:
kermit_empty
kermit_half
kermit_full
  1. 初始化设置最大评分数 maxRating(即总共的星星数量)时,就把所有的 imageView 添加到视图上,然后发送 [self setNeedsLayout] 方法让系统布局子视图(即 layoutSubviews),计算每个 imageView 的位置和大小:

    /*
     设置最大评分数,它决定了我们会有多少个 UIImageView 子视图
     做了两件事:
     1.添加图片:根据最大评分数初始化图片数量,移除旧图片,添加新图片。
     2.刷新UI,设置图片大小:调用 setNeedsLayout 方法后,系统会调用 layoutSubviews 方法来设置每个图片的位置和大小。
     */
    - (void)setMaxRating:(int)maxRating {
        _maxRating = maxRating;
        
        // 移除所有旧的图片
        for (int i = 0; i < self.imageViews.count; i++) {
            UIImageView *imageView = (UIImageView *)[self.imageViews objectAtIndex:i];
            [imageView removeFromSuperview];
        }
        [self.imageViews removeAllObjects];
        
        // 重新添加新的图片
        for (int i = 0; i < maxRating; i++) {
            UIImageView *imageView = [[UIImageView alloc] init];
            imageView.contentMode = UIViewContentModeScaleAspectFit;
            [self.imageViews addObject:imageView];
            [self addSubview:imageView];
        }
        
        // 重新布局、刷新UI
        [self setNeedsLayout];
        [self refresh];
    }
    
    // 设置合适的子视图大小
    - (void)layoutSubviews {
        [super layoutSubviews];
        
        if (!self.notSelectedImage) {
            return;
        }
        
        /*
         设置每个五角星的尺寸大小
         这里,教程中的计算方法如下,请仔细看,他的数学估计是体育老师教的吧。😂😂😂
         float desiredImageWidth = (self.frame.size.width - (self.leftMargin*2) - (self.midMargin*self.imageViews.count)) / self.imageViews.count;
         */
        float desiredImageWidth = (self.frame.size.width - self.leftMargin*2 -(self.midMargin*(self.imageViews.count-1))) / self.imageViews.count;
        float imageWidth = MAX(self.minImageSize.width, desiredImageWidth);
        float imageHeight = MAX(self.minImageSize.height, desiredImageWidth);
        for (int i = 0; i < self.imageViews.count; i++) {
            
            UIImageView *imageView = [self.imageViews objectAtIndex:i];
            CGRect imageFrame = CGRectMake(self.leftMargin + i*(self.midMargin+imageWidth), 0, imageWidth, imageHeight);
            imageView.frame = imageFrame;
        
        }
    }
    
  2. 以上两个步骤中,我们设置了最大评分数、添加并布局好了所有的 imageView。接下来,需要接收用户事件,根据用户手指触摸的位置计算评分值 rating:

    首先,要接受用户的触摸事件:

    因为 UIViewUIResponder 的子类,所以覆盖以下四个方法就可以处理四种不同的触摸事件:

    // ① 一根手指或多根手指触摸屏幕
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
    
    // ② 一根手指或多根手指在屏幕上移动(随着手指的移动,相关的对象会持续发送该消息)
    - (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
    
    // ③ 一根手指或多根手指离开屏幕
    - (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
    
    // ④ 在触摸操作正常结束前,某个系统事件(例如突然来电话)打断了触摸过程
    - (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
    

    这里我们只用到了前三个方法:

    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        UITouch *touch = [touches anyObject];
        CGPoint touchLocation = [touch locationInView:self];
        [self handleTouchAtLocation:touchLocation];
    }
    
    - (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        UITouch *touch = [touches anyObject];
        CGPoint touchLocation = [touch locationInView:self];
        [self handleTouchAtLocation:touchLocation];
    }
    
    - (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        [self.delegate rateView:self ratingDidChange:self.rating];
    }
    

    最最重要的是,计算评分值的方法:将当前手指触摸位置的 X 值与每个 imageView 宽度的中点作比较

    五角星-2
    1. 如果触摸位置的X值 > 当前图片宽度的中点,那 rating 值就是整数,是全星评价。
    2. 如果 触摸位置的X值 <= 当前图片宽带的中点,并且触摸位置的X值 > 当前图片的起始值,那 rating 值就是 非整数,半星评价。
    // 根据手指触摸位置,计算当前评分值
    - (void)handleTouchAtLocation:(CGPoint)touchLocation {
        if (!self.editable) {
            return;
        }
        
        float newRating = 0;
        for (NSInteger i = self.imageViews.count - 1; i >= 0 ; i--) {
            UIImageView *imageView = [self.imageViews objectAtIndex:i];
            CGFloat originValue = imageView.frame.origin.x;
            CGFloat midValue = originValue + imageView.frame.size.width / 2;
                    
            if (touchLocation.x > midValue) {
                newRating = i+1;
                break;
            }else if ((touchLocation.x > originValue) && (touchLocation.x <= midValue)) {
                newRating = i + 0.5;
                break;
            }else {
                continue;
            }
        }
        
        self.rating = newRating;
    }
    
    - (void)setRating:(float)rating {
        _rating = rating;
        [self refresh]; // 每次评分值改变,就调用刷新方法,重新设置 imageView.![Demo](StarRateView/image/Demo.gif)
    }
    
  3. 上面一个步骤,我们得到了评分值。最后,每次 rating 值该改变,就重新设置 imageView:

    // 刷新视图,根据当前评分修改对应的五角星图片
    - (void)refresh {
        // 从左往右遍历每个小星星
        for (int i = 0; i < self.imageViews.count; i++) {
            UIImageView *imageView = [self.imageViews objectAtIndex:i];
            if (self.rating >= i+1) {
                // 如果 rating >= 该星星图片的索引,则该星星是全星图片。
                imageView.image = self.fullSelectedImage;
            }else if (self.rating > i) {
                // 如果 rating > 上一个图片索引,则该星星是半星图片。
                imageView.image = self.halfSelectedImage;
            }else {
                // 默认,空星图片。
                imageView.image = self.notSelectedImage;
            }
        }
    }
    
  4. 整个实现思路大致如上。

实现方式二

参考 @XHJCoder 的源码:

简书:【iOS】实现星级评分

1.Demo示例

2.实现的功能

  1. 可任意设置星星数量;
  2. 支持动画修改评论;
  3. 评分样式支持:整星评论、半星评论、不完整星星评论;
  4. 支持 block 和 delegate 两种方式返回修改结果;

3.实现原理,根据 rating 值整体修改视图的 frame 宽度

我对源码 fork 并进行了一些修改:XHStarRateView

下面是视图的层级结构:

视图层级
  • backgroundStarView 、foregroundStarView 是 UIView 的实例对象。
  • backgroundStarView 视图上添加5个灰色的 imageView。
  • foregroundStarView 视图上添加5个黄色的 image View。
  1. 初始化时添加所有视图:

    // 指定初始化方法
    - (instancetype)initWithFrame:(CGRect)frame
                     numberOfStar:(NSInteger)numberOfStar
                        rateStyle:(XHStarRateViewRateStye)rateStyle
                      isAnimation:(BOOL)isAnimation
                       completion:(XHStarRateViewRateCompletionBlock)completionBlock
                         delegate:(id)delegate
    {
        if (self = [super initWithFrame:frame]) {
            _numberOfStar    = numberOfStar;
            _rateStyle       = rateStyle;
            _isAnimation     = isAnimation;
            _completionBlock = completionBlock;
            _delegate        = delegate;
            [self createStarView];
        }
        return self;
    }
    
    - (void)createStarView {
        self.foregroundStarView = [self createStarViewWithImageNamed:KForegroundStarImage];
        self.backgroundStarView = [self createStarViewWithImageNamed:KBackgroundStarImage];
        
        self.foregroundStarView.frame = CGRectMake(0, 0, self.bounds.size.width * _currentRating / _numberOfStar, self.bounds.size.height);
        [self addSubview:self.backgroundStarView];
        [self addSubview:self.foregroundStarView];
    
        //...
    }
    
    - (UIView *)createStarViewWithImageNamed:(NSString *)name {
        UIView *view = [[UIView alloc] initWithFrame:self.bounds];
        view.clipsToBounds = YES;
        view.backgroundColor = [UIColor clearColor];
        for (NSInteger i = 0; i < _numberOfStar; i ++) {
            UIImageView *imageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:name]];
            imageView.frame = CGRectMake(i * self.bounds.size.width / _numberOfStar, 0, self.bounds.size.width / _numberOfStar, self.bounds.size.height);
            imageView.contentMode = UIViewContentModeScaleAspectFit;
            [view addSubview:imageView];
        }
        return view;
    }
    
  2. 使用 UITapGestureRecognizer 实现手势识别并更新 rating 值:

    - (void)createStarView {
     //...
     
        UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(userTapRateView:)];
        tapGesture.numberOfTapsRequired = 1;
        [self addGestureRecognizer:tapGesture];
    }
    
    // 这里面用到了两个数学函数
    // ceilf():返回大于或者等于指定表达式的最小整数。
    // roundf():返回四舍五入的整数。
    - (void)userTapRateView:(UITapGestureRecognizer *)gesture {
        
        CGPoint tapPoint = [gesture locationInView:self];
        CGFloat offset = tapPoint.x;
        CGFloat realRating = offset / (self.bounds.size.width / _numberOfStar);
    
        switch (_rateStyle) {
            case XHStarRateViewRateStyeFullStar: {
                self.currentRating = ceilf(realRating);
                break;
            }
            case XHStarRateViewRateStyeHalfStar: {
                float round = roundf(realRating);
                self.currentRating = (round > realRating) ? round : (round + 0.5);
                break;
            }
            case XHStarRateViewRateStyeIncompleteStar: {
                self.currentRating = realRating;
                break;
            }
        }
    }
    
  3. 每次更新 rating 值就修改 foregroundStarView 的 frame 宽度实现动画效果。

    - (void)layoutSubviews {
        [super layoutSubviews];
        
        CGFloat animationDuration = (self.isAnimation ? 0.2 : 0);
        [UIView animateWithDuration:animationDuration animations:^{
            self.foregroundStarView.frame = CGRectMake(0, 0, self.bounds.size.width / self.numberOfStar * self.currentRating, self.bounds.size.height);
        }];
    }
    
  4. OK,第二种方法就是实现如上。

  5. 第二种方法封装得相对第一种方法更好一些,调用也很方便:

        /*
         1. Delegate 方式创建
         */
        XHStarRateView *starRateView = [[XHStarRateView alloc] initWithFrame:CGRectMake(20, 60, 200, 30)];
        starRateView.isAnimation = YES; // 有动画
        starRateView.rateStyle = XHStarRateViewRateStyeIncompleteStar; //允许不完整星评论
        starRateView.tag = 1;
        starRateView.delegate = self;
        [self.view addSubview:starRateView];
        
        /*
         2. 初始化方法创建
         半星评论、无动画
         */
        XHStarRateView *starRateView2 = [[XHStarRateView alloc] initWithFrame:CGRectMake(20, 100, 200, 30)
                                                                 numberOfStar:5
                                                                    rateStyle:XHStarRateViewRateStyeHalfStar
                                                                  isAnimation:NO
                                                                     delegate:self];
        starRateView2.tag = 2;
        [self.view addSubview:starRateView2];
        
        /*
         3. block 方法1
         默认设置:完整星评论、
         */
        XHStarRateView *starRateView3 = [[XHStarRateView alloc] initWithFrame:CGRectMake(20, 140, 200, 30) completion:^(CGFloat currentScore) {
            NSLog(@"3----  %f",currentScore);
        }];
        
        [self.view addSubview:starRateView3];
        
        /*
         4. block 方法2
         半星评论、有动画
         */
        XHStarRateView *starRateView4 = [[XHStarRateView alloc] initWithFrame:CGRectMake(20, 180, 200, 30) numberOfStar:8 rateStyle:XHStarRateViewRateStyeHalfStar isAnimation:YES completion:^(CGFloat currentScore) {
            NSLog(@"4----  %f",currentScore);
        }];
        [self.view addSubview:starRateView4];
    

第三方框架

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