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

最近做項(xiàng)目的時(shí)候遇到一個(gè)需求,由于App兼容的語(yǔ)言類(lèi)型太多,導(dǎo)致App內(nèi)很多標(biāo)簽顯示的文字不全,這時(shí)候需要滾動(dòng)去顯示。需求類(lèi)似下面這個(gè)樣子。


滾動(dòng)效果展示

思考

1.整個(gè)App的label去做一個(gè)滾動(dòng)顯示,首先要考慮到性能問(wèn)題,性能問(wèn)題的話(huà)就不考慮使用UIView層去實(shí)現(xiàn)該效果,因此,文字滾動(dòng)層考慮用Layer去處理。
2.能夠影響到文本顯示和是否能完全顯示文本的屬性有text、frame、font、textColor,因此我們需要對(duì)這幾項(xiàng)屬性進(jìn)行處理。

在設(shè)置text的屬性的時(shí)候,我們需要判斷text的長(zhǎng)度是否大于label的長(zhǎng)度, 如果大于label的長(zhǎng)度,我們需要將text文本處理到layer層,并且做滾動(dòng)動(dòng)畫(huà)。在這是framefont的時(shí)候我們需要重新做該判斷,因?yàn)檫@兩個(gè)屬性影響了label的長(zhǎng)度和文本的大小,設(shè)置textColor的時(shí)候我們需要把layer上的文本顏色也相應(yīng)修改成該顏色。

實(shí)現(xiàn)

創(chuàng)建一個(gè)UILabel的子類(lèi),并且添加CATextLayer的屬性

import UIKit

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

重寫(xiě)以上四個(gè)屬性的set方法,加上判斷是否需要滾動(dòng)的邏輯

    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
            {
                /// 如果判斷需要滾動(dòng),設(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
                /// 動(dòng)畫(huà)
                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
            {
                /// 如果不需要滾動(dòng),移除動(dòng)畫(huà)和layer
                textLayer.removeAllAnimations()
                textLayer.removeFromSuperlayer()
            }
        }
    }

因?yàn)樗膫€(gè)重寫(xiě)的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
    }

測(cè)試結(jié)果得到如下結(jié)果,因?yàn)橥浱幚韑abel自己本身顯示的文本。

效果展示

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

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

經(jīng)過(guò)簡(jiǎn)單測(cè)試,無(wú)論是改變font、frame、text都能夠正常判斷并且達(dá)到想要的效果。

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

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

我們創(chuàng)建一個(gè)UILabel的分類(lèi),并且根據(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)方法,我們需要重寫(xiě)的方法我們都要替換
        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 標(biāo)簽顯示的文字
- (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 坐標(biāo)
- (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ù)文字長(zhǎng)短自動(dòng)判斷是否需要顯示TextLayer,并且滾動(dòng)
- (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存放動(dòng)畫(huà)對(duì)象,避免多次生成
- (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;
}

/// 判斷是否需要滾動(dòng)
- (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文件中導(dǎo)入該分類(lèi),項(xiàng)目中所有的UILabel就能自動(dòng)判斷并且實(shí)現(xiàn)滾動(dòng)顯示的效果。
大家可以試一試,如果有問(wèn)題可以相互交流一下。

手?jǐn)]代碼不易,如果對(duì)您有幫助的,可以幫忙點(diǎn)個(gè)贊!謝謝

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容