需求
项目中需要用到跑马灯来仅展示一条消息,长度合适则不滚动,过长则循环滚动。
虽然不是我写的,但看了看代码,是在一个UIView里面放入两个UILabel,
在前一个快结束的时候,另一个显示。然而点击处理的 确是UIView的点击事件。
然而看到比如地铁、公交里面的跑马灯是分了很多段显示的。虽然说可以将多段合并为一段来显示,
但是如果各个需要点击事件又该如何处理呢?于是我来自己实现可点击的多段跑马灯。
所以这篇随笔我要实现的跑马灯包含下面这种效果:(图中有5段 点击不同文本可触发相应的事件)
弯路
还记得上一篇随笔【IOS】将字体大小不同的文字底部对齐 么?
虽然不能够做到多个UILabel的底部对齐,但是我们可以通过继承UILabel来改变文本竖直方向的位置。
所以呢,我最初的想法是继承UILabel,可以保持其继承性, 通过NSTimer来直接慢慢移动UILable里面的文本。
这里出现了两个问题:(以@"这是自定义跑马灯里面要移动的文本"为例)
1.移动是可以移动,但是在文本左移至快要看不见(只剩下"移动的文本")的时候, 如何让@"这是.."开始从右侧出现呢?
2.文本过长的时候,看不见的部分将被截断,所以在移动的时候,只有部分文本了。
第一种好像没有办法,UILabel只存在一个文本的bounds, 不可能让他一部分在左边, 一部分在右边。
第二种就因为存在默认的属性NSLineBreakMode:NSLineBreakByWordWrapping,就算不截断文本也只会变为省略号。
所以这种方法作罢。。。。
实现
首先要明确的是本跑马灯继承了UIView且需要两个UILabel、定时器NSTimer。
在初始化时,传入字符串数组,并计算各个字符串的自适应大小
CGRect textRect = [((NSString *)_textArray[i]) boundingRectWithSize:maxSize options:NSStringDrawingUsesLineFragmentOrigin attributes:@{NSFontAttributeName:kFont} context:nil];
[_textRectArray addObject:[NSValue valueWithCGRect:textRect]];
如果传入的字符串数组个数为1且自适应宽度<UIView宽度,则不会滚动。重新写一个UILabel用于显示就行了
其他情况下,就是可以滚动的, 在此实例化两个UILabel,并打开定时器了:
定时器相关:
_timer = [NSTimer scheduledTimerWithTimeInterval:0.02 target:self selector:@selector(timerAction:) userInfo:nil repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes];
//为什么要将定时器起放入LOOP中呢?
//如果此RunLoop正在执行一个连续性的运算,timer就会被延时出发。
//也就是说,如果你将跑马灯放入scrollview上,当滑动scrollview的时候,定时器就不会动了 //相关方法:
//[_timer setFireDate:[NSDate date]]; 开始
//[_timer setFireDate:[NSDate distantFuture]]; 暂停
//取消定时器
//[_timer invalidate];
//_timer = nil; //防止野指针
//定时器执行事件:
-(void)timerAction:(NSTimer *)timer{}
现在我们要做的 就是在每次进入该方法的时候来设置两个UILabel。
好了,现在假设传入了4个字符串@[@"这个是第0个字符串",@"这个是第1个字符串",@"这个是第2个字符串",@"这个是第3个字符串"];
只有两个Label, 不管向右滚动还是向左滚动,我们将最初显示的定为Labels[0], 后来显示的定位Labels[1]
定义一个变量,实时的存放前一个UILabel的origin.X值,从0开始
1.每次前一个UILabel暂未完全隐藏前,后一个UIlabel就已经出现 (两者间有一个固定的距离 internalWidth)
还需要根据speed的值更改Labels[0]的大小的增减来控制Labels[0]的位置(更改offsetX值)。
通过这个距离和前一个的位置则可以实时的计算后一个UILabel的位置(origin.X值)。
2.每次前一个UILabel完全隐藏时就需要重新设置一个值, 此时刻在每次前一个UILabel完全看不到后只进入一次
同时将左右两个UILabel变换一下位置。 往左滚动: A<--B,A消失后,A跑到B右边去了; 成为B<--A,B消失后,又要到A右边去。
所以只需要设置offsetX = _labels[1].frame.origin.x;//A消失后,将后面的B位置作为下一个将消失的label的位置,A变为后面一个,
//其位置根据B的位置实时计算出来。每次前一个消失后,如此循环的更换。
但是这样只更改了位置,文本以及大小却没有变换,见3.
3.对于只有一个文本来说,AB的内容都是一样的。但是对于传入的四个字符串而言,每次重新设置值的时候,需要更改AB内容。
同时,对于长度不等的字符串,需要根据不同的文本大小来设置相应AB的Frame。
所以需将四个字符串文本大小,文本内容在之前保存为一个数组。定义一个始终记录当前正准备消失的(前一个)UIlabel的位置:_currentIndex
在步骤1中: 从两个数组中分别获取用于显示在A.B里的文本数组:labelTextArray frame数组:labelArray(从中取得宽和高)
每次AB位置交换的时候,需将currentIndex+1 : 即_currentIndex = (_currentIndex + 1) % _textArray.count;以供交换后使用。
之后分别取得当前以及下一个的Text和frame 分别保存到长度为2的数组 以便使用
上面太多、太乱。。。。。。我不想看我不想看我不想看。。。。。。
这里有图: 看完上面还完全不懂的请看这个吧。
再次解释
<===============左移==================
================右移=================>
现在只看颜色 从图中看可以到 无论左滚右滚 绿色始终是Labels[0](将要消失的Label) 红色始终是Label[1]
正常滚动情况下:
绿色的offsetX值随着speed而变 : self.offsetX = self.offsetX - sign * self.speed;
红色的X值会随着绿色的offsetX和固定间距的关系而变 : CGFloat nextOffX = self.offsetX + sign * (((self.orientation == RollingOrientationLeft)? firstRect.size.width : lastRect.size.width) + self.internalWidth);
通过_currentIndex值从保存到的数据中获取到红色、绿色的内容和大小后赋值:
当绿色消失的一瞬间:
本该是在右边的红色一下子吓绿了 : self.offsetX = _labels[1].frame.origin.x;
消失的绿色又将会按照正常滚动的情况下变为红色
_currentIndex指得始终是绿色内容的索引: _currentIndex = (_currentIndex + 1) % _textArray.count;
通过这个值又将会获取按照正常滚动的情况下 红色、绿色的大小和文本内容
(两个又将会进行的流程将会在"正常滚动情况下"的蓝色部分操作)
}
好了 解释到此结束 看着好累。。。。 慢着 还有点击事件没写完
点击事件:在给UILabel添加了Tap手势后进行处理
-(void)labelTap:(UITapGestureRecognizer *)gesture{
NSInteger tag = ((UILabel *)[gesture view]).tag - ;
NSInteger index;
if(tag == ){ //如果是(Labels[0])绿色
index = _currentIndex;
}else if (tag == ){ //如果是(Labels[1])红色 就是当前点击的后一个
index = (_currentIndex + ) % _textArray.count;
}else{
index = _currentIndex;
}
if(self.labelClickBlock){
self.labelClickBlock(index);
}
}
终点
代码见GitHub: ====> YFRollingLabel PS:源码以及GitHub文档都是用蹩脚的英语写的,也不知道会不会有人看。。
另外说明记录存在的问题:
对于放入的文本数组 长度不能太短 因为里面只有两个UILabel 如果长度太短的话 并且间距也小的情况下
在绿色刚消失后, 又会立马变为红色,出现在目前的绿色右边,而不是慢慢的移动出现。
文本太长太长的话(几百个中文,正常情况下不会设置这么多吧), 会导致获取的文本宽度过长,UILabel宽度过长,文本直接就不显示了,但点击事件还是有的 说明了还存在。。。这就搞不懂。。。
好了,终于写完了,Windows 10 Mobile 万岁!!!
PS:新的知识点:CADisplayLink
CADisplayLink
是一个能让我们以和屏幕刷新率相同的频率(每秒60次)将内容画到屏幕上的定时器。
但如果调用的方法比较耗时,超过了屏幕刷新周期,就会导致跳过若干次回调调用机会。
//创建
CADisplayLink displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(doSomeThing:)];
[displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; //设置是否停止
displayLink.Pause = NO/YES; //释放
[displayLink invalidate];
displayLink = nil;