需求
項目中需要用到跑馬燈來僅展示一條消息,長度合適則不滾動,過長則循環滾動。
雖然不是我寫的,但看了看代碼,是在一個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;