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

思考
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à)。在這是frame和font的時(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è)贊!謝謝