我如何为这个 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)
    }
}