连续从左到右动画 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()
}
}
输出:
我正在尝试制作一些圆形图像的动画,以便它们从左向右移动。一旦圆圈开始从屏幕右侧消失,它就会连续循环地重新出现在左侧。我正在使用 stackviews 来完成此操作,但很难将图像环绕在容器中。
我从这个
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()
}
}
输出: