swift iOS 中的 3 个点的动画

Animating 3 dots in swift iOS

我有一个标签和一个图像视图。我的目标是让 3 个点在标签的前面设置动画,并在标签的底部显示如下

我只能在 12promax 上使点移动得很好,但是我无法弄清楚如何使它始终按预期在不同的 phone 屏幕尺寸上工作。我该如何编码,以便无论屏幕大小或 uilabel fontsize 都能达到相同的结果?

动画 ImageView(3 个点)

  func showAnimatingDotsInImageView(dots: UIImageView)
 {
        let newX = view.bounds.width / 896 * 20
        let lay = CAReplicatorLayer()
        lay.frame = CGRect(x: newX,y: 0,width: dots.bounds.width,height: dots.bounds.height)
        let bar = CALayer()
        bar.frame = CGRect(x: 0,y: (dots.bounds.height/2) + 8 ,width: 8,height: 8)  //make the objs smaller or bigger
        bar.cornerRadius = bar.frame.width / 2  //make a circle, if you uncomment this you will get rects
        bar.backgroundColor = UIColor.black.cgColor   //colour of the objs
        lay.addSublayer(bar)
        lay.instanceCount = 3   //How many instances / objs do you want to see
        lay.instanceTransform = CATransform3DMakeTranslation(15, 0, 0) //1st arg is the spacing between the instances
        let anim = CABasicAnimation(keyPath: #keyPath(CALayer.opacity))
        anim.fromValue = 1.0
        anim.toValue = 0.2
        anim.duration = 1
        anim.repeatCount = .infinity
        bar.add(anim, forKey: nil)
        lay.instanceDelay = anim.duration / Double(lay.instanceCount)
    
        dots.layer.addSublayer(lay)    // add to the view

        }

'Retrieving boxes from the main menu' 是 UILabel 并且点在 UIImageView

上动画

你已经很接近了...只要做一些改变就可以让你到达你想要的位置。

如果您将 UILabelUIImageView(或普通 UIView)限制在标签的右边缘,您应该能够将 CAReplicatorLayer无需担心“屏幕尺寸”。

看看我是如何修改你的代码的:

// baseline = to put the bottom of the dots at the baseline of the text in the label
// dotXOffset = gap between end of label and first dot
// dotSize = dot width and height
// dotSpacing = gap between dots
func showAnimatingDotsInImageView(dotsView: UIView, baseline: CGFloat, dotXOffset: CGFloat, dotSize: CGFloat, dotSpacing: CGFloat) {
    let lay = CAReplicatorLayer()
    let bar = CALayer()
    bar.frame = CGRect(x: dotXOffset, y: baseline - dotSize, width: dotSize, height: dotSize)
    bar.cornerRadius = bar.frame.width / 2  // we want round dots
    bar.backgroundColor = UIColor.black.cgColor
    lay.addSublayer(bar)
    lay.instanceCount = 3   //How many instances / objs do you want to see
    lay.instanceTransform = CATransform3DMakeTranslation(dotSpacing, 0, 0) //1st arg is the spacing between the instances
    let anim = CABasicAnimation(keyPath: #keyPath(CALayer.opacity))
    anim.fromValue = 1.0
    anim.toValue = 0.2
    anim.duration = 1
    anim.repeatCount = .infinity
    bar.add(anim, forKey: nil)
    lay.instanceDelay = anim.duration / Double(lay.instanceCount)
    dotsView.layer.addSublayer(lay)    // add to the view
}

这是一个完整的例子:

class SimpleViewController: UIViewController {
    
    let testLabel = UILabel()
    let testDotsView = UIView()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        testLabel.font = .systemFont(ofSize: 24.0)
        
        testLabel.text = "Retrieving boxes"
        
        // so we can see the label frame
        testLabel.backgroundColor = .cyan
        
        testLabel.translatesAutoresizingMaskIntoConstraints = false
        testDotsView.translatesAutoresizingMaskIntoConstraints = false
        
        view.addSubview(testLabel)
        view.addSubview(testDotsView)
        
        // always respect safe area
        let g = view.safeAreaLayoutGuide
        
        NSLayoutConstraint.activate([
            
            // let's constrain the label
            //  40-pts from Leading
            //  40-pts from Bottom
            testLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
            testLabel.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -40.0),
            
            // constrain dots view to
            //  Top of label
            //  Trailing of label
            testDotsView.topAnchor.constraint(equalTo: testLabel.topAnchor),
            testDotsView.leadingAnchor.constraint(equalTo: testLabel.trailingAnchor, constant: 0.0),
            // dots image view Width and Height can be 0 (we can draw the layer outside the bounds)
            testDotsView.heightAnchor.constraint(equalToConstant: 0.0),
            testDotsView.widthAnchor.constraint(equalToConstant: 0.0),
            
        ])
        
        // get the label font's baseline y-value
        let bl: CGFloat = testLabel.font.ascender
        showAnimatingDotsInImageView(dotsView: testDotsView, baseline: bl, dotXOffset: 4.0, dotSize: 4.0, dotSpacing: 8.0)
    }
    
    // baseline = to put the bottom of the dots at the baseline of the text in the label
    // dotXOffset = gap between end of label and first dot
    // dotSize = dot width and height
    // dotSpacing = gap between dots
    func showAnimatingDotsInImageView(dotsView: UIView, baseline: CGFloat, dotXOffset: CGFloat, dotSize: CGFloat, dotSpacing: CGFloat) {
        let lay = CAReplicatorLayer()
        let bar = CALayer()
        bar.frame = CGRect(x: dotXOffset, y: baseline - dotSize, width: dotSize, height: dotSize)
        bar.cornerRadius = bar.frame.width / 2  // we want round dots
        bar.backgroundColor = UIColor.black.cgColor
        lay.addSublayer(bar)
        lay.instanceCount = 3   //How many instances / objs do you want to see
        lay.instanceTransform = CATransform3DMakeTranslation(dotSpacing, 0, 0) //1st arg is the spacing between the instances
        let anim = CABasicAnimation(keyPath: #keyPath(CALayer.opacity))
        anim.fromValue = 1.0
        anim.toValue = 0.2
        anim.duration = 1
        anim.repeatCount = .infinity
        bar.add(anim, forKey: nil)
        lay.instanceDelay = anim.duration / Double(lay.instanceCount)
        dotsView.layer.addSublayer(lay)    // add to the view
    }

}

编辑 - 回应“krishan kumar”评论...

要让动画在从后台返回时恢复,您需要添加一个 Notification Observer。

为“动画点”视图使用自定义 UIView 子类会容易得多,所以这里有一个简单的例子:

class DotsView: UIView {
    
    // baseline = to put the bottom of the dots at the baseline of the text in the label
    // dotXOffset = gap between end of label and first dot
    // dotSize = dot width and height
    // dotSpacing = gap between dots

    public var baseline: CGFloat = 0
    public var dotXOffset: CGFloat = 4.0
    public var dotSize: CGFloat = 4.0
    public var dotSpacing: CGFloat = 8.0

    private let lay = CAReplicatorLayer()
    private let bar = CALayer()

    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    func commonInit() {
        lay.addSublayer(bar)
        layer.addSublayer(lay)
    }
    public func beginAnimating() {
        bar.frame = CGRect(x: dotXOffset, y: baseline - dotSize, width: dotSize, height: dotSize)
        // we want round dots
        bar.cornerRadius = bar.frame.width / 2.0
        bar.backgroundColor = UIColor.black.cgColor
        //How many instances / objs we want to see
        lay.instanceCount = 3
        //1st arg is the spacing between the instances
        lay.instanceTransform = CATransform3DMakeTranslation(dotSpacing, 0, 0)
        let anim = CABasicAnimation(keyPath: #keyPath(CALayer.opacity))
        anim.fromValue = 1.0
        anim.toValue = 0.2
        anim.duration = 1
        anim.repeatCount = .infinity
        bar.add(anim, forKey: nil)
        // so the dots animate in sequence
        lay.instanceDelay = anim.duration / Double(lay.instanceCount)
    }
    public func stopAnimating() {
        layer.removeAllAnimations()
    }
}

以及展示如何使用它的示例控制器,包括 starting/stopping 应用在前景和背景之间移动时的动画:

class SimpleViewController: UIViewController {
    
    let testLabel = UILabel()
    
    // custom DotsView
    let testDotsView = DotsView()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = .systemBackground
        
        testLabel.font = .systemFont(ofSize: 24.0)
        
        testLabel.text = "Retrieving boxes"
        
        // so we can see the label frame
        testLabel.backgroundColor = .cyan
        
        testLabel.translatesAutoresizingMaskIntoConstraints = false
        testDotsView.translatesAutoresizingMaskIntoConstraints = false
        
        view.addSubview(testLabel)
        view.addSubview(testDotsView)
        
        // always respect safe area
        let g = view.safeAreaLayoutGuide
        
        NSLayoutConstraint.activate([
            
            // let's constrain the label
            //  40-pts from Leading
            //  40-pts from Bottom
            testLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
            testLabel.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -40.0),
            
            // constrain dots view to
            //  Top of label
            //  Trailing of label
            testDotsView.topAnchor.constraint(equalTo: testLabel.topAnchor),
            testDotsView.leadingAnchor.constraint(equalTo: testLabel.trailingAnchor, constant: 0.0),
            // dots image view Width and Height can be 0 (we can draw the layer outside the bounds)
            testDotsView.heightAnchor.constraint(equalToConstant: 0.0),
            testDotsView.widthAnchor.constraint(equalToConstant: 0.0),
            
        ])
        
        // get the label font's baseline y-value
        testDotsView.baseline = testLabel.font.ascender
        
        // use defaults or set values here
        //testDotsView.dotXOffset = 4.0
        //testDotsView.dotSize = 4.0
        //testDotsView.dotSpacing = 8.0
        
        testDotsView.beginAnimating()

        // we want to
        //  Stop the Dots animation when the app goes into the Background, and
        //  Start the Dots animation when the app Enters the Foreground
        NotificationCenter.default.addObserver(self, selector: #selector(myEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(myEnterBackground), name: UIApplication.didEnterBackgroundNotification, object: nil)
    }
    
    @objc func myEnterBackground() {
        testDotsView.stopAnimating()
    }
    @objc func myEnterForeground() {
        testDotsView.beginAnimating()
    }
    
}