三个元素相互叠加的滑出动画
Slide out animation of three elements on top of each other
我正在寻找制作以下动画的最佳方法。我尝试了一些解决方案,但其中 none 似乎解决了我的问题,因为每个解决方案似乎都在某个时候失败了。我需要做的动画如下:
用户向上滑动手指,tableView & 1 & 2 向上滚动(就像 tableView & 1 & 2 是一个可滚动元素)。然后,当 2 在滚动时变得不可见时,3 & 1 & tableView 变得可滚动(同样,就好像它是一个可滚动元素一样)。然后,当 3 变得不可见(因为它被滚动)时,tableView 是唯一可滚动的元素。
我尝试了什么:
我最初尝试使用简单的动画,例如根据滚动偏移更改每个 1/2/3 元素的高度限制,虽然对我来说这很好,但对审阅者来说却不是,因为他想要在隐藏元素动画之间更精确地滚动
然后我尝试将 panGesture 与滚动结合起来。我将 1 & 2 和 tableView 嵌入到一个滚动视图中,并使用委托函数 shouldRecognizeSimultaneouslyWith 为它设置 panGesture 识别器,在禁用 tableView 滚动时返回 true。然后,在相交 3 时,我禁用了 panGesture 并尝试启用 tableView 滚动,但无法识别哪个 panGesture/scrolling 有效或无效,哪个要禁用,哪个失败或有效 alone/simultaneously.
亲爱的开发人员,您将如何解决这个问题,使动画流畅并如描述的那样?也许,你有一个很棒的主意:)
我使用的是你的想法的简化版本,有一个 UIScrollView
& 3 UILabel
个实例。
您可以轻松地将其调整为 UITableView
& 3 UIView
个实例。
想法
UIScrollView
& 3 UILabel
个实例有一个共同的超级视图。在本例中是 UIViewController.view
.
UIScrollView
布局为全屏(边到边)并延伸到这 3 个 UILabel
实例下方。
UIScrollView
有 contentInset.top = height_of_three_labels
所以它的内容从这些其他实例下面开始。
- 每当
UIScrollView.contentOffset
发生变化时,我们都会为这 3 个实例移动 frame.origin.y
以提供所需的效果。
UI 设置
import UIKit
class ViewController: UIViewController {
let tileHeight: CGFloat = 60
let view1 = UILabel()
let view2 = UILabel()
let view3 = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
let bounds = self.view.bounds
let scrollView = UIScrollView(frame: bounds)
scrollView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
scrollView.delegate = self
scrollView.alwaysBounceVertical = true
self.view.addSubview(scrollView)
view1.frame = CGRect(x: 0, y: 2*tileHeight, width: bounds.width, height: tileHeight)
configureLabel(view1, text: "1", backgroundColor: .red)
view2.frame = CGRect(x: 0, y: tileHeight, width: bounds.width, height: tileHeight)
configureLabel(view2, text: "2", backgroundColor: .blue)
view3.frame = CGRect(x: 0, y: 0, width: bounds.width, height: tileHeight)
configureLabel(view3, text: "3", backgroundColor: .green)
scrollView.contentInset = UIEdgeInsets(top: 3*tileHeight, left: 0, bottom: 0, right: 0)
// 3 is between 2 and 1, 1 is at the top, order is important here
self.view.addSubview(view2)
self.view.addSubview(view3)
self.view.addSubview(view1)
}
private func configureLabel(_ label: UILabel, text: String, backgroundColor: UIColor) {
label.text = text
label.textColor = .white
label.font = .boldSystemFont(ofSize: 34)
label.textAlignment = .center
label.backgroundColor = backgroundColor
}
}
卷轴管理
extension ViewController: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let offset = scrollView.adjustedContentInset.top + scrollView.contentOffset.y
print(offset)
if offset > 0 {
view3.frame.origin.y = (offset > tileHeight) ? (tileHeight - offset) : 0
view2.frame.origin.y = tileHeight - offset
view1.frame.origin.y = 2*tileHeight - offset
}
else {
view3.frame.origin.y = 0
view2.frame.origin.y = tileHeight
view1.frame.origin.y = 2*tileHeight
}
}
}
输出
我认为您将在尝试切换滚动时打一场必败仗。
这是另一种方法...
- 添加table视图作为“容器”的子视图
UIView
- 将三个“顶级”视图添加为子视图
- 给table视图一个
contentInset.top
三个视图的高度加垂直间距
- 限制三个“顶部”视图
3
会一直粘在上面,直到被1
推上去
2
直到 3
滑到下面 1
向上推
- 当 table 视图滚动
时更新 1
的顶部约束
您可以使用此示例代码进行尝试(不需要 @IBOutlet
连接):
class ExampleViewController: UIViewController, UIScrollViewDelegate {
let tableView: UITableView = {
let v = UITableView()
v.translatesAutoresizingMaskIntoConstraints = false
v.separatorInset = .zero
return v
}()
let view1: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.textAlignment = .center
v.backgroundColor = .systemRed
v.text = "1"
return v
}()
let view2: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.textAlignment = .center
v.backgroundColor = .systemGreen
v.text = "2"
return v
}()
let view3: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.textAlignment = .center
v.backgroundColor = .systemBlue
v.text = "3"
return v
}()
// this will hold the tableView and the
// three other views
let containerView: UIView = {
let v = UIView()
v.translatesAutoresizingMaskIntoConstraints = false
// clip to bounds to prevent the "top" views from showing
// as they are "pushed up" out of bounds
v.clipsToBounds = true
return v
}()
// this constraint constant will be changed
// in scrollViewDidScroll
@IBOutlet var view1TopConstraint: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
// add our container view
view.addSubview(containerView)
// add our tableView and three "top" views
containerView.addSubview(tableView)
containerView.addSubview(view2)
containerView.addSubview(view3)
containerView.addSubview(view1)
for v in [view1, view2, view3] {
// all three "top" views should be
// equal width to tableView
// horizontally centered to tableView
// 40-pts tall
NSLayoutConstraint.activate([
v.widthAnchor.constraint(equalTo: tableView.widthAnchor),
v.centerXAnchor.constraint(equalTo: tableView.centerXAnchor),
v.heightAnchor.constraint(equalToConstant: 40.0),
])
}
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// constrain container view with 20-pts "padding"
containerView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
containerView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
containerView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
containerView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0),
// constrain all 4 sides of tableView ot container view
tableView.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 0.0),
tableView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 0.0),
tableView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: 0.0),
tableView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: 0.0),
])
// view3 should stick to the top of the table,
// unless it's being pushed up by view1
let v3Top = view3.topAnchor.constraint(equalTo: tableView.topAnchor)
v3Top.priority = .defaultHigh + 1
// view2 should stick to the bottom of view3,
// unless it's being pushed up by view1
let v2Top = view2.topAnchor.constraint(equalTo: view3.bottomAnchor, constant: 4.0)
v2Top.priority = .defaultHigh
// view1 should ALWAYS be 4-pts from bottom of view2
let v1TopA = view1.topAnchor.constraint(equalTo: view2.bottomAnchor, constant: 4.0)
v1TopA.priority = .required
// view1 should ALWAYS be greater-than-or-equal 4-pts from bottom of view3
let v1TopB = view1.topAnchor.constraint(greaterThanOrEqualTo: view3.bottomAnchor, constant: 4.0)
v1TopB.priority = .required
// view1 top should ALWAYS be greater-than-or-equal top of tableView
let v1TopC = view1.topAnchor.constraint(greaterThanOrEqualTo: tableView.topAnchor)
v1TopC.priority = .required
// 88-pts is 40-pts for view3 and view2 plus 4-pts vertical spacing
// view1 top should NEVER be more-than 88-pts from top of tableView
let v1TopD = view1.topAnchor.constraint(lessThanOrEqualTo: tableView.topAnchor, constant: 88.0)
v1TopD.priority = .required
// view1 top will start at 88-pts from top of tableView
view1TopConstraint = view1.topAnchor.constraint(equalTo: tableView.topAnchor, constant: 88.0)
view1TopConstraint.priority = .defaultHigh + 2
// activate those constraints
NSLayoutConstraint.activate([
v3Top,
v2Top,
v1TopA,
v1TopB,
v1TopC,
v1TopD,
view1TopConstraint,
])
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
tableView.dataSource = self
tableView.delegate = self
// top inset is
// three 40-pt tall views
// plus 4-pts vertical spacing between each
// and 4-pts vertical spacing below view1
tableView.contentInset.top = 132
tableView.contentOffset.y = -132
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
// we're getting called when the tableView scrolls
// invert the contentOffset y
let y = -scrollView.contentOffset.y
// subtract 44-pts (40-pt tall view plus 4-pts vertical spacing)
view1TopConstraint.constant = y - 44.0
}
}
extension ExampleViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 30
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let c = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
c.textLabel?.text = "Row \(indexPath.row)"
return c
}
}
这是开始的样子:
然后,向上滚动一点(2
在 3
下方滑动):
再滚动一点(1
将 3
推到视图之外):
然后 table 滚动,同时 1
保持在顶部:
我正在寻找制作以下动画的最佳方法。我尝试了一些解决方案,但其中 none 似乎解决了我的问题,因为每个解决方案似乎都在某个时候失败了。我需要做的动画如下:
用户向上滑动手指,tableView & 1 & 2 向上滚动(就像 tableView & 1 & 2 是一个可滚动元素)。然后,当 2 在滚动时变得不可见时,3 & 1 & tableView 变得可滚动(同样,就好像它是一个可滚动元素一样)。然后,当 3 变得不可见(因为它被滚动)时,tableView 是唯一可滚动的元素。
我尝试了什么:
我最初尝试使用简单的动画,例如根据滚动偏移更改每个 1/2/3 元素的高度限制,虽然对我来说这很好,但对审阅者来说却不是,因为他想要在隐藏元素动画之间更精确地滚动
然后我尝试将 panGesture 与滚动结合起来。我将 1 & 2 和 tableView 嵌入到一个滚动视图中,并使用委托函数 shouldRecognizeSimultaneouslyWith 为它设置 panGesture 识别器,在禁用 tableView 滚动时返回 true。然后,在相交 3 时,我禁用了 panGesture 并尝试启用 tableView 滚动,但无法识别哪个 panGesture/scrolling 有效或无效,哪个要禁用,哪个失败或有效 alone/simultaneously.
亲爱的开发人员,您将如何解决这个问题,使动画流畅并如描述的那样?也许,你有一个很棒的主意:)
我使用的是你的想法的简化版本,有一个 UIScrollView
& 3 UILabel
个实例。
您可以轻松地将其调整为 UITableView
& 3 UIView
个实例。
想法
UIScrollView
& 3UILabel
个实例有一个共同的超级视图。在本例中是UIViewController.view
.UIScrollView
布局为全屏(边到边)并延伸到这 3 个UILabel
实例下方。UIScrollView
有contentInset.top = height_of_three_labels
所以它的内容从这些其他实例下面开始。- 每当
UIScrollView.contentOffset
发生变化时,我们都会为这 3 个实例移动frame.origin.y
以提供所需的效果。
UI 设置
import UIKit
class ViewController: UIViewController {
let tileHeight: CGFloat = 60
let view1 = UILabel()
let view2 = UILabel()
let view3 = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
let bounds = self.view.bounds
let scrollView = UIScrollView(frame: bounds)
scrollView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
scrollView.delegate = self
scrollView.alwaysBounceVertical = true
self.view.addSubview(scrollView)
view1.frame = CGRect(x: 0, y: 2*tileHeight, width: bounds.width, height: tileHeight)
configureLabel(view1, text: "1", backgroundColor: .red)
view2.frame = CGRect(x: 0, y: tileHeight, width: bounds.width, height: tileHeight)
configureLabel(view2, text: "2", backgroundColor: .blue)
view3.frame = CGRect(x: 0, y: 0, width: bounds.width, height: tileHeight)
configureLabel(view3, text: "3", backgroundColor: .green)
scrollView.contentInset = UIEdgeInsets(top: 3*tileHeight, left: 0, bottom: 0, right: 0)
// 3 is between 2 and 1, 1 is at the top, order is important here
self.view.addSubview(view2)
self.view.addSubview(view3)
self.view.addSubview(view1)
}
private func configureLabel(_ label: UILabel, text: String, backgroundColor: UIColor) {
label.text = text
label.textColor = .white
label.font = .boldSystemFont(ofSize: 34)
label.textAlignment = .center
label.backgroundColor = backgroundColor
}
}
卷轴管理
extension ViewController: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let offset = scrollView.adjustedContentInset.top + scrollView.contentOffset.y
print(offset)
if offset > 0 {
view3.frame.origin.y = (offset > tileHeight) ? (tileHeight - offset) : 0
view2.frame.origin.y = tileHeight - offset
view1.frame.origin.y = 2*tileHeight - offset
}
else {
view3.frame.origin.y = 0
view2.frame.origin.y = tileHeight
view1.frame.origin.y = 2*tileHeight
}
}
}
输出
我认为您将在尝试切换滚动时打一场必败仗。
这是另一种方法...
- 添加table视图作为“容器”的子视图
UIView
- 将三个“顶级”视图添加为子视图
- 给table视图一个
contentInset.top
三个视图的高度加垂直间距 - 限制三个“顶部”视图
3
会一直粘在上面,直到被1
推上去
2
直到3
滑到下面1
向上推- 当 table 视图滚动 时更新
1
的顶部约束
您可以使用此示例代码进行尝试(不需要 @IBOutlet
连接):
class ExampleViewController: UIViewController, UIScrollViewDelegate {
let tableView: UITableView = {
let v = UITableView()
v.translatesAutoresizingMaskIntoConstraints = false
v.separatorInset = .zero
return v
}()
let view1: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.textAlignment = .center
v.backgroundColor = .systemRed
v.text = "1"
return v
}()
let view2: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.textAlignment = .center
v.backgroundColor = .systemGreen
v.text = "2"
return v
}()
let view3: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.textAlignment = .center
v.backgroundColor = .systemBlue
v.text = "3"
return v
}()
// this will hold the tableView and the
// three other views
let containerView: UIView = {
let v = UIView()
v.translatesAutoresizingMaskIntoConstraints = false
// clip to bounds to prevent the "top" views from showing
// as they are "pushed up" out of bounds
v.clipsToBounds = true
return v
}()
// this constraint constant will be changed
// in scrollViewDidScroll
@IBOutlet var view1TopConstraint: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
// add our container view
view.addSubview(containerView)
// add our tableView and three "top" views
containerView.addSubview(tableView)
containerView.addSubview(view2)
containerView.addSubview(view3)
containerView.addSubview(view1)
for v in [view1, view2, view3] {
// all three "top" views should be
// equal width to tableView
// horizontally centered to tableView
// 40-pts tall
NSLayoutConstraint.activate([
v.widthAnchor.constraint(equalTo: tableView.widthAnchor),
v.centerXAnchor.constraint(equalTo: tableView.centerXAnchor),
v.heightAnchor.constraint(equalToConstant: 40.0),
])
}
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// constrain container view with 20-pts "padding"
containerView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
containerView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
containerView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
containerView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0),
// constrain all 4 sides of tableView ot container view
tableView.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 0.0),
tableView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 0.0),
tableView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: 0.0),
tableView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: 0.0),
])
// view3 should stick to the top of the table,
// unless it's being pushed up by view1
let v3Top = view3.topAnchor.constraint(equalTo: tableView.topAnchor)
v3Top.priority = .defaultHigh + 1
// view2 should stick to the bottom of view3,
// unless it's being pushed up by view1
let v2Top = view2.topAnchor.constraint(equalTo: view3.bottomAnchor, constant: 4.0)
v2Top.priority = .defaultHigh
// view1 should ALWAYS be 4-pts from bottom of view2
let v1TopA = view1.topAnchor.constraint(equalTo: view2.bottomAnchor, constant: 4.0)
v1TopA.priority = .required
// view1 should ALWAYS be greater-than-or-equal 4-pts from bottom of view3
let v1TopB = view1.topAnchor.constraint(greaterThanOrEqualTo: view3.bottomAnchor, constant: 4.0)
v1TopB.priority = .required
// view1 top should ALWAYS be greater-than-or-equal top of tableView
let v1TopC = view1.topAnchor.constraint(greaterThanOrEqualTo: tableView.topAnchor)
v1TopC.priority = .required
// 88-pts is 40-pts for view3 and view2 plus 4-pts vertical spacing
// view1 top should NEVER be more-than 88-pts from top of tableView
let v1TopD = view1.topAnchor.constraint(lessThanOrEqualTo: tableView.topAnchor, constant: 88.0)
v1TopD.priority = .required
// view1 top will start at 88-pts from top of tableView
view1TopConstraint = view1.topAnchor.constraint(equalTo: tableView.topAnchor, constant: 88.0)
view1TopConstraint.priority = .defaultHigh + 2
// activate those constraints
NSLayoutConstraint.activate([
v3Top,
v2Top,
v1TopA,
v1TopB,
v1TopC,
v1TopD,
view1TopConstraint,
])
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
tableView.dataSource = self
tableView.delegate = self
// top inset is
// three 40-pt tall views
// plus 4-pts vertical spacing between each
// and 4-pts vertical spacing below view1
tableView.contentInset.top = 132
tableView.contentOffset.y = -132
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
// we're getting called when the tableView scrolls
// invert the contentOffset y
let y = -scrollView.contentOffset.y
// subtract 44-pts (40-pt tall view plus 4-pts vertical spacing)
view1TopConstraint.constant = y - 44.0
}
}
extension ExampleViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 30
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let c = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
c.textLabel?.text = "Row \(indexPath.row)"
return c
}
}
这是开始的样子:
然后,向上滚动一点(2
在 3
下方滑动):
再滚动一点(1
将 3
推到视图之外):
然后 table 滚动,同时 1
保持在顶部: