iOS 文字掉落效果 CATextLayer

本文介绍如何使用Swift和TextKit框架中的NSTextStorage、NSLayoutManager、NSTextContainer以及CATextLayer实现文字掉落的动画效果。文章详细展示了代码实现过程,包括文字的布局、动画设置以及在UIView中的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

对于每个文字的处理是很复杂的一个工作,今天我们就先说一个文字掉落的效果,先看张效果图。

 

这个是通过 NSTextStorage,NSLayoutManager,NSTextContainer以及CATextLayer实现的。我们先看代码:

设备:xcode 10.1

语言:swift 4.2 

  • NSTextStorage
Note for subclassing NSTextStorage: NSTextStorage is a semi-abstract subclass of NSMutableAttributedString. It implements change management (beginEditing/endEditing), verification of attributes, delegate handling, and layout management notification. The one aspect it does not implement is the actual attributed string storage --- this is left up to the subclassers, which need to override the two NSMutableAttributedString primitives in addition to two NSAttributedString primitives:
     
     - (NSString *)string;
     - (NSDictionary *)attributesAtIndex:(NSUInteger)location effectiveRange:(NSRangePointer)range;
    
     - (void)replaceCharactersInRange:(NSRange)range withString:(NSString *)str;
     - (void)setAttributes:(NSDictionary *)attrs range:(NSRange)range;
     
     These primitives should perform the change then call edited:range:changeInLength: to get everything else to happen.

大意思是说他主要是保存文字信息属性,但是他并不真正的保存文字,需要我们自己来实现这几个方法,他是NSMutableAttributedString的子类。

  • NSLayoutManager
NSLayoutManager maps Unicode character codes to glyphs, sets the glyphs in a series of NSTextContainer objects, and displays them in a series of NSTextView objects. In addition to its core function of laying out text, a layout manager object coordinates its text view objects, provides services to those text views to support NSRulerView instances for editing paragraph styles, and handles the layout and display of text attributes not inherent in glyphs (such as underline or strikethrough). You can create a subclass of NSLayoutManager to handle additional text attributes, whether inherent or not.

这个是文字排版及文字内容的布局。

  • NSTextContainer
An NSLayoutManager uses NSTextContainer to determine where to break lines, lay out portions of text, and so on. An NSTextContainer object typically defines rectangular regions, but you can define exclusion paths inside the text container to create regions where text does not flow. You can also subclass to create text containers with nonrectangular regions, such as circular regions, regions with holes in them, or regions that flow alongside graphics.

Instances of the NSTextContainer, NSLayoutManager, and NSTextStorage classes can be accessed from threads other than the main thread as long as the app guarantees access from only one thread at a time.

这个是需要显示文字的区域。

代码实现:

class TextAnimationView: UIView , NSLayoutManagerDelegate,CAAnimationDelegate{

    private let textStorage = NSTextStorage(string: "");
    private let textLayout = NSLayoutManager();
    private let textContainer = NSTextContainer();
    
    private var textLayers = [CALayer]();
    private var textLayerPostions = [CGPoint]();
    
    var text: String!{
        didSet{
            let textA = NSAttributedString(string: text, attributes: [NSAttributedString.Key.foregroundColor:UIColor.red,NSAttributedString.Key.font:UIFont.fitSize(size: 20)]);
            textStorage.setAttributedString(textA);

        }
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame);
        
       
        
        textStorage.addLayoutManager(textLayout);
        textLayout.addTextContainer(textContainer);
        textLayout.delegate = self;
        textContainer.size = bounds.size;
        textContainer.maximumNumberOfLines = 0;
        
    }

    
    
    
    func layoutManager(_ layoutManager: NSLayoutManager, didCompleteLayoutFor textContainer: NSTextContainer?, atEnd layoutFinishedFlag: Bool) {
        
        
        if layoutFinishedFlag {
            calculateLayer();
        }
        
    }
    
    func calculateLayer() -> Void {
        textLayers.removeAll();
        textLayerPostions.removeAll();
        if text == nil {
            return;
        }
        
        var index = 0;
        
        while index < text.count {
            
            let glyRang = NSRange(location: index, length: 1);
            
            let count = textStorage.attributedSubstring(from: glyRang);
            

            let charRange = textLayout.characterRange(forGlyphRange: glyRang, actualGlyphRange: nil);
            let glyRect = textLayout.boundingRect(forGlyphRange: glyRang, in: textContainer);
            
            let textLayer = CATextLayer();
            textLayer.string = count;
            textLayer.frame = glyRect;
            textLayer.backgroundColor = UIColor.clear.cgColor;
            textLayer.contentsScale = iOSIPhoneInfoData.scale;
            layer.addSublayer(textLayer);
            textLayers.append(textLayer);
            textLayerPostions.append(textLayer.position);

            index += charRange.length;

        }
    }
    
    func runTextAnimationPostion() -> Void {
        
        for (idx,item) in textLayers.enumerated() {
            item.position = textLayerPostions[idx];
            item.removeAllAnimations();
        }
        
        for (index,item) in textLayers.enumerated() {
            let position = textLayerPostions[index];
            
            let point = CGPoint(x: item.position.x, y: -100);
            item.position = point;
            let animation = CABasicAnimation(keyPath: "position");
            animation.fromValue = point
            animation.toValue = position;
            let duration = Double(10.1 / Double(textLayers.count));
            animation.duration = duration;
            animation.repeatCount = 1;
            animation.beginTime = CACurrentMediaTime() + Double(index) * duration;
            animation.fillMode = .forwards;
            animation.isRemovedOnCompletion = false;
            item.add(animation, forKey: "\(index)");
            
            
        }
        
    }

    
    func runTextAnimation() -> Void {
        runTextAnimationPostion();
        return;
        for item in textLayers {
            var transform = CATransform3DRotate(item.transform, .pi/4, 0, 0, 1);
            transform.m34 = 0.1/400.0;
            
            let animation = CABasicAnimation(keyPath: "transform");
            animation.fromValue = transform//CGPoint(x: CGFloat(arc4random_uniform(UInt32(width))), y: CGFloat(arc4random_uniform(UInt32(height))));
            animation.toValue = item.transform;
            animation.duration = 4;
            animation.repeatCount = 1;
            animation.fillMode = .forwards;
            animation.isRemovedOnCompletion = false;
            item.add(animation, forKey: "123");
            
        }
        
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
}

调用:

textLabel = TextAnimationView(frame: bounds().insetBy(dx: 20, dy: 80));
addSubview(subView: textLabel);
textLabel.backgroundColor = UIColor.white;
textLabel.text = "1.要渲染展示的内容。2.将内容渲染在某个视图上。3.内容渲染在视图上的尺寸位置和形状。在TextKit框架中,提供了几个类分别对应处理上述的必要条件:1.NSTextStorage对应要渲染展示的内容。2.UITextView对应要渲染的视图。";

运行:

textLabel.runTextAnimation();

最后:demo地址

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值