当滚动一个滚动视图时更改触摸接收器

3

像苹果地图应用或谷歌地图这样的应用程序使用可滚动的底部表单覆盖来呈现其他内容。虽然这种行为不太难以重建,但我很难实现一个重要的功能:

当底部表单中嵌入了滚动视图时,用户可以将其向上滚动,但是 - 而不是在顶部反弹 - 底部表单开始向下滚动而不是表格视图。

以下是我所说的示例视频:

示例视频:

Bottom Sheet Example

这是一种不错的用户体验,因为滚动没有中断,而且作为用户,这就是我期望的:一旦内容滚动视图到达顶部,手势接收器会自动移交给超级滚动视图。


为了实现这种行为,我看到有三种不同的方法:

  1. I track the content scroll view's contentOffset in the scroll view's scrollViewDidScroll(_:) delegate method. Then I do

    if contentScrollView.contentOffset.y < 0 {
        contentScrollView.contentOffset.y = 0
    }
    

    to keep the content scroll view from scrolling above the top of its content. Instead, I pass the y distance that it would have scrolled to the super scroll view which scrolls the whole bottom sheet.

    1. I find a way to change the receiver of the scrolling (pan) gesture recognizer from the content scroll view to the super scroll view as soon as the content scroll view has scrolled to its top.

    2. I handle everything inside the super scroll view. It asks its content view controller through a delegate protocol if it wants to handle the touches and only if it doesn't (because its content scroll view has reached the top) the super scroll view scrolls by itself.


虽然我已经成功实现了第一种方法(您可以在视频中看到),但我强烈推荐使用第二种或第三种方法。这是一种更加简洁的方式,可以使控制底部表单的视图控制器管理所有滚动逻辑,而不会暴露其内部。

不幸的是,我还没有找到一种将 pan 手势分成两个组件的方法(一个用于控制接收器滚动视图,另一个用于控制另一个滚动视图)。

有关如何实现此类行为的任何想法吗?


touchesBegan、touchesMoved和touchesEnded都会被调用,这样你就可以追踪第一个触摸的起始点,如果contentOffset达到目标,则以当前触摸点到最大内容偏移点的距离开始移动。然后,如果向另一个方向移动,您可以检查已发生多少平移并在允许滚动之前向另一个方向移动。我认为scrollview从来没有失去触摸,因为滚动指示器始终存在。我也想知道这一点,以及我所说的是对还是错。如果是的话,内容偏移如何锁定得如此好? - agibson007
1个回答

0

我对这个问题非常感兴趣,希望通过提供我的实现方式,不会压制真正传递响应者的答案。我认为我在评论中提到的技巧是跟踪触摸事件。我忘记了滚动视图如何吞噬这些事件,但您可以使用UIPanGesture。看看这是否接近您要寻找的内容。我遇到的唯一一个可能需要更多思考的情况是使用滚动来关闭底部视图。大部分代码都是为了在视图中获取可工作的滚动视图。我认为属性动画可能是使其可中断的最佳方法,甚至是我个人最喜欢的Facebook Pop动画。为了保持简单,我只使用了UIView动画。如果这解决了您要寻找的问题,请告诉我。以下是代码,这里是结果sogif。滚动视图仍然可滚动和活动。我通过更新框架来进行动画处理,但更新约束也可以起作用。

import UIKit

    class ViewController: UIViewController{
        //setup
        var items : [Int] = []
        lazy var tableView : UITableView = {
            let tv = UITableView(frame: CGRect(x: 0, y: topViewHeight, width: self.view.frame.width, height: self.view.frame.height))
            tv.autoresizingMask = [.flexibleWidth,.flexibleHeight]
            tv.delegate = self
            tv.dataSource = self
            tv.layer.cornerRadius = 4
            return tv
        }()

        lazy var topView : UIView = {
            let v = UIView(frame: CGRect(x: 0, y: 0, width: self.view.frame.width, height: topViewHeight))
            v.backgroundColor = .green
            v.autoresizingMask = [.flexibleWidth,.flexibleHeight]
            return v
        }()

        let cellIdentifier = "ourCell"



        //for animation
        var isAnimating = false
        var lastOffset : CGPoint = .zero
        var startingTouch : CGPoint?
        let topViewHeight : CGFloat = 500
        var isShowing : Bool = false
        let maxCollapse : CGFloat = 50




        override func viewDidLoad() {
            super.viewDidLoad()

            for x in 0...100{
                items.append(x)
            }
            // Do any additional setup after loading the view, typically from a nib.
            self.view.addSubview(topView)
            self.view.addSubview(tableView)
            self.tableView.reloadData()

            let pan = UIPanGestureRecognizer(target: self, action: #selector(moveFunction(pan:)))
            pan.delegate = self
            self.view.addGestureRecognizer(pan)
        }

        @objc func moveFunction(pan:UIPanGestureRecognizer) {
            let point:CGPoint = pan.location(in: self.view)
            switch pan.state {
            case .began:
                startingTouch = point
                break
            case .changed:
                  processMove(touchPoint:point.y)
                break
            default:
                processEnding(currentPointY: point.y)
                break
            }
        }

    }

    extension ViewController : UITableViewDelegate,UITableViewDataSource {

        func numberOfSections(in tableView: UITableView) -> Int {
            return 1
        }

        func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            return items.count
        }

        func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            var cell : UITableViewCell!
            cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier)
            if cell == nil {
                cell = UITableViewCell(style: .default, reuseIdentifier: cellIdentifier)
            }
            cell.textLabel?.text = "\(items[indexPath.row])"
            return cell
        }

        func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
            return 30
        }
    }

    extension ViewController : UIScrollViewDelegate{
        func scrollViewDidScroll(_ scrollView: UIScrollView) {
            if isAnimating == true{
                scrollView.contentOffset = lastOffset
                return
            }
            lastOffset = scrollView.contentOffset
        }
    }

    extension ViewController : UIGestureRecognizerDelegate{
        func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
            return true
        }
    }

    extension ViewController{
        func processMove(touchPoint:CGFloat){
            if let start = startingTouch{
                if touchPoint <= topViewHeight && start.y > topViewHeight{
                    isAnimating = true
                    tableView.frame = CGRect(x: 0, y:touchPoint, width: self.view.frame.width, height: self.view.frame.height)
                    return
                }else if touchPoint >= self.maxCollapse && isShowing == true && start.y < self.maxCollapse{
                    isAnimating = true
                    tableView.frame = CGRect(x: 0, y:touchPoint, width: self.view.frame.width, height: self.view.frame.height)
                    return
                }else if isShowing == true && self.tableView.contentOffset.y <= 0{
                    //this is the only one i am slightly unsure about
                    isAnimating = true
                    tableView.frame = CGRect(x: 0, y:touchPoint, width: self.view.frame.width, height: self.view.frame.height)
                    return
                }
            }
            self.isAnimating = false
        }

        func processEnding(currentPointY:CGFloat){
            startingTouch = nil

            if isAnimating{
                if currentPointY < topViewHeight/2{
                    UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.9, initialSpringVelocity: 0.0, options: .curveEaseInOut, animations: {
                        self.tableView.frame = CGRect(x: 0, y:self.maxCollapse, width: self.view.frame.width, height: self.view.frame.height)
                    }) { (finished) in
                        self.isShowing = true
                    }
                }else{
                    UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.9, initialSpringVelocity: 0.0, options: .curveEaseInOut, animations: {
                        self.tableView.frame = CGRect(x: 0, y:self.topViewHeight, width: self.view.frame.width, height: self.view.frame.height)
                    }) { (finished) in
                        self.isShowing = false

                    }
                }
            }
            self.isAnimating = false
        }
    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接