三个元素相互叠加的滑出动画

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. 我最初尝试使用简单的动画,例如根据滚动偏移更改每个 1/2/3 元素的高度限制,虽然对我来说这很好,但对审阅者来说却不是,因为他想要在隐藏元素动画之间更精确地滚动

  2. 然后我尝试将 panGesture 与滚动结合起来。我将 1 & 2 和 tableView 嵌入到一个滚动视图中,并使用委托函数 shouldRecognizeSimultaneouslyWith 为它设置 panGesture 识别器,在禁用 tableView 滚动时返回 true。然后,在相交 3 时,我禁用了 panGesture 并尝试启用 tableView 滚动,但无法识别哪个 panGesture/scrolling 有效或无效,哪个要禁用,哪个失败或有效 alone/simultaneously.

亲爱的开发人员,您将如何解决这个问题,使动画流畅并如描述的那样?也许,你有一个很棒的主意:)

我使用的是你的想法的简化版本,有一个 UIScrollView & 3 UILabel 个实例。

您可以轻松地将其调整为 UITableView & 3 UIView 个实例。

想法

  1. UIScrollView & 3 UILabel 个实例有一个共同的超级视图。在本例中是 UIViewController.view.
  2. UIScrollView 布局为全屏(边到边)并延伸到这 3 个 UILabel 实例下方。
  3. UIScrollViewcontentInset.top = height_of_three_labels 所以它的内容从这些其他实例下面开始。
  4. 每当 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
    }
}

这是开始的样子:

然后,向上滚动一点(23 下方滑动):

再滚动一点(13 推到视图之外):

然后 table 滚动,同时 1 保持在顶部: