1. 程式人生 > >iOS 滾動數字控制元件:DPScrollNumberLabel 實現

iOS 滾動數字控制元件:DPScrollNumberLabel 實現

寫在前面

第一次寫部落格,有點小激動,同時也害怕寫的很爛,所以希望大家能夠包容,如果大家覺得看不下去我的部落格,可以直接翻到最後有原始碼和demo的github地址。開發ios也有大半年了,所以想要嘗試一下寫點部落格,好了廢話不多說了下面開始正題了。

正文

簡介

由於公司前段時間專案裡要用到一個可以滾動的數字標籤,所以就寫了這樣一個控制元件,現在有時間了,就寫篇部落格記錄一下實現這個控制元件的過程。

上一張gif動畫
效果圖

這個控制元件的邏輯是當個位數從0~9~0時十位數向上滾動1,當十位完成一個0~9~0迴圈時,百位向上滾動1依次類推

實現思路

先說一下我實現這個控制元件的思路,其實比較簡單,數字的每一列都是一個很長的UILabel,然後由上至下是數字0~9,當數字改變時,讓這個label上下移動,產生滾動的動畫,只需要計算迴圈的次數,滾動的方向,滾動的時間就可以了。

Talk is cheap,show you The code!

首先看一下標頭檔案裡的內容:

//
//  DPScrollNumberLabel.h
//  DPScrollNumberLabelDemo
//
//  Created by Dai Pei on 16/5/23.
//  Copyright © 2016年 Dai Pei. All rights reserved.
//


#import <UIKit/UIKit.h>


@interface DPScrollNumberLabel : UIView

@property (nonatomic, strong)NSNumber
*displayedNumber; - (instancetype)initWithNumber:(NSNumber *)originNumber fontSize:(CGFloat)size; - (instancetype)initWithNumber:(NSNumber *)originNumber fontSize:(CGFloat)size textColor:(UIColor *)color; - (instancetype)initWithNumber:(NSNumber *)originNumber font:(UIFont *)font; - (instancetype)initWithNumber:(NSNumber
*)originNumber font:(UIFont *)font textColor:(UIColor *)color; - (instancetype)initWithNumber:(NSNumber *)originNumber fontSize:(CGFloat)size rowNumber:(NSUInteger)rowNumber; - (instancetype)initWithNumber:(NSNumber *)originNumber fontSize:(CGFloat)size textColor:(UIColor *)color rowNumber:(NSUInteger)rowNumber; - (instancetype)initWithNumber:(NSNumber *)originNumber font:(UIFont *)font textColor:(UIColor *)color rowNumber:(NSUInteger)rowNumber;//rowNumber should less than or equal 8 - (void)changeToNumber:(NSNumber *)number animated:(BOOL)animated; - (void)changeToNumber:(NSNumber *)number interval:(CGFloat)interval animated:(BOOL)animated; @end

可以看到標頭檔案裡有7個初始化方法,分為兩大類:規定列數和不規定列數。當規定列數時,則控制元件的列數固定,當傳入的數字大於列數限制時函式直接返回,當不規定列數時,以初始化傳入的數字的列數為初始列數,當後面傳入的數字大於初始列數時,會自動在左邊補加列(最大列數不能超過8列)
其中最重要的一個引數是字型的大小,我需要這個引數去計算此控制元件的大小。
另外兩個方法當需要顯示的數字改變時呼叫,animated傳入YES時會有動畫,傳入NO時直接改變不播放動畫。

下面只貼上其中一個init方法:

- (instancetype)initWithNumber:(NSNumber *)originNumber fontSize:(CGFloat)size textColor:(UIColor *)color rowNumber:(NSUInteger)rowNumber {
    self = [super init];
    if (self) {
        self.displayedNumber = originNumber;
        self.font = [UIFont systemFontOfSize:size];
        self.textColor = color;
        self.isAnimation = NO;
        self.finishedAnimationCount = 0;
        self.rowNumber = (rowNumber > 0 && rowNumber <= 8) ? rowNumber : 0;
        self.maxRowNumber = (self.rowNumber == 0) ? 8 : rowNumber;
        [self commonInit];
    }
    return self;
}

初始化後進入commonInit方法:

- (void)commonInit {
    [self initCell];
    [self initParent];
}

然後先初始化cell,再初始化parent:

- (void)initCell {
    int originNumber = self.displayedNumber.intValue;
    //如果沒有規定列數 就自己計算 方法具體實現會在後面貼出
    if (self.rowNumber == 0) {
        self.rowNumber = [self calculateRowNumber:originNumber];
    }
    //用於儲存所有的cell
    self.cellArray = [[NSMutableArray alloc] init];
    NSString *text = @"0\n1\n2\n3\n4\n5\n6\n7\n8\n9\n0";
    //這個方法很贊 可以直接根據內容來計算一個view的大小
    CGRect rect = [text boundingRectWithSize:CGSizeZero
    options:NSStringDrawingUsesLineFragmentOrigin
attributes:@{NSFontAttributeName:self.font} context:nil];
    //然後儲存寬度和高度值,後面很多地方都會用到
    self.cellWidth = rect.size.width;
    self.cellHeight = rect.size.height;
    //拿到每個位數上顯示的數字並儲存在一個數組中
    NSArray *displayNumberArray = [self getCellDisplayNumberWithNumber:self.displayedNumber.integerValue];
    //初始化每個cell 並且儲存在陣列中
    for (NSInteger i = 0; i < self.rowNumber; i++) {
        UILabel *scrollCell = [self makeScrollCell];
        scrollCell.frame = CGRectMake((self.rowNumber - 1 - i) * self.cellWidth, 0, self.cellWidth, self.cellHeight);
        scrollCell.text = text;
        NSNumber *displayNum = [displayNumberArray objectAtIndex:i];
        //此方法調整cell的位置,使其顯示相應的數字 方法具體實現會在後面貼出
        [self setScrollCell:scrollCell toNumber:displayNum.integerValue];
        [self.cellArray addObject:scrollCell];
    }
}
#pragma mark - Getters
- (UILabel *)makeScrollCell {
    UILabel *cell = [[UILabel alloc] init];
    cell.font = self.font;
    cell.numberOfLines = 11;
    cell.textColor = self.textColor;
    return  cell;
}

然後是實現parent:

- (void)initParent{
    self.bounds = CGRectMake(0, 0, self.rowNumber * self.cellWidth, self.cellHeight / 11);
    self.backgroundColor = [UIColor clearColor];
    self.layer.masksToBounds = YES;
    //把cell佈局到parent中 
    [self layoutCell:self.rowNumber withAnimation:YES];
}
- (void)layoutCell:(NSUInteger)rowNumber withAnimation:(BOOL)animated{
    //先將子view全部移除 此處移除是有原因的
    for (UIView *subView in self.subviews) {
        [subView removeFromSuperview];
    }
    //再新增進去
    for (UILabel *cell in self.cellArray) {
        [self addSubview:cell];
    }
    //此處用動畫重新排列所有的cell
    __weak typeof(self) weakSelf = self;
    [UIView animateWithDuration:0.2 * (rowNumber - self.rowNumber) animations:^{
        for (int i = 0; i < rowNumber; i++) {
            UILabel *cell = [weakSelf.cellArray objectAtIndex:i];
            cell.frame = CGRectMake((rowNumber - 1 - i) * weakSelf.cellWidth,
                                    cell.frame.origin.y,
                                    weakSelf.cellWidth,
                                    weakSelf.cellHeight);
        }
        self.frame = CGRectMake(self.frame.origin.x,
                                self.frame.origin.y,
                                rowNumber * self.cellWidth,
                                self.cellHeight/11);
    } completion:nil];
}

至此,初始化的工作全部完成

下面讓我們關注標頭檔案裡另外的兩個公有方法:

- (void)changeToNumber:(NSNumber *)number animated:(BOOL)animated {
- 
    [self changeToNumber:number interval:0 animated:animated];
}

- (void)changeToNumber:(NSNumber *)number interval:(CGFloat)interval animated:(BOOL)animated {
    //因為沒有做負數部分,所以當傳入的數字小於0 直接返回
    if (number.integerValue < 0) {
        return ;
    }
    //當大於最大行數,直接返回
    if ([self calculateRowNumber:number.integerValue] > self.maxRowNumber) {
        return ;
    }
    //如果傳入數字和本身顯示相同,直接返回
    if (number.integerValue == self.displayedNumber.integerValue) {
        return ;
    }
    //當傳入數字時,當前動畫還沒有播放完成,加入陣列,等待動畫播放完成
    if (self.isAnimation) {
        if (!self.taskArray) {
            self.taskArray = [NSMutableArray array];
        }
        [self.taskArray addObject:@{keyTaskDisplayNumber:number, keyTaskChangeNumber:@(number.integerValue - self.displayedNumber.integerValue),keyTaskInterval:@(interval)}];
    }else {
        if (animated) {
        //animated 為YES時 進入此方法
            [self playAnimationWithChange:number.integerValue - self.displayedNumber.integerValue displayNumber:number interval:interval];
            self.isAnimation = YES;
        }else {
        //animated 為NO時 直接改變
        //這個方法在前面說過,具體實現會在後面貼出
            NSArray<NSNumber *> *displayNumbers = [self getCellDisplayNumberWithNumber:number.integerValue];
            for (int i = 0; i < displayNumbers.count; i++) {
                [self setScrollCell:self.cellArray[i] toNumber:displayNumbers[i].integerValue];
            }
        }
    }
    self.displayedNumber = number;
}

下面就要進入動畫部分了

static const CGFloat bufferModulus = 0.7f;

- (void)playAnimationWithChange:(NSInteger)changeNumber displayNumber:(NSNumber *)displayNumber interval:(CGFloat)interval{
    //改名後的列數
    NSInteger nextRowNumber = [self calculateRowNumber:displayNumber.intValue];
    //只有當列數增加時才重新佈局
    if (nextRowNumber > self.rowNumber) {
        [self reInitCell:nextRowNumber];
        [self layoutCell:nextRowNumber withAnimation:YES];
        self.rowNumber = nextRowNumber;
    }
    //儲存每一位迴圈次數的陣列
    NSArray *repeatCountArray = [self getRepeatTimesWithChangeNumber:changeNumber displayNumber:displayNumber.integerValue];
    //儲存每一位將顯示的數值
    NSArray *willDisplayNums = [self getCellDisplayNumberWithNumber:displayNumber.integerValue];

    //如果沒有設定動畫間隔,則根據改變的大小來進行計算
    if (interval == 0) {
        interval = [self getIntervalWithOriginalNumber:displayNumber.integerValue - changeNumber displayNumber:displayNumber.integerValue];
    }
    //獲得滾動的方向
    ScrollAnimationDirection direction = (changeNumber > 0)? ScrollAnimationDirectionUp : ScrollAnimationDirectionDown;

    CGFloat delay = 0.0f;

    if (repeatCountArray.count != 0) {
        for (NSInteger i = 0; i < repeatCountArray.count; i++) {
            NSNumber *repeat = [repeatCountArray objectAtIndex:i];
            NSInteger repeatCount = repeat.integerValue;
            NSNumber *willDisplayNum = [willDisplayNums objectAtIndex:i];
            UILabel *cell = [self.cellArray objectAtIndex:i];
            CGFloat startDuration = 0;
            //當不是一個完整0~9~0迴圈時,只進行一個Single動畫(我將這裡的動畫分為兩類:single和multi)
            if (repeatCount == 0) {
                [self makeSingleAnimationWithCell:cell duration:interval delay:delay animationCount:repeatCountArray.count displayNumber:willDisplayNum.integerValue];
            }else {
            //當>=一個迴圈時,進行multi動畫
                if (direction == ScrollAnimationDirectionUp) {
                //此處計算三個部分的動畫時間
                    startDuration = interval * (10 - [self getDisplayNumberOfCell:cell]) / ceilf(fabs(changeNumber / pow(10, i)));
                    CGFloat cycleDuration = interval * 10 / fabs(changeNumber / pow(10, i));
                    if (repeatCount == 1) {
                        cycleDuration = 0;
                    }
                    CGFloat endDuration = bufferModulus * pow(willDisplayNum.integerValue, 0.3) / (i + 1);
                    NSDictionary *attribute = @{keyStartDuration:   @(startDuration),
                                                keyStartDelay:      @(delay),
                                                keyCycleDuration:   @(cycleDuration),
                                                keyEndDuration:     @(endDuration),
                                                keyRepeatCount:     @(repeatCount - 1),
                                                keyDisplayNumber:   willDisplayNum};
                    [self makeMultiAnimationWithCell:cell direction:direction animationCount:repeatCountArray.count attribute:attribute];
                }else if (direction == ScrollAnimationDirectionDown) {
                    startDuration = interval * ([self getDisplayNumberOfCell:cell] - 0) / ceilf(fabs(changeNumber / pow(10, i)));
                    CGFloat cycleDuration = interval * 10 / fabs(changeNumber / pow(10, i));
                    //此處可能有些疑問,因為這個repeatCount不是真正迴圈的次數,是cycle的次數加end部分的1,所以如果repeat為1 真正迴圈次數是0
                    if (repeatCount == 1) {
                        cycleDuration = 0;
                    }
                    CGFloat endDuration = bufferModulus * pow(10 - willDisplayNum.integerValue, 0.3) / (i + 1);
                    NSDictionary *attribute = @{keyStartDuration:   @(startDuration),
                                                keyStartDelay:      @(delay),
                                                keyCycleDuration:   @(cycleDuration),
                                                keyEndDuration:     @(endDuration),
                                                keyRepeatCount:     @(repeatCount - 1),
                                                keyDisplayNumber:   willDisplayNum};
                    [self makeMultiAnimationWithCell:cell direction:direction animationCount:repeatCountArray.count attribute:attribute];
                }
            }
            delay = delay + startDuration;
        }
    }
}

動畫分為single和multi兩種,multi動畫裡包括3個部分start,cycle,end;start部分是當前數值~9的動畫,cycle部分是0~9的數次迴圈,end部分為0~最終數值(此處以數字增加舉例,數字減少時相反)

下面貼出single和multi兩種動畫的程式碼:

- (void)makeSingleAnimationWithCell:(UILabel *)cell duration:(CGFloat)duration delay:(CGFloat)delay animationCount:(NSInteger)count displayNumber:(NSInteger)displayNumber{

    [UIView animateWithDuration:duration delay:delay options:UIViewAnimationOptionCurveEaseOut animations:^{
        [self setScrollCell:cell toNumber:displayNumber];
    } completion:^(BOOL finished) {
    //當動畫結束,檢查是否有待執行的動畫
        [self checkTaskArrayWithAnimationCount:count];
        NSLog(@"single animation finish!");
    }];
}
- (void)makeMultiAnimationWithCell:(UILabel *)cell
                         direction:(ScrollAnimationDirection)direction
                    animationCount:(NSInteger)count
                         attribute:(NSDictionary *)attribute{
    NSNumber *startDuration = [attribute objectForKey:keyStartDuration];
    NSNumber *cycleDuration = [attribute objectForKey:keyCycleDuration];
    NSNumber *endDuration = [attribute objectForKey:keyEndDuration];
    NSNumber *repeatCount = [attribute objectForKey:keyRepeatCount];
    NSNumber *willDisplayNum = [attribute objectForKey:keyDisplayNumber];
    NSNumber *startDelay = [attribute objectForKey:keyStartDelay];

    [UIView animateWithDuration:startDuration.floatValue delay:startDelay.floatValue options:UIViewAnimationOptionCurveEaseIn animations:^{
    //這是開始部分的動畫
        [self setScrollCell:cell toNumber:(direction == ScrollAnimationDirectionUp)?10 : 0];
    } completion:^(BOOL finished) {
        NSLog(@"start animation finish!");
        //開始動畫結束後將cell歸位到迴圈開始的地方
        [self setScrollCell:cell toNumber:(direction == ScrollAnimationDirectionUp)?0 : 10];

        if (cycleDuration.floatValue == 0) {
        //當迴圈次數為0,直接執行結束部分的動畫
            [UIView animateWithDuration:endDuration.floatValue delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{
                [self setScrollCell:cell toNumber:willDisplayNum.integerValue];
            } completion:^(BOOL finished) {
                [self checkTaskArrayWithAnimationCount:count];
                NSLog(@"end animation finish!");
            }];
        }else {
        //否則進入迴圈動畫
            [UIView animateWithDuration:cycleDuration.floatValue delay:0 options:UIViewAnimationOptionCurveLinear | UIViewAnimationOptionRepeat animations:^{
                [UIView setAnimationRepeatCount:repeatCount.integerValue];
                switch (direction) {
                    case ScrollAnimationDirectionUp:
                        [self setScrollCell:cell toNumber:10];
                        break;
                    case ScrollAnimationDirectionDown:
                        [self setScrollCell:cell toNumber:0];
                        break;
                    default:
                        break;
                }
            } completion:^(BOOL finished) {
                NSLog(@"cycle animation finish!");
                [self setScrollCell:cell toNumber:(direction == ScrollAnimationDirectionUp)?0 : 10];
                //這是迴圈後的結束動畫
                [UIView animateWithDuration:endDuration.floatValue delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{
                    [self setScrollCell:cell toNumber:willDisplayNum.integerValue];
                    } completion:^(BOOL finished) {
                        [self checkTaskArrayWithAnimationCount:count];
                        NSLog(@"end animation finish!");
                    }];
            }];
        }
    }];
}

核心動畫部分程式碼都已經貼出來了,下面看下一些privite方法

首先是每個動畫結束的checkTaskArrayWithAnimationCount:方法

//這裡傳入的cout是一次變化總共的動畫次數
- (void)checkTaskArrayWithAnimationCount:(NSInteger)count {
    //每個動畫結束,計數加一,當等於count時,說明所有動畫都已經結束了
    self.finishedAnimationCount++;
    if (self.finishedAnimationCount == count) {
        self.finishedAnimationCount = 0;
        if (self.taskArray.count != 0) {
            NSDictionary *task = [self.taskArray objectAtIndex:0];
            [self.taskArray removeObject:task];
            NSNumber *displayNumber = [task objectForKey:keyTaskDisplayNumber];
            NSNumber *changeNumber = [task objectForKey:keyTaskChangeNumber];
            NSNumber *interval = [task objectForKey:keyTaskInterval];
            //如果taskArray裡有task,則進行下一次動畫
            [self playAnimationWithChange:changeNumber.integerValue displayNumber:displayNumber interval:interval.floatValue];
        }else {
            self.isAnimation = NO;
        }
    }
}

計算列數的方法:calculateRowNumber:

這個方法很簡單,就不做解釋了

- (NSInteger)calculateRowNumber:(NSInteger)number {
    NSInteger rowNumber = 1;
    while ((number = number / 10) != 0) {
        rowNumber++;
    }
    return rowNumber;
}

使指定cell顯示相應數值的方法:setScrollCell: toNumber:

- (void)setScrollCell:(UILabel *)cell toNumber:(NSInteger)number {
    CGFloat originX = cell.frame.origin.x;
    CGFloat floatNumber = number;
    CGFloat y = - ((CGFloat)floatNumber / 11) * self.cellHeight;
    cell.frame = CGRectMake(originX, y, self.cellWidth, self.cellHeight);
}

計算repeatCount的方法:getRepeatTimesWithChangeNumber: displayNumber:

這個方法我想用幾個小例子來解釋,等看完這個例子,再去看程式碼就不會覺得那麼難懂了:比如當前展示數為521,要讓它改變到530,注意個位數要迴圈多少次呢,其實它只有開始部分和結束部分,並沒有迴圈部分,我程式碼的操作是將個位置0,520和530,530-520=10,10/10^1=1所以得到的迴圈次數是1,而我前面說過這個1不是真正的迴圈次數,而是迴圈次數加上end部分的1,所以當repeatCount=1時,只會有開始動畫和結束動畫。再舉一個例子:521和542,先置0,520和540,520-540=20,20/10^1=2,repeatCount=2,而真正迴圈次數為1,我們看,當521經過開始動畫變位530,隨後經過一個迴圈變為540,再經過結束動畫到達542,那麼真正迴圈確實是1次,說明這種演算法沒有問題。當然這隻舉了個位為例,其它位以此類推都是相同的。

- (NSArray<NSNumber *> *)getRepeatTimesWithChangeNumber:(NSInteger)change displayNumber:(NSInteger)number{
    NSMutableArray *repeatTimesArray = [[NSMutableArray alloc] init];
    NSInteger originNumber = number - change;
    if (change > 0) {
        do {
            number = (number / 10) * 10;
            originNumber = (originNumber / 10) * 10;
            NSNumber *repeat = @((number - originNumber) / 10);
            [repeatTimesArray addObject:repeat];
            number = number / 10;
            originNumber = originNumber / 10;
        } while ((number - originNumber) != 0);
    }else {
        do {
            number = (number / 10) * 10;
            originNumber = (originNumber / 10) * 10;
            NSNumber *repeat = @((originNumber - number) / 10);
            [repeatTimesArray addObject:repeat];
            number = number / 10;
            originNumber = originNumber / 10;
        } while ((originNumber - number) != 0);
    }
    return repeatTimesArray;
}

將一個數字準換為各位上應當展示的數值的方法:getCellDisplayNumberWithNumber:

舉個例子就是這個數字是:521,則返回一個數組:[@1,@2,@5]

- (NSArray<NSNumber *> *)getCellDisplayNumberWithNumber:(NSInteger)displayNumber {
    NSMutableArray *displayCellNumbers = [[NSMutableArray alloc] init];
    NSInteger tmpNumber;
    for (NSInteger i = 0; i < self.rowNumber; i++) {
        tmpNumber = displayNumber % 10;
        NSNumber *number = @(tmpNumber);
        [displayCellNumbers addObject:number];
        displayNumber = displayNumber / 10;
    }
    return displayCellNumbers;
}

獲得指定cell展示的數值的方法:getDisplayNumberOfCell:

- (NSInteger)getDisplayNumberOfCell:(UILabel *)cell {
    CGFloat y = cell.frame.origin.y;
    CGFloat tmpNumber = (- (y * 11 / self.cellHeight));
    NSInteger displayNumber = (NSInteger)roundf(tmpNumber);
    return displayNumber;
}

計算動畫總時間的方法:getIntervalWithOriginalNumber: displayNumber:

這個時間是根據最高位改變的位數來計算的,位數越高,每改變一次時間就越長。

static const CGFloat normalModulus = 0.3f;

- (CGFloat)getIntervalWithOriginalNumber:(NSInteger)number displayNumber:(NSInteger)displayNumber {

    NSArray *repeatTimesArray = [self getRepeatTimesWithChangeNumber:displayNumber - number displayNumber:displayNumber];
    NSUInteger count = repeatTimesArray.count;
    NSInteger tmp1 = displayNumber / (NSInteger)pow(10, count - 1);
    NSInteger tmp2 = number / (NSInteger)pow(10, count - 1);

    NSLog(@"tmp1:%ld tmp2:%ld", (long)tmp1, (long)tmp2);
    NSInteger maxChangeNum = labs(tmp1 % 10 - tmp2 % 10);

    return normalModulus * count * maxChangeNum;

}

程式碼部分到此結束

如果覺得我的程式碼對你有用處,請在github上給我一個贊,謝謝!