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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
可以看到头文件里有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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
初始化后进入commonInit方法:
– (void)commonInit {
[self initCell];
[self initParent];
}
1
2
3
4
然后先初始化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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
然后是实现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];
}
1
2
3
4
5
6
7
– (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];
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
至此,初始化的工作全部完成
下面让我们关注头文件里另外的两个公有方法:
– (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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
下面就要进入动画部分了
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;
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
动画分为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!”);
}];
}];
}
}];
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
核心动画部分代码都已经贴出来了,下面看下一些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;
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
计算列数的方法:calculateRowNumber:
这个方法很简单,就不做解释了
– (NSInteger)calculateRowNumber:(NSInteger)number {
NSInteger rowNumber = 1;
while ((number = number / 10) != 0) {
rowNumber++;
}
return rowNumber;
}
1
2
3
4
5
6
7
使指定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);
}
1
2
3
4
5
6
计算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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
将一个数字准换为各位上应当展示的数值的方法: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;
}
1
2
3
4
5
6
7
8
9
10
11
获得指定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;
}
1
2
3
4
5
6
计算动画总时间的方法: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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
代码部分到此结束
————————————————
版权声明:本文为CSDN博主「太肥小次郎」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/dp948080952/article/details/51871485