我如何为这个 tableview header Y 位置变化设置动画
How can I animate this tableview header Y position change
如果用户向上滚动我的提要并且 header 不在屏幕上,我想将我的 table 视图 header 滑动到视图中。如果用户然后向下滚动我想再次隐藏它。
我相信我可以使用以下方法工作:
final class AccountSettingsController: UITableViewController {
let items: [Int] = Array(0...500)
private lazy var menuView: UIView = {
let view = UIView(frame: .zero)
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .systemPink
view.heightAnchor.constraint(equalToConstant: 120).isActive = true
view.widthAnchor.constraint(equalToConstant: 100).isActive = true
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
edgesForExtendedLayout = []
extendedLayoutIncludesOpaqueBars = false
tableView.tableHeaderViewWithAutolayout = menuView
tableView.tableFooterView = .init()
tableView.refreshControl = .init()
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell()
cell.textLabel?.text = "This is setting #\(indexPath.row)"
return cell
}
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
let contentOffset = scrollView.contentOffset.y
let transY = scrollView.panGestureRecognizer.translation(in: scrollView).y
if scrollView.contentOffset.y > 120 {
if transY > 0 {
menuView.transform = .init(translationX: 0, y: contentOffset)
} else if transY < 0 {
menuView.transform = .identity
}
return
}
if scrollView.contentOffset.y <= 120 {
if transY < 0 {
menuView.transform = .identity
} else if transY > 0 {
menuView.transform = .init(translationX: 0, y: contentOffset)
}
}
}
}
我想制作滑入/滑出 header 的动画,使其上下滑动,而不是仅仅出现。
我尝试添加 UIView.animate
个块,例如
if scrollView.contentOffset.y <= 120 {
if transY < 0 {
menuView.transform = .identity
} else if transY > 0 {
UIView.animate(withDuration: 0.25, animations: {
self.menuView.transform = .init(translationX: 0, y: contentOffset)
})
}
}
但这会在 header 上产生随机跳跃效果,根本达不到我想要的效果。
我附上了一张显示当前效果的 gif,您可以看到它只是出现和消失。我希望它可以上下动画以显示/隐藏。
我还有一个扩展程序可以为我的 table 视图 header 设置正确的大小,您可以在此处找到 -
extension UITableView {
var tableHeaderViewWithAutolayout: UIView? {
set (view) {
tableHeaderView = view
if let view = view {
lowerPriorities(view)
view.frame.size = view.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
tableHeaderView = view
}
}
get {
return tableHeaderView
}
}
fileprivate func lowerPriorities(_ view: UIView) {
for cons in view.constraints {
if cons.priority.rawValue == 1000 {
cons.priority = UILayoutPriority(rawValue: 999)
}
for v in view.subviews {
lowerPriorities(v)
}
}
}
}
我同意 Claudio 的观点,使用 tableHeaderView
可能会使事情复杂化,因为它并不是真正打算以这种方式进行操作。
试试下面的方法,看看是否能满足您的需求。
不是操纵 header 的偏移量,而是操纵高度。还是可以给人一种卷轴的感觉。
如果您想应用一个阈值,您也可以引入 scrollView.panGestureRecognizer.velocity(in: scrollView)
,以便菜单仅在特定的滚动速度后出现。
class MyTableViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
private let data: [Int] = Array(0...99)
private let menuView: UIView = {
let view = UIView(frame: .zero)
view.backgroundColor = .purple
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
private lazy var tableView: UITableView = {
let view = UITableView(frame: .zero)
view.translatesAutoresizingMaskIntoConstraints = false
view.dataSource = self
view.delegate = self
// view.refreshControl = .init()
view.contentInsetAdjustmentBehavior = .never
view.tableFooterView = .init()
return view
}()
var menuViewHeightAnchor: NSLayoutConstraint!
var tableViewTopAnchor: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
menuViewHeightAnchor = menuView.heightAnchor.constraint(equalToConstant: 120)
tableViewTopAnchor = tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 120)
edgesForExtendedLayout = []
[tableView, menuView].forEach(view.addSubview)
NSLayoutConstraint.activate([
menuViewHeightAnchor,
menuView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
menuView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
menuView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tableViewTopAnchor,
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
])
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell(frame: .zero)
cell.textLabel?.text = "Cell #\(indexPath.row)"
return cell
}
private var previousOffsetY: CGFloat = 0
private var velocityThreshold: CGFloat = 500
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let offsetY = min(120, max(0, scrollView.contentOffset.y))
let translation = scrollView.panGestureRecognizer.translation(in: scrollView)
let velocity = scrollView.panGestureRecognizer.velocity(in: scrollView)
let offsetYDiff = previousOffsetY - offsetY
previousOffsetY = offsetY
let adjustedOffset = menuViewHeightAnchor.constant + offsetYDiff
tableViewTopAnchor.constant = 120 - max(0, offsetY)
menuViewHeightAnchor.constant = min(120, adjustedOffset)
guard offsetY == 120 else { return }
if translation.y > 0 { // DRAGGED DOWN
UIView.animate(withDuration: 0.33, animations: {
self.menuViewHeightAnchor.constant = 120
self.view.layoutIfNeeded()
})
} else if translation.y < 0 { // DRAGGED UP
UIView.animate(withDuration: 0.33, animations: {
self.menuViewHeightAnchor.constant = 0
self.view.layoutIfNeeded()
})
}
}
}
我认为最好的方法是将 menuView
设置为 tableViewController
的子视图,例如 viewDidLoad
方法并在 [=16] 上添加一个插图=] 像这样:
let menuHeight: CGFloat = 120
var scrollStartingYPoint: CGFloat = 0
private lazy var menuView: UIView = {
let view = UIView(frame: .zero)
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .systemPink
view.heightAnchor.constraint(equalToConstant: menuHeight).isActive = true
view.widthAnchor.constraint(equalToConstant: 100).isActive = true
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
view.addSubview(menuView)
NSLayoutConstraint.activate([
menuView.leftAnchor.constraint(equalTo: view.leftAnchor),
menuView.topAnchor.constraint(equalTo: view.topAnchor)
])
edgesForExtendedLayout = []
extendedLayoutIncludesOpaqueBars = false
tableView.contentInset.top = menuHeight
}
然后你可以在show/hide菜单中添加滚动逻辑,我做的和你用scrollViewWillBeginDragging
方法存储初始滚动偏移的方法有点不同,然后确定滚动方向和偏移量:
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
scrollStartingYPoint = scrollView.contentOffset.y
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let tableYScrollOffset = scrollView.contentOffset.y
let offset = abs(tableYScrollOffset - scrollStartingYPoint)
if tableYScrollOffset < scrollStartingYPoint { // scrolling down
if menuView.frame.origin.y >= 0 {
return
}
var translationY = -menuHeight + offset
if translationY > 0 {
translationY = 0
}
menuView.transform = CGAffineTransform(translationX: 0, y: translationY)
} else { // scrolling up
if menuView.frame.origin.y <= -menuHeight {
return
}
var translationY = -offset
if translationY < -menuHeight {
translationY = -menuHeight
}
menuView.transform = CGAffineTransform(translationX: 0, y: translationY)
}
}
如果你想动画化菜单的呈现,那么你可以改变之前的代码并像这样使用你的动画代码:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let tableYScrollOffset = scrollView.contentOffset.y
let offset = abs(tableYScrollOffset - scrollStartingYPoint)
if tableYScrollOffset < scrollStartingYPoint { // scrolling down
UIView.animate(withDuration: 0.25, animations: {
self.menuView.transform = .identity
})
} else { // scrolling up
if menuView.frame.origin.y <= -menuHeight {
return
}
var translationY = -offset
if translationY < -menuHeight {
translationY = -menuHeight
}
menuView.transform = CGAffineTransform(translationX: 0, y: translationY)
}
}
如果用户向上滚动我的提要并且 header 不在屏幕上,我想将我的 table 视图 header 滑动到视图中。如果用户然后向下滚动我想再次隐藏它。
我相信我可以使用以下方法工作:
final class AccountSettingsController: UITableViewController {
let items: [Int] = Array(0...500)
private lazy var menuView: UIView = {
let view = UIView(frame: .zero)
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .systemPink
view.heightAnchor.constraint(equalToConstant: 120).isActive = true
view.widthAnchor.constraint(equalToConstant: 100).isActive = true
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
edgesForExtendedLayout = []
extendedLayoutIncludesOpaqueBars = false
tableView.tableHeaderViewWithAutolayout = menuView
tableView.tableFooterView = .init()
tableView.refreshControl = .init()
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell()
cell.textLabel?.text = "This is setting #\(indexPath.row)"
return cell
}
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
let contentOffset = scrollView.contentOffset.y
let transY = scrollView.panGestureRecognizer.translation(in: scrollView).y
if scrollView.contentOffset.y > 120 {
if transY > 0 {
menuView.transform = .init(translationX: 0, y: contentOffset)
} else if transY < 0 {
menuView.transform = .identity
}
return
}
if scrollView.contentOffset.y <= 120 {
if transY < 0 {
menuView.transform = .identity
} else if transY > 0 {
menuView.transform = .init(translationX: 0, y: contentOffset)
}
}
}
}
我想制作滑入/滑出 header 的动画,使其上下滑动,而不是仅仅出现。
我尝试添加 UIView.animate
个块,例如
if scrollView.contentOffset.y <= 120 {
if transY < 0 {
menuView.transform = .identity
} else if transY > 0 {
UIView.animate(withDuration: 0.25, animations: {
self.menuView.transform = .init(translationX: 0, y: contentOffset)
})
}
}
但这会在 header 上产生随机跳跃效果,根本达不到我想要的效果。
我附上了一张显示当前效果的 gif,您可以看到它只是出现和消失。我希望它可以上下动画以显示/隐藏。
我还有一个扩展程序可以为我的 table 视图 header 设置正确的大小,您可以在此处找到 -
extension UITableView {
var tableHeaderViewWithAutolayout: UIView? {
set (view) {
tableHeaderView = view
if let view = view {
lowerPriorities(view)
view.frame.size = view.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
tableHeaderView = view
}
}
get {
return tableHeaderView
}
}
fileprivate func lowerPriorities(_ view: UIView) {
for cons in view.constraints {
if cons.priority.rawValue == 1000 {
cons.priority = UILayoutPriority(rawValue: 999)
}
for v in view.subviews {
lowerPriorities(v)
}
}
}
}
我同意 Claudio 的观点,使用 tableHeaderView
可能会使事情复杂化,因为它并不是真正打算以这种方式进行操作。
试试下面的方法,看看是否能满足您的需求。
不是操纵 header 的偏移量,而是操纵高度。还是可以给人一种卷轴的感觉。
如果您想应用一个阈值,您也可以引入 scrollView.panGestureRecognizer.velocity(in: scrollView)
,以便菜单仅在特定的滚动速度后出现。
class MyTableViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
private let data: [Int] = Array(0...99)
private let menuView: UIView = {
let view = UIView(frame: .zero)
view.backgroundColor = .purple
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
private lazy var tableView: UITableView = {
let view = UITableView(frame: .zero)
view.translatesAutoresizingMaskIntoConstraints = false
view.dataSource = self
view.delegate = self
// view.refreshControl = .init()
view.contentInsetAdjustmentBehavior = .never
view.tableFooterView = .init()
return view
}()
var menuViewHeightAnchor: NSLayoutConstraint!
var tableViewTopAnchor: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
menuViewHeightAnchor = menuView.heightAnchor.constraint(equalToConstant: 120)
tableViewTopAnchor = tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 120)
edgesForExtendedLayout = []
[tableView, menuView].forEach(view.addSubview)
NSLayoutConstraint.activate([
menuViewHeightAnchor,
menuView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
menuView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
menuView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tableViewTopAnchor,
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
])
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell(frame: .zero)
cell.textLabel?.text = "Cell #\(indexPath.row)"
return cell
}
private var previousOffsetY: CGFloat = 0
private var velocityThreshold: CGFloat = 500
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let offsetY = min(120, max(0, scrollView.contentOffset.y))
let translation = scrollView.panGestureRecognizer.translation(in: scrollView)
let velocity = scrollView.panGestureRecognizer.velocity(in: scrollView)
let offsetYDiff = previousOffsetY - offsetY
previousOffsetY = offsetY
let adjustedOffset = menuViewHeightAnchor.constant + offsetYDiff
tableViewTopAnchor.constant = 120 - max(0, offsetY)
menuViewHeightAnchor.constant = min(120, adjustedOffset)
guard offsetY == 120 else { return }
if translation.y > 0 { // DRAGGED DOWN
UIView.animate(withDuration: 0.33, animations: {
self.menuViewHeightAnchor.constant = 120
self.view.layoutIfNeeded()
})
} else if translation.y < 0 { // DRAGGED UP
UIView.animate(withDuration: 0.33, animations: {
self.menuViewHeightAnchor.constant = 0
self.view.layoutIfNeeded()
})
}
}
}
我认为最好的方法是将 menuView
设置为 tableViewController
的子视图,例如 viewDidLoad
方法并在 [=16] 上添加一个插图=] 像这样:
let menuHeight: CGFloat = 120
var scrollStartingYPoint: CGFloat = 0
private lazy var menuView: UIView = {
let view = UIView(frame: .zero)
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .systemPink
view.heightAnchor.constraint(equalToConstant: menuHeight).isActive = true
view.widthAnchor.constraint(equalToConstant: 100).isActive = true
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
view.addSubview(menuView)
NSLayoutConstraint.activate([
menuView.leftAnchor.constraint(equalTo: view.leftAnchor),
menuView.topAnchor.constraint(equalTo: view.topAnchor)
])
edgesForExtendedLayout = []
extendedLayoutIncludesOpaqueBars = false
tableView.contentInset.top = menuHeight
}
然后你可以在show/hide菜单中添加滚动逻辑,我做的和你用scrollViewWillBeginDragging
方法存储初始滚动偏移的方法有点不同,然后确定滚动方向和偏移量:
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
scrollStartingYPoint = scrollView.contentOffset.y
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let tableYScrollOffset = scrollView.contentOffset.y
let offset = abs(tableYScrollOffset - scrollStartingYPoint)
if tableYScrollOffset < scrollStartingYPoint { // scrolling down
if menuView.frame.origin.y >= 0 {
return
}
var translationY = -menuHeight + offset
if translationY > 0 {
translationY = 0
}
menuView.transform = CGAffineTransform(translationX: 0, y: translationY)
} else { // scrolling up
if menuView.frame.origin.y <= -menuHeight {
return
}
var translationY = -offset
if translationY < -menuHeight {
translationY = -menuHeight
}
menuView.transform = CGAffineTransform(translationX: 0, y: translationY)
}
}
如果你想动画化菜单的呈现,那么你可以改变之前的代码并像这样使用你的动画代码:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let tableYScrollOffset = scrollView.contentOffset.y
let offset = abs(tableYScrollOffset - scrollStartingYPoint)
if tableYScrollOffset < scrollStartingYPoint { // scrolling down
UIView.animate(withDuration: 0.25, animations: {
self.menuView.transform = .identity
})
} else { // scrolling up
if menuView.frame.origin.y <= -menuHeight {
return
}
var translationY = -offset
if translationY < -menuHeight {
translationY = -menuHeight
}
menuView.transform = CGAffineTransform(translationX: 0, y: translationY)
}
}