iOS-实现星级评分(star score)

我们知道,很多app都有星星评分的功能,特别是商城app,需要你对商品质量、发货速度、服务态度等进行打分。
项目开发的app正好也需要这个功能,于是自己进行了封装,使用起来也是很简单,满足大部分功能需要,功能如下:

  • 可全星打分
  • 可半星打分
  • 可不完整星打分
  • 可点击、可滑动打分
  • 可设置星星数量、大小、间隔等
    一切根据你的需要来定制,看一下效果图。


    星星打分效果图.png

使用也是非常简单的

GBStarRateView *starRateView = [[GBStarRateView alloc] initWithFrame:CGRectMake(150, 30, 200, 10) style:GBStarRateViewStyleHalfStar numberOfStars:5 isAnimation:YES delegate:self];
            [cell.contentView addSubview:starRateView];
            starRateView.numberOfStars = 5;
            starRateView.style = GBStarRateViewStyleHalfStar;

首先看.h头文件

typedef NS_ENUM(NSInteger, GBStarRateViewStyle){
    
    GBStarRateViewStyleWholeStar = 0,//全星评分
    GBStarRateViewStyleHalfStar = 1, //可半星评分
    GBStarRateViewStyleIncompleteStar = 2,//不完整星评分
};
@class GBStarRateView;
@protocol GBStarRateViewDelegate <NSObject>
@optional
- (void)starRateView:(GBStarRateView *)starRateView didSelecteStarAtStarRate:(CGFloat)starRate;
@end

typedef void(^GBStarRateDidSelectStarBlock)(GBStarRateView *starRateView, CGFloat starRate);

@interface GBStarRateView : UIView

@property (nonatomic, assign) GBStarRateViewStyle style; //星星评分样式
@property (nonatomic, weak) id <GBStarRateViewDelegate> delegate; //代理
@property (nonatomic, assign) NSInteger numberOfStars; //星星数量 默认为5
@property (nonatomic, assign) CGFloat currentStarRate; //当前打分 默认为0.0
@property (nonatomic, assign) CGFloat spacingBetweenStars; //星星间隔 默认为10
@property (nonatomic, assign) CGSize starSize; //星星尺寸 默认 {24,24}
@property (nonatomic, strong) UIImage *starImage; //未选中star image 有默认图片
@property (nonatomic, strong) UIImage *currentStarImage; //选中star image 有默认图片
@property (nonatomic, assign) BOOL isAnimation; //是否动画 默认YES
@property (nonatomic, assign) BOOL allowSlideScore; //是否滑动打分 默认NO 适用于不完整星星打分
@property (nonatomic, assign) BOOL allowClickScore; //是否点击打分 默认YES
@property (nonatomic, copy) GBStarRateDidSelectStarBlock didSelectStarBlock;//点击或滑动打分的回调

- (instancetype)initWithFrame:(CGRect)frame style:(GBStarRateViewStyle)style numberOfStars:(NSInteger)numbersOfStars isAnimation:(BOOL)isAnimation delegate:(id <GBStarRateViewDelegate>)delegate;

- (instancetype)initWithFrame:(CGRect)frame style:(GBStarRateViewStyle)style numberOfStars:(NSInteger)numbersOfStars isAnimation:(BOOL)isAnimation finish:(GBStarRateDidSelectStarBlock)finish;
@end

然后再看一下.m的实现

static CGFloat const kAnimatinDuration = 0.3;

@interface GBStarRateView ()

@property (nonatomic, strong) NSMutableArray <UIView *> *starsContentViews;

@end


@implementation GBStarRateView

@synthesize starSize = _starSize;

- (instancetype)initWithFrame:(CGRect)frame style:(GBStarRateViewStyle)style numberOfStars:(NSInteger)numbersOfStars isAnimation:(BOOL)isAnimation finish:(GBStarRateDidSelectStarBlock)finish {
    
    if (self = [super initWithFrame:frame]) {
        [self config];
        self.style = style;
        self.numberOfStars = numbersOfStars;
        self.isAnimation = isAnimation;
        self.didSelectStarBlock = finish;
    }
    return self;
}

