UILabel顯示不全時循環(huán)滾動顯示,利用Runtime黑魔法全局修改

最近做項目的時候遇到一個需求何乎,由于App兼容的語言類型太多句惯,導致App內(nèi)很多標簽顯示的文字不全,這時候需要滾動去顯示支救。需求類似下面這個樣子抢野。


滾動效果展示

思考

1.整個App的label去做一個滾動顯示,首先要考慮到性能問題各墨,性能問題的話就不考慮使用UIView層去實現(xiàn)該效果指孤,因此,文字滾動層考慮用Layer去處理贬堵。
2.能夠影響到文本顯示和是否能完全顯示文本的屬性有text恃轩、frame、font黎做、textColor叉跛,因此我們需要對這幾項屬性進行處理。

在設(shè)置text的屬性的時候蒸殿,我們需要判斷text的長度是否大于label的長度筷厘, 如果大于label的長度鸣峭,我們需要將text文本處理到layer層,并且做滾動動畫酥艳。在這是framefont的時候我們需要重新做該判斷摊溶,因為這兩個屬性影響了label的長度和文本的大小,設(shè)置textColor的時候我們需要把layer上的文本顏色也相應修改成該顏色玖雁。

實現(xiàn)

創(chuàng)建一個UILabel的子類更扁,并且添加CATextLayer的屬性

import UIKit

class LCAutoScrollLabel: UILabel {
        
    private var textLayer : CATextLayer = CATextLayer()
    
    private var shouldScroll : Bool = false
  
}

重寫以上四個屬性的set方法,加上判斷是否需要滾動的邏輯

    override var text: String? {
        didSet {
            shouldScroll = false
            ///把String轉(zhuǎn)化成NSString赫冬,根據(jù)文本和Font得到文本的Size
            let textString : NSString = NSString(string: self.text ?? "")
            let size = textString.size(withAttributes: [NSAttributedString.Key.font : self.font!])
            let stringWidth = size.width
            let labelWidth = frame.size.width
            if labelWidth < stringWidth {
                shouldScroll = true
            }
            if shouldScroll
            {
                /// 如果判斷需要滾動浓镜,設(shè)置textLayer的屬性
                let textString : NSString = self.text as NSString? ?? ""
                let size = textString.size(withAttributes: [NSAttributedString.Key.font : self.font!])
                let stringWidth = size.width
                let stringHeight = size.height
                textLayer.frame = CGRect(x: 0, y: (frame.height - stringHeight)/2, width: stringWidth, height: stringHeight)
                textLayer.string = text
                textLayer.alignmentMode = .center
                textLayer.font = font
                textLayer.fontSize = font.pointSize
                textLayer.foregroundColor = self.textColor.cgColor
                /// 動畫
                let ani = CABasicAnimation(keyPath: "position.x")
                ani.toValue = -textLayer.frame.width
                ani.fromValue = textLayer.frame.width
                ani.duration = 4
                ani.fillMode = .backwards
                ani.repeatCount = 1000000000.0
                ani.isRemovedOnCompletion = false
                
                textLayer.add(ani, forKey: nil)
                layer.addSublayer(textLayer)
            }
            else
            {
                /// 如果不需要滾動,移除動畫和layer
                textLayer.removeAllAnimations()
                textLayer.removeFromSuperlayer()
            }
        }
    }

