iOS中實現一個支持小數的星星評分組件

ilaa_8384 7年前發布 | 17K 次閱讀 iOS開發 移動開發

在很多電商,外賣,餐飲型應用里,都會在商品結束后評價中有一個星星組件。核心思路就是用UIControl并自定義實現其中的trackTouch的幾個方法。而顯示不到一個的星星,比如半個星星的思路是根據分數切割星星的圖像并顯示其中一部分。實現后效果如下。

單個星星的實現

對于單個星星的實現,先考慮星星有三個狀態,完全置灰狀態,完全高亮狀態,根據百分比半高亮狀態。而我這邊用的是UIButton來實現,因為UIButton本身已經有普通,高亮,選擇的狀態。主要實現的就是百分比高亮狀態。我們可以根據百分比將圖像進行裁剪,讓新圖像的寬度只有百分比所占的整個圖像的寬度。但是這時候調用setImage,會發現圖片處于整個button中間。這是因為UIButton的imageView的contentMode默認是AspectFit的,而AspectFit是默認居中的。Contentmode中的UIViewContentModeLeft,是不會把長邊縮放到imageView的邊長的。況且,直接改變UIButton里的ImageView的ContentMode是沒有效果的。要通過UIControl的contentVerticalAlignment屬性(垂直)和contentHorizontalAlignment屬性(水平)來達到類似ImageView的contentMode的效果。但是這兩個屬性都沒有帶Fit后綴的選項,也就是說并不會根據邊長等比縮放。所以需要先把圖片縮放到和Button大小一致。

屏幕快照 2017-05-21 下午6.32.58

之后,我們就可以按百分比裁剪圖片

+ (UIImage *)croppedImage:(UIImage *)image fraction:(CGFloat)fractonPart{
    CGFloat width = image.size.width * fractonPart * image.scale;
    CGRect newFrame = CGRectMake(0, 0, width , image.size.height * image.scale);
    CGImageRef resultImage = CGImageCreateWithImageInRect(image.CGImage, newFrame);
    UIImage *result = [UIImage imageWithCGImage:resultImage scale:image.scale orientation:image.imageOrientation];
    CGImageRelease(resultImage);
    return result;
}

并在BackgroundImage設置為灰色的星星圖像,設置為Button的highlighted狀態

- (void)setFractionPart:(CGFloat)fractionPart{
    if (fractionPart == 0) {
        return;
    }
    UIImage *image = [CDZStarButton croppedImage:self.highlightedImage fraction:fractionPart];
    self.imageView.contentMode = UIViewContentModeScaleAspectFit;
    self.contentHorizontalAlignment = UIControlContentHorizontalAlignmentLeft;
    self.contentVerticalAlignment = UIControlContentVerticalAlignmentFill;
    [self setImage:image forState:UIControlStateHighlighted];
    [self setBackgroundImage:self.normalImage forState:UIControlStateHighlighted];
    self.selected = NO;
    self.highlighted = YES;
}

而全高亮的狀態可以設置為UIButtonde點擊態

- (void)setHighlightedImage:(UIImage *)highlightedImage{
    _highlightedImage = [CDZStarButton reSizeImage:highlightedImage toSize:self.frame.size];
    [self setImage:_highlightedImage forState:UIControlStateSelected];
}

而點擊事件交由給上層的UIControl去處理,因為點擊的時候,除了點擊的星星本身,之前的星星也應該處于完全高亮狀態。在上層初始化按鈕的時候把其userInteractionEnabled屬性設置為NO即可,這樣觸摸事件就會忽略傳遞到Button去做事件處理,從而從響應者鏈傳遞上去交由父視圖的UIControl處理。

如果點擊到星星的一半,我們應該把點擊點轉換成小數部分給上層。C語言的round函數可以四舍五入,這里的10代表保留一位小數,可以根據實際情況保留位數,一般評分很少需要更高精度的。

- (CGFloat)fractionPartOfPoint:(CGPoint)point{
    CGFloat fractionPart =  (point.x - self.frame.origin.x) / self.frame.size.width;
    return round(fractionPart * 10) / 10;
}

到這里一個可以顯示分數的星星按鈕就完成了,接下來就是在上層的UIControl去處理觸摸事件的響應。

UIControl部分的實現

主要分兩塊,星星按鈕的布局,觸摸事件響應。

布局

首先根據星星的數量一個個添加上視圖,用UIView的tag來表示對應第幾個星星按鈕。

- (void)setupView {
    for (NSInteger index = 0; index < self.numberOfStars; index++) {
        CDZStarButton *starButton = [CDZStarButton.alloc initWithSize:self.starSize];
        starButton.tag = index;
        starButton.normalImage = self.normalStarImage;
        starButton.highlightedImage = self.highlightedStarImage;
        starButton.userInteractionEnabled = NO;//關閉事件響應,交由UIControl本身處理
        [self addSubview:starButton];
    }
}

然后是計算每個星星的位置,計算間隔和上下邊距并layout

- (void)layoutSubviews {
    [super layoutSubviews];
    for (NSInteger index = 0; index < self.numberOfStars; index ++) {
        CDZStarButton *starButton =  [self starForTag:index];
        CGFloat newY = (self.frame.size.height - self.starSize.height) / 2;
        CGFloat margin = 0;
        if (self.numberOfStars > 1) {
            margin = (self.frame.size.width - self.starSize.width * self.numberOfStars) / (self.numberOfStars - 1);
        }
        starButton.frame = CGRectMake((self.starSize.width + margin) * index, newY, self.starSize.width, self.starSize.height);
    }
}

這時,我們可以加上兩個方法去找到對應的星星button,根據tag和根據點擊的CGPoint

- (CDZStarButton *)starForPoint:(CGPoint)point {
    for (NSInteger i = 0; i < self.numberOfStars; i++) {
        CDZStarButton *starButton = [self starForTag:i];
        if (CGRectContainsPoint(starButton.frame, point)) {
            return starButton;
        }
    }
    return nil;
}

- (CDZStarButton *)starForTag:(NSInteger)tag {
    __block UIView *target;
    [self.subviews enumerateObjectsUsingBlock:^(__kindof UIView * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        if (obj.tag == tag) {
            target = obj;
            *stop = YES;
        }
    }];
    return (CDZStarButton *)target;
}

處理觸摸事件

我們先加上兩個方法。一個是從第一個到某個星星開始從左到右依次點亮,一個是從最后一個星星到某個星星從右到左依次熄滅。

- (void)starsDownToIndex:(NSInteger)index {
    for (NSInteger i = self.numberOfStars; i > index; --i) {
        CDZStarButton *starButton = [self starForTag:i];
        starButton.selected = NO;
        starButton.highlighted = NO;
    }
}

- (void)starsUpToIndex:(NSInteger)index {
    for (NSInteger i = 0; i <= index; i++) {
        CDZStarButton *starButton = [self starForTag:i];
        starButton.selected = YES;
        starButton.highlighted = NO;
    }
}

然后設置一個評分的屬性,重寫其setter方法。allowFraction,用來判斷組件是否需要分數表示,floor函數是取最大整數,相當于直接去除小數點后面的數字。判斷評分的整數部分是否已經亮著,亮著那么說明從左到右最后一個亮著的右邊,反之在左邊,分別調用從右到左依次熄滅,或從左到右依次點亮的方法,最后再設置分數部分。

- (void)setScore:(CGFloat)score{
    if (_score == score) {
        return;
    }
    _score = score;
    NSInteger index = floor(score);
    CGFloat fractionPart = score - index;;
    if (!self.isAllowFraction || fractionPart == 0) {
        index -= 1;
    }
    CDZStarButton *starButton = [self starForTag:index];
    if (starButton.selected) {
        [self starsDownToIndex:index];
    }
    else{
        [self starsUpToIndex:index];
    }
    starButton.fractionPart = fractionPart;
}

主要用到UIControl的四個方法

屏幕快照 2017-05-21 下午6.31.56

第一個是開始點擊的時候的事件處理,第二個是手指未抬起在屏幕上繼續移動的事件處理,第三個是離開屏幕,第四個是因為別的特殊情況事件被結束的處理。

點擊時候只要確定點擊的星星,確定其小數部分,然后調用評分屬性的setter方法就好了。

- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event {
    CGPoint point = [touch locationInView:self];
    CDZStarButton *pressedStar = [self starForPoint:point];
    if (pressedStar) {
        self.currentStar = pressedStar;
        NSInteger index = pressedStar.tag;
        CGFloat fractionPart = 1;
        if (self.isAllowFraction) {
            fractionPart = [pressedStar fractionPartOfPoint:point];
        }
        self.score = index + fractionPart;
    }
    return YES;
}

移動處理除了和點擊一樣的判斷邏輯,還要注意手指移開了星星之外的地方,分為所在星星的左邊(當前星星熄滅),右邊(當前星星完全點亮)兩種。

- (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event {
    CGPoint point = [touch locationInView:self];
    CDZStarButton *pressedStar = [self starForPoint:point];
    if (pressedStar) {
        self.currentStar = pressedStar;
        NSInteger index = pressedStar.tag;
        CGFloat fractionPart = 1;
        if (self.isAllowFraction) {
            fractionPart = [pressedStar fractionPartOfPoint:point];
        }
        self.score = index + fractionPart;
    }
    else{
          //移到了當前星星的左邊
        if (point.x < self.currentStar.frame.origin.x) {
            self.score = self.currentStar.tag;
        }
          //移到了當前星星的右邊
        else if (point.x > (self.currentStar.frame.origin.x + self.currentStar.frame.size.width)){
            self.score = self.currentStar.tag + 1;
        }
    }
    return YES;
}

而完成觸摸操作時,我們就可以用一個回調將當前的評分傳給外界。

@protocol CDZStarsControlDelegate






@optional

/**
 回調星星改變后的分數

 @param starsControl 星星組件
 @param score 分數
 */
- (void)starsControl:(CDZStarsControl *)starsControl didChangeScore:(CGFloat)score;
@end


  
- (void)endTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event {
    [super endTrackingWithTouch:touch withEvent:event];
    if ([self.delegate respondsToSelector:@selector(starsControl:didChangeScore:)]) {
        [self.delegate starsControl:self didChangeScore:self.score];
    }
}


- (void)cancelTrackingWithEvent:(UIEvent *)event {
    [super cancelTrackingWithEvent:event];
    if ([self.delegate respondsToSelector:@selector(starsControl:didChangeScore:)]) {
        [self.delegate starsControl:self didChangeScore:self.score];
    }
}

封裝

- (instancetype)initWithFrame:(CGRect)frame
                        stars:(NSInteger)number
                     starSize:(CGSize)size
              noramlStarImage:(UIImage *)normalImage
         highlightedStarImage:(UIImage *)highlightedImage{
    if (self = [super initWithFrame:frame]) {
        _numberOfStars = number;
        _normalStarImage = normalImage;
        _highlightedStarImage = highlightedImage;
        _starSize = size;
        _allowFraction = NO;
        self.clipsToBounds = YES;
        self.backgroundColor = [UIColor clearColor];
        [self setupView];
    }
    return self;
}

開放score屬性和allowFraction屬性即可。使用起來也很簡單。

 CDZStarsControl *starsControl = [CDZStarsControl.alloc initWithFrame:CGRectMake(0, 100, self.view.frame.size.width, 50) stars:5 starSize:CGSizeMake(50, 50) noramlStarImage:[UIImage imageNamed:@"star_normal"] highlightedStarImage:[UIImage imageNamed:@"star_highlighted"]];
    starsControl.delegate = self;
    starsControl.allowFraction = YES;
    starsControl.score = 2.6f;
    [self.view addSubview:starsControl];

然后在delegate里處理分數改變后的操作即可。

最后

所有源碼和 Demo

可以直接拿文件去使用或修改,也支持Cocoapods.

  • 如果您覺得有幫助,不妨給個star鼓勵一下,歡迎關注&交流

  • 有任何問題歡迎評論私信或者提issue

  • QQ:757765420

  • Email:nemocdz@gmail.com

  • Github: Nemocdz

  • 微博:@Nemocdz

謝謝觀看

來自:http://www.jianshu.com/p/c5d0ed7f035c

 

 本文由用戶 ilaa_8384 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!