- (instancetype)initWithFrame:(CGRect)frame style:(GBStarRateViewStyle)style numberOfStars:(NSInteger)numbersOfStars isAnimation:(BOOL)isAnimation delegate:(id<GBStarRateViewDelegate>)delegate {
    
    if (self = [super initWithFrame:frame]) {
        [self config];
        self.style = style;
        self.numberOfStars = numbersOfStars;
        self.isAnimation = isAnimation;
        self.delegate = delegate;
    }
    return self;
}

- (id)init {
    
    if (self = [super init]) {
        
        [self config];
    }
    return self;
}

- (id)initWithFrame:(CGRect)frame {
    if (self = [super initWithFrame:frame]) {
        
        [self config];
    }
    return self;
}

- (id)initWithCoder:(NSCoder *)aDecoder {
    
    if (self = [super initWithCoder:aDecoder]) {
        
        [self config];
    }
    return self;
}
#pragma mark - 基本配置
- (void)config {
    
    self.style = GBStarRateViewStyleWholeStar;
    self.numberOfStars = 5;
    self.spacingBetweenStars = 10;
    _starSize = CGSizeMake(24, 24);
    self.currentStarImage = [UIImage imageNamed:@"ic_star_selected"];
    self.starImage = [UIImage imageNamed:@"ic_star_default"];
    self.isAnimation = YES;
    self.allowClickScore = YES;
    self.allowSlideScore = NO;
}

#pragma mark - 创建视图
- (void)resetStarsContentView {
    
    for (UIView *starContentView in self.starsContentViews) {
        [starContentView removeFromSuperview];
    }
    [self.starsContentViews removeAllObjects];
    
    [self createStarsContentView:self.starImage starRate:_numberOfStars];
    [self createStarsContentView:self.currentStarImage starRate:_currentStarRate];
}

- (void)createStarsContentView:(UIImage *)starImage starRate:(CGFloat)starRate {
    
    if (self.numberOfStars == 0) {
        return;
    }
    CGRect frame = [self frameForStarsContentViewAtCurrentStarRate:starRate];
    UIView *starsContentView = [[UIView alloc] initWithFrame:frame];
    starsContentView.clipsToBounds = YES;//必须要设置,不设试试效果
    [self addSubview:starsContentView];
    
    for (int i = 0; i < self.numberOfStars; i++) {
        
        UIImageView *imageView = [[UIImageView alloc] initWithImage:starImage];
        imageView.frame = CGRectMake((self.starSize.width + self.spacingBetweenStars) * i, 0, self.starSize.width, self.starSize.height);
        imageView.contentMode = UIViewContentModeScaleAspectFit;
        [starsContentView addSubview:imageView];
    }
    
    [self.starsContentViews addObject:starsContentView];
}
#pragma mark - StarsContentView frame
- (CGRect)frameForStarsContentViewAtCurrentStarRate:(CGFloat)currentStarRate {
    
    NSInteger index = (NSInteger)floor(currentStarRate);
    CGFloat w = (self.starSize.width + self.spacingBetweenStars) * index + (currentStarRate - index) * self.starSize.width;
    CGFloat x = (CGRectGetWidth(self.bounds) - [self sizeForNumberOfStar:self.numberOfStars].width) * 0.5;
    CGFloat y = (CGRectGetHeight(self.bounds) - [self sizeForNumberOfStar:self.numberOfStars].height) * 0.5;
    CGFloat h = self.starSize.height;
    return CGRectMake(x, y, w, h);
}

#pragma mark - setter
- (void)setNumberOfStars:(NSInteger)numberOfStars {
    
    if (_numberOfStars != numberOfStars) {
        _numberOfStars = numberOfStars;
        [self resetStarsContentView];
    }
}

- (void)setSpacingBetweenStars:(CGFloat)spacingBetweenStars {
    
    if (_spacingBetweenStars != spacingBetweenStars) {
        _spacingBetweenStars = spacingBetweenStars;
        [self resetStarsContentView];
    }
}

- (void)setStarImage:(UIImage *)starImage {
    
    if (_starImage != starImage) {
        _starImage = starImage;
        [self resetStarsContentView];
    }
}
- (void)setCurrentStarImage:(UIImage *)currentStarImage {
    
    if (_currentStarImage != currentStarImage) {
        _currentStarImage = currentStarImage;
        [self resetStarsContentView];
    }
}

- (void)setStarSize:(CGSize)starSize {
    
    if (!CGSizeEqualToSize(_starSize, starSize)) {
        _starSize = starSize;
        [self resetStarsContentView];
    }
}

- (CGSize)starSize {

    if (CGSizeEqualToSize(_starSize, CGSizeZero)) {

        _starSize = self.starImage.size;
    }
    return _starSize;
}

- (void)setCurrentStarRate:(CGFloat)currentStarRate {
    
    if (self.starsContentViews.count == 0 || _currentStarRate == currentStarRate) {
        return;
    }
    if (currentStarRate  < 0) {
        return;
    } else if (currentStarRate > self.numberOfStars) {
        
        _currentStarRate = self.numberOfStars;
    } else {
        _currentStarRate = currentStarRate;
    }

    UIView *starsContentView = self.starsContentViews[1];
    [UIView animateWithDuration:_isAnimation ? kAnimatinDuration : 0.0 animations:^{
        starsContentView.frame = [self frameForStarsContentViewAtCurrentStarRate:currentStarRate];
    }];
    if (self.didSelectStarBlock) {
        self.didSelectStarBlock(self, currentStarRate);
    }
    if ([self.delegate respondsToSelector:@selector(starRateView:didSelecteStarAtStarRate:)]) {
        [self.delegate starRateView:self didSelecteStarAtStarRate:currentStarRate];
    }
}

#pragma mark - event
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {

    if (_allowClickScore) {
        UITouch *touch = [touches anyObject];
        UIView *view = touch.view;
        if (view != self) {
            CGPoint point = [touch locationInView:view];
            [self setupScoreWithOffsetX:point.x];
        }
    }
}

- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    if (_allowSlideScore) {
        UITouch *touch = [touches anyObject];
        UIView *view = touch.view;
        if (view != self && [self.starsContentViews containsObject:view]) {
            CGPoint point = [touch locationInView:view];
            [self setupScoreWithOffsetX:point.x];
        }
    }
}

#pragma mark - 根据offsetx计算分数
- (void)setupScoreWithOffsetX:(CGFloat)offsetX {
    
    NSInteger index = offsetX / (self.starSize.width + self.spacingBetweenStars);
    CGFloat mathOffsetX =  (index + 1) * self.starSize.width + index * self.spacingBetweenStars;
    CGFloat score = (offsetX - index * self.spacingBetweenStars)/(self.starSize.width);
    if (offsetX > mathOffsetX) {
        score = index + 1;
    }
    self.currentStarRate = [self currentStarRateWithScore:score];
    NSLog(@"offsetX=%f,index=%ld, score=%f, starRate=%f", offsetX, index, score, self.currentStarRate);

}

- (CGFloat)currentStarRateWithScore:(CGFloat)score {
    
    switch (self.style) {
        case GBStarRateViewStyleWholeStar: //全星
            score = ceil(score);
            break;
        case GBStarRateViewStyleHalfStar: //半星
            score = round(score) > score ? round(score) : (score < (ceil(score)-0.5) ? (ceil(score)-0.5) : ceil(score));
            break;
        case GBStarRateViewStyleIncompleteStar: //不完整星
            score = score;
            break;
    }
    return score;
}


- (CGSize)sizeForNumberOfStar:(NSInteger)starCount {
    
    CGFloat w = (self.starSize.width + self.spacingBetweenStars)*starCount - self.spacingBetweenStars;
    return CGSizeMake(w, self.starSize.height);
}


- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    
    //实现在星星范围内也能响应 即使父视图高度少于星星高度
    if (point.y <= (self.starImage.size.height*0.5) && point.y >= - (self.starImage.size.height*0.5 - self.bounds.size.height*0.5)) {
        return YES;
    }
    return [super pointInside:point withEvent:event];
}


#pragma mark - getter
- (NSMutableArray<UIView *> *)starsContentViews {
    
    if (!_starsContentViews) {
        _starsContentViews = [NSMutableArray array];
    }
    return _starsContentViews;
}

欢迎大家下载使用传送门, 比心~~~

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