因為四個重寫的set方法里面邏輯基本一致劲厌,整理一下代碼

    override var text: String? {
        didSet {
            shouldScroll = shouldAutoScroll()
            setTextLayerScroll()
        }
    }
    
    override var font: UIFont! {
        didSet {
            shouldScroll = shouldAutoScroll()
            setTextLayerScroll()
        }
    }
    
    override var frame: CGRect {
        didSet {
            shouldScroll = shouldAutoScroll()
            setTextLayerScroll()
        }
    }

    override var textColor: UIColor! {
        didSet {
            textLayer.foregroundColor = textColor.cgColor
        }
    }
    
    func setTextLayerScroll() {
        if shouldScroll
        {
            setTextLayer()
            textLayer.add(getLayerAnimation(), forKey: nil)
            layer.addSublayer(textLayer)
        }
        else
        {
            textLayer.removeAllAnimations()
            textLayer.removeFromSuperlayer()
        }
    }
    
    func shouldAutoScroll() -> Bool {
        var shouldScroll = false
        let textString : NSString = NSString(string: self.text ?? "")
        let size = textString.size(withAttributes: [NSAttributedString.Key.font : self.font!])
        let stringWidth = size.width
        let labelWidth = frame.size.width
        if labelWidth < stringWidth {
            shouldScroll = true
        }
        return shouldScroll
    }
    
    func setTextLayer() {
        let textString : NSString = self.text as NSString? ?? ""
        let size = textString.size(withAttributes: [NSAttributedString.Key.font : self.font!])
        let stringWidth = size.width
        let stringHeight = size.height
        textLayer.frame = CGRect(x: 0, y: (frame.height - stringHeight)/2, width: stringWidth, height: stringHeight)
        textLayer.string = text
        textLayer.alignmentMode = .center
        textLayer.font = font
        textLayer.fontSize = font.pointSize
        textLayer.foregroundColor = self.textColor.cgColor
    }
    
    func getLayerAnimation() -> CABasicAnimation {
        let ani = CABasicAnimation(keyPath: "position.x")
        ani.toValue = -textLayer.frame.width
        ani.fromValue = textLayer.frame.width
        ani.duration = 4
        ani.fillMode = .backwards
        ani.repeatCount = 1000000000.0
        ani.isRemovedOnCompletion = false
        return ani
    }

測試結(jié)果得到如下結(jié)果膛薛,因為忘記處理label自己本身顯示的文本。

效果展示

我們可以出重寫drawText的方法补鼻,根據(jù)是否需要滾動的判斷去決定是否要顯示text

    override func drawText(in rect: CGRect) {
        if !shouldScroll
        {
            super.drawText(in: rect)
        }
    }

經(jīng)過簡單測試哄啄,無論是改變font、frame风范、text都能夠正常判斷并且達到想要的效果咨跌。

但是有一個新的問題來了,由于項目是老項目硼婿,代碼量已經(jīng)十分龐大锌半,如果要一個一個去把以前使用UILabel的地方全部替換成這個類,是一項非常耗時的工作寇漫,由于工期緊張刊殉,顯然是給不了我這么多時間的。這時候我們就要用到我們的黑魔法Runtime了

黑魔法Runtime修改系統(tǒng)方法州胳,快速達到目的

我們創(chuàng)建一個UILabel的分類记焊,并且根據(jù)我們現(xiàn)有代碼的邏輯,得到如下代碼

#import "UILabel+AutoScroll.h"
#import <objc/runtime.h>

static NSString * textLayer = @"textLayer";
static NSString * scrollAnimation = @"scrollAnimation";

@implementation UILabel (AutoScroll)

+ (void)load
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        /// 替換系統(tǒng)方法栓撞,我們需要重寫的方法我們都要替換
        Method setTextMethod = class_getInstanceMethod(self, @selector(setText:));
        Method setColorMethod = class_getInstanceMethod(self, @selector(setTextColor:));
        Method setFontMethod = class_getInstanceMethod(self, @selector(setFont:));
        Method setFrameMethod = class_getInstanceMethod(self, @selector(setFrame:));
        Method drawTextMethon = class_getInstanceMethod(self, @selector(drawTextInRect:));
        
        Method scrollSetTextMethod = class_getInstanceMethod(self, @selector(autoScrollSetText:));
        Method scrollSetColorMethod = class_getInstanceMethod(self, @selector(autoScrollSetTextColor:));
        Method scrollSetFontMethod = class_getInstanceMethod(self, @selector(autoScrollSetFont:));
        Method scrollSetFrameMethod = class_getInstanceMethod(self, @selector(autoScrollSetFrame:));
        Method scrollDrawText = class_getInstanceMethod(self, @selector(autoScrollDrawText:));

        method_exchangeImplementations(setTextMethod, scrollSetTextMethod);
        method_exchangeImplementations(setColorMethod, scrollSetColorMethod);
        method_exchangeImplementations(setFontMethod, scrollSetFontMethod);
        method_exchangeImplementations(setFrameMethod, scrollSetFrameMethod);
        method_exchangeImplementations(drawTextMethon, scrollDrawText);
    });
    
}

