连续从左到右动画 UIStackView

Animate UIStackView left to right continuously

我正在尝试制作一些圆形图像的动画,以便它们从左向右移动。一旦圆圈开始从屏幕右侧消失,它就会连续循环地重新出现在左侧。我正在使用 stackviews 来完成此操作,但很难将图像环绕在容器中。

我从这个 中得到了一些提示,但我没有得到圆圈的环绕效果。我现在所拥有的圆圈从左到右进入,在动画结束时,mockedTemplateStackView 圆圈位于屏幕中间,在动画开始之前向左半屏 space。

    override func viewDidLoad() {
        super.viewDidLoad()
        
        mockTemplateStackView.frame = templateStackView.frame
        mockTemplateStackView.transform = CGAffineTransform.identity.translatedBy(x: -self.view.bounds.width, y: 0)
        mockTemplateStackView.alignment = .fill
        mockTemplateStackView.distribution = .fillEqually
        mockTemplateStackView.axis = .horizontal
        
        masterTemplateStackView.addArrangedSubview(mockTemplateStackView)
        
        for _ in 1...5 {
            let imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: templateStackView.bounds.height * 0.5, height: templateStackView.bounds.height))
            imageView.image = UIImage(named: "circle")
            imageView.backgroundColor = .white
            imageView.contentMode = .scaleAspectFit
            templateStackView.addArrangedSubview(imageView)
        }
        
        for _ in 1...5 {
            let imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: mockTemplateStackView.bounds.height * 0.5, height: mockTemplateStackView.bounds.height))
            imageView.image = UIImage(named: "circle")
            imageView.backgroundColor = .white
            imageView.contentMode = .scaleAspectFit
            mockTemplateStackView.addArrangedSubview(imageView)
        }
    }
    
    override func viewDidAppear(_ animated: Bool) {
       super.viewDidAppear(animated)

       startAnimating()
    }

   func startAnimating() {
    UIView.animateKeyframes(withDuration: 10, delay: 0, options: .repeat, animations: {
               self.templateStackView.center.x += self.view.bounds.width
        self.mockTemplateStackView.center.x += self.view.bounds.width

           }, completion: nil)
   }

StackView布局

动画开始:

动画结束:

你走在正确的轨道上...

我们需要两个堆栈视图,所以我们可以显示一个进入,另一个离开。

一种方法是给每个堆栈视图两个 前导约束。每个都有一个等于“容器”视图的前导锚点的前导约束,每个都有一个等于另一个堆栈视图的前导锚点的前导约束.

通过更改约束优先级,我们可以使一个堆栈“跟随”另一个堆栈。当“leader”堆栈退出视图时,我们交换“leader”和“follower”并再次设置动画。

这是一个示例 - 我试图包含大量注释以使其清楚,但请注意它只是 示例代码,不应被视为“生产就绪” :

SimpleCircleView - 轮廓圆视图

class SimpleCircleView: UIView {
    var c: UIColor = .white
    override func layoutSubviews() {
        layer.borderWidth = 1
        layer.borderColor = c.cgColor
        layer.cornerRadius = bounds.width * 0.5
    }
}

MyStackView - 用于管理约束的堆栈视图子类

class MyStackView: UIStackView {
    var leaderConstraint: NSLayoutConstraint!
    var followerConstraint: NSLayoutConstraint!
}

CarouselView - 执行动画的视图

class CarouselView: UIView {
    
    public var speed: TimeInterval = 10
    public var stack1Color: UIColor = .blue
    public var stack2Color: UIColor = .blue

    private var stacks: [MyStackView] = []
    private var leaderStackID: Int = 0
    private var animator: UIViewPropertyAnimator!
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    func commonInit() -> Void {
        
        // create two stack views
        //  each with 5 circle views
        //  we can use different colors if we want to see the "lead/follow" change
        [stack1Color, stack2Color].forEach { c in
            let sv = MyStackView()
            sv.distribution = .fillEqually
            sv.translatesAutoresizingMaskIntoConstraints = false
            addSubview(sv)
            for _ in 1...5 {
                let v = SimpleCircleView()
                v.c = c
                // 1:1 ratio
                v.widthAnchor.constraint(equalTo: v.heightAnchor).isActive = true
                sv.addArrangedSubview(v)
            }
            NSLayoutConstraint.activate([
                sv.topAnchor.constraint(equalTo: topAnchor),
                sv.bottomAnchor.constraint(equalTo: bottomAnchor),
            ])
            stacks.append(sv)
        }
        
        var c: NSLayoutConstraint = NSLayoutConstraint()

        // init "leader" constraints
        //  each stack will be constrained Leading to Leading of self
        c = stacks[0].leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0)
        c.priority = .defaultLow
        c.isActive = true
        stacks[0].leaderConstraint = c

