IOS自定义可点击的多文本跑马灯YFRollingLabel

Posted 汪小饭

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了IOS自定义可点击的多文本跑马灯YFRollingLabel相关的知识,希望对你有一定的参考价值。

需求

项目中需要用到跑马灯来仅展示一条消息,长度合适则不滚动,过长则循环滚动。

虽然不是我写的,但看了看代码,是在一个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 - 100;
    NSInteger index;
    if(tag == 0){   //如果是(Labels[0])绿色
        index = _currentIndex;
    }else if (tag == 1){  //如果是(Labels[1])红色  就是当前点击的后一个
        index = (_currentIndex + 1) % _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;

 

以上是关于IOS自定义可点击的多文本跑马灯YFRollingLabel的主要内容,如果未能解决你的问题,请参考以下文章

iOS - 带有自定义单元格的可点击 UITableView

iOS-文本内容展开/收起实现方案

Android 自定义Button 圆角大小可设置+点击变暗效果

如何在我绘制文本的自定义视图中使链接、电话号码可点击(与 textview 中的行为相同)?

自定义键盘文本完全替换 UISearchBar 中的文本,而不是添加到其中 [iOS]

“Android Utils“ 实现TextView 区域自定义点击