/// 用于替換系統(tǒng)setText方法
/// @param text 標簽顯示的文字
- (void)autoScrollSetText:(NSString *)text
{
    [self autoScrollSetText:text];
    // 這句是為了讓textlayer超出label的部分不顯示
    self.layer.masksToBounds = true;
    [self setTextLayerScroll];
}

/// 用于替換系統(tǒng)setTextColor方法
/// @param color 文字顏色
- (void)autoScrollSetTextColor:(UIColor *)color
{
    [self autoScrollSetTextColor:color];
    [self setTextLayerScroll];
}

/// 用于替換系統(tǒng)的setFont方法
/// @param font 字體
- (void)autoScrollSetFont:(UIFont *)font
{
    [self autoScrollSetFont:font];
    [self setTextLayerScroll];
}

/// 用于替換系統(tǒng)的setFrame方法
/// @param frame 坐標
- (void)autoScrollSetFrame:(CGRect)frame
{
    [self autoScrollSetFrame:frame];
    [self setTextLayerScroll];
}

/// 用于替換系統(tǒng)的drawText方法
/// @param rect frame
- (void)autoScrollDrawText:(CGRect)rect
{
    BOOL shouldScroll = [self shouldAutoScroll];
    if (!shouldScroll)
    {
        [self autoScrollDrawText:rect];
    }
}

/// 根據(jù)文字長短自動判斷是否需要顯示TextLayer遍膜,并且滾動
- (void)setTextLayerScroll
{
    BOOL shouldScroll = [self shouldAutoScroll];
    CATextLayer * textLayer = [self getTextLayer];
    if (shouldScroll)
    {
        CABasicAnimation * ani = [self getAnimation];
        [textLayer addAnimation:ani forKey:nil];
        [self.layer addSublayer:textLayer];
    }
    else
    {
        [textLayer removeAllAnimations];
        [textLayer removeFromSuperlayer];
    }
}

/// runtime存放textLayer,避免多次生成
- (CATextLayer *)getTextLayer
{
    CATextLayer * layer = objc_getAssociatedObject(self, &textLayer);
    if (!layer) {
        layer = [CATextLayer layer];
        objc_setAssociatedObject(self, &textLayer, layer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    CGSize size = [self.text sizeWithAttributes:@{NSFontAttributeName:self.font}];
    CGFloat stringWidth = size.width;
    CGFloat stringHeight = size.height;
    layer.frame = CGRectMake(0, (self.frame.size.height - stringHeight)/2, stringWidth, stringHeight);
    layer.alignmentMode = kCAAlignmentCenter;
    layer.font = (__bridge CFTypeRef _Nullable)(self.font.fontName);
    layer.fontSize = self.font.pointSize;
    layer.foregroundColor = self.textColor.CGColor;
    layer.string = self.text;
    return layer;
}

/// runtime存放動畫對象瓤湘,避免多次生成
- (CABasicAnimation *)getAnimation
{
    CABasicAnimation * ani = objc_getAssociatedObject(self, &scrollAnimation);
    if (!ani) {
        ani = [CABasicAnimation animationWithKeyPath:@"position.x"];
        objc_setAssociatedObject(self, &scrollAnimation, ani, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    CATextLayer * textLayer = [self getTextLayer];
    id toValue = @(-textLayer.frame.size.width);
    id fromValue = @(textLayer.frame.size.width);
    ani.toValue = toValue;
    ani.fromValue = fromValue;
    ani.duration = 4;
    ani.fillMode = @"backwards";
    ani.repeatCount = 1000000000.0;
    ani.removedOnCompletion = false;
    return ani;
}

/// 判斷是否需要滾動
- (BOOL)shouldAutoScroll
{
    BOOL shouldScroll = false;
    CGSize size = [self.text sizeWithAttributes:@{NSFontAttributeName:self.font}];
    CGFloat stringWidth = size.width;
    CGFloat labelWidth = self.frame.size.width;
    if (labelWidth < stringWidth) {
        shouldScroll = true;
    }
    return shouldScroll;
}

@end

最后在pch文件中導入該分類瓢颅,項目中所有的UILabel就能自動判斷并且實現(xiàn)滾動顯示的效果。
大家可以試一試岭粤,如果有問題可以相互交流一下惜索。

手擼代碼不易,如果對您有幫助的剃浇,可以幫忙點個贊巾兆!謝謝

代碼地址:https://github.com/aYq524/AutoScrollLabel

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末猎物,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子角塑,更是在濱河造成了極大的恐慌蔫磨,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,542評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件圃伶,死亡現(xiàn)場離奇詭異堤如,居然都是意外死亡,警方通過查閱死者的電腦和手機窒朋,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評論 3 394
  • 文/潘曉璐 我一進店門搀罢,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人侥猩,你說我怎么就攤上這事榔至。” “怎么了欺劳?”我有些...
    開封第一講書人閱讀 163,912評論 0 354
  • 文/不壞的土叔 我叫張陵唧取,是天一觀的道長。 經(jīng)常有香客問我划提,道長枫弟,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,449評論 1 293
  • 正文 為了忘掉前任鹏往,我火速辦了婚禮淡诗,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘掸犬。我一直安慰自己袜漩,他們只是感情好绪爸,可當我...
    茶點故事閱讀 67,500評論 6 392
  • 文/花漫 我一把揭開白布湾碎。 她就那樣靜靜地躺著,像睡著了一般奠货。 火紅的嫁衣襯著肌膚如雪介褥。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,370評論 1 302
  • 那天递惋,我揣著相機與錄音柔滔,去河邊找鬼。 笑死萍虽,一個胖子當著我的面吹牛睛廊,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播杉编,決...
    沈念sama閱讀 40,193評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼超全,長吁一口氣:“原來是場噩夢啊……” “哼咆霜!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起嘶朱,我...
    開封第一講書人閱讀 39,074評論 0 276
  • 序言:老撾萬榮一對情侶失蹤蛾坯,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后疏遏,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體脉课,經(jīng)...
    沈念sama閱讀 45,505評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,722評論 3 335
  • 正文 我和宋清朗相戀三年财异,在試婚紗的時候發(fā)現(xiàn)自己被綠了倘零。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,841評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡戳寸,死狀恐怖视事,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情庆揩,我是刑警寧澤俐东,帶...
    沈念sama閱讀 35,569評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站订晌,受9級特大地震影響虏辫,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜锈拨,卻給世界環(huán)境...
    茶點故事閱讀 41,168評論 3 328
  • 文/蒙蒙 一砌庄、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧奕枢,春花似錦娄昆、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,783評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至谷浅,卻和暖如春扒俯,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背一疯。 一陣腳步聲響...
    開封第一講書人閱讀 32,918評論 1 269
  • 我被黑心中介騙來泰國打工撼玄, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人墩邀。 一個月前我還...
    沈念sama閱讀 47,962評論 2 370
  • 正文 我出身青樓掌猛,卻偏偏與公主長得像,于是被迫代替她去往敵國和親眉睹。 傳聞我的和親對象是個殘疾皇子荔茬,可洞房花燭夜當晚...
    茶點故事閱讀 44,781評論 2 354

推薦閱讀更多精彩內(nèi)容