        c = stacks[1].leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0)
        c.priority = .defaultLow
        c.isActive = true
        stacks[1].leaderConstraint = c
        
        // init "follower" constraints
        //  each stack will be constrained Leading to Leading of other stack
        c = stacks[0].leadingAnchor.constraint(equalTo: stacks[1].leadingAnchor)
        c.priority = .defaultLow
        c.isActive = true
        stacks[0].followerConstraint = c
        
        c = stacks[1].leadingAnchor.constraint(equalTo: stacks[0].leadingAnchor)
        c.priority = .defaultLow
        c.isActive = true
        stacks[1].followerConstraint = c
        
        // start with both stacks hidden
        stacks[0].isHidden = true
        stacks[1].isHidden = true

        // set to false if we want to see the circles
        //  as they travel outside the bounds of self
        //  (likely during dev only)
        clipsToBounds = true
    }
    
    public func toggleAnimation() -> Void {
        // start or toggle running/paused
        if animator == nil {
            startAnimation()
        } else {
            if animator.isRunning {
                animator.pauseAnimation()
            } else {
                animator.startAnimation()
            }
        }
    }
    
    private func startAnimation() -> Void {
        
        // un-hide both stacks
        stacks[0].isHidden = false
        stacks[1].isHidden = false
        
        leaderStackID = 0
        
        let svLeader = stacks[leaderStackID]
        let svFollow = stacks[abs(leaderStackID - 1)]

        // set both follower constraint constants to width of self
        svLeader.followerConstraint.constant = -(bounds.width)
        svFollow.followerConstraint.constant = -(bounds.width)

        // start with the Leader ready to "come into view"
        svLeader.leaderConstraint.constant = -stacks[0].frame.width
        svLeader.leaderConstraint.priority = .defaultHigh
        
        // tell the Follower to "follow the Leader"
        svFollow.followerConstraint.priority = .defaultHigh

        DispatchQueue.main.async {
            self.runAnimation()
        }
    }
    
    private func runAnimation() {
        
        let svLeader = stacks[leaderStackID]
        
        animator = UIViewPropertyAnimator(duration: speed, curve: .linear)
        
        animator.addAnimations {
            // animate the Leader to disappear off the right side of self
            svLeader.leaderConstraint.constant = self.bounds.width
            self.layoutIfNeeded()
        }

        animator.addCompletion { _ in
            self.swapLeader()
        }
        
        animator.startAnimation()

    }
    
    private func swapLeader() -> Void {

        // current Leader has gone off the right side
        // current Follower has appeard at the left side
        // so swap the constraint priorities so
        //  Follower becomes Leader
        //    and
        //  Leader becomes Follower
        
        let svLeader = stacks[leaderStackID]
        let svFollow = stacks[abs(leaderStackID - 1)]
        
        svLeader.leaderConstraint.priority = .defaultLow
        svLeader.followerConstraint.priority = .defaultHigh

        svFollow.followerConstraint.priority = .defaultLow
        svFollow.leaderConstraint.priority = .defaultHigh
        
        svFollow.leaderConstraint.constant = 0.0

        // this will toggle the leader ID between 0 and 1
        leaderStackID = abs(leaderStackID - 1)

        DispatchQueue.main.async {
            self.runAnimation()
        }
    }
    
}

CarouselTestViewController - 示例视图控制器

class CarouselTestViewController: UIViewController {
    
    let cView = CarouselView()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = .lightGray
        
        cView.backgroundColor = .white
        cView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(cView)

        let g = view.safeAreaLayoutGuide
        NSLayoutConstraint.activate([
            cView.topAnchor.constraint(equalTo: g.topAnchor, constant: 60.0),
            cView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
            cView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
            cView.heightAnchor.constraint(equalToConstant: 30.0),
        ])
        
        let t = UITapGestureRecognizer(target: self, action: #selector(self.didTap(_:)))
        view.addGestureRecognizer(t)

        // set animation duration if desired
        //cView.speed = 5
        // set circle colors if desired
        //cView.stack1Color = .red
        //cView.stack2Color = .green

        // add an instruction label
        let v = UILabel()
        v.text = "Tap anywhere to toggle animation."
        v.backgroundColor = .yellow
        v.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(v)
        NSLayoutConstraint.activate([
            v.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            v.centerYAnchor.constraint(equalTo: view.centerYAnchor),
        ])
    }
    
    @objc func didTap(_ g: UIGestureRecognizer) -> Void {
        cView.toggleAnimation()
    }
}

输出: