如何在 iOS 应用上用自定义视图实现星级评分功能呢?最近研究学习了两个实现方法,现总结如下:
一、实现方式一
参考 Ray Wenderlich 系列教程:
UIView Tutorial for iOS: How To Make a Custom UIView in iOS 5: A 5 Star Rating View
1.Demo 示例
2.实现的功能:
- 可任意设置星星数量;
- 可以设置星星是否可编辑、星星的左边距、星星之间的间隔等;
- 整星评价和半星评价;
- 不仅仅是点击触摸屏幕评分,还可以实现手指按住屏幕滑动评分;
- 用 Delegate 方式实现回调;
3.视图层级
视图的层级比较简单,一个自定义 UIView 子类对象,上面添加了 5 个星星图片对象,即 UIImageView 对象。
评分值修改时,我们需要遍历这5个 UIImageView 对象,并对他们的图片进行相应的更改。
4.实现原理:根据 rating 值遍历设置每个星星图案
讲解一下大概的实现思路,具体内容可以参考源码:GitHub
- 首先需要三张星星图片:空星图片、半星图片、全星图片:
-
初始化设置最大评分数
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; } }
-
以上两个步骤中,我们设置了最大评分数、添加并布局好了所有的 imageView。接下来,需要接收用户事件,根据用户手指触摸的位置计算评分值
rating
:首先,要接受用户的触摸事件:
因为 UIView 是 UIResponder 的子类,所以覆盖以下四个方法就可以处理四种不同的触摸事件:
// ① 一根手指或多根手指触摸屏幕 - (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
宽度的中点作比较:- 如果触摸位置的X值 > 当前图片宽度的中点,那
rating
值就是整数,是全星评价。 - 如果 触摸位置的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) }
- 如果触摸位置的X值 > 当前图片宽度的中点,那
-
上面一个步骤,我们得到了评分值。最后,每次
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; } } }
整个实现思路大致如上。
实现方式二
参考 @XHJCoder 的源码:
1.Demo示例
2.实现的功能
- 可任意设置星星数量;
- 支持动画修改评论;
- 评分样式支持:整星评论、半星评论、不完整星星评论;
- 支持 block 和 delegate 两种方式返回修改结果;
3.实现原理,根据 rating 值整体修改视图的 frame 宽度
我对源码 fork 并进行了一些修改:XHStarRateView
下面是视图的层级结构:
- backgroundStarView 、foregroundStarView 是 UIView 的实例对象。
- backgroundStarView 视图上添加5个灰色的 imageView。
- foregroundStarView 视图上添加5个黄色的 image View。
-
初始化时添加所有视图:
// 指定初始化方法 - (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; }
-
使用 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; } } }
-
每次更新 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); }]; }
OK,第二种方法就是实现如上。
-
第二种方法封装得相对第一种方法更好一些,调用也很方便:
/* 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];