【问题标题】:Change touch receiver while scrolling a scroll view滚动滚动视图时更改触摸接收器
【发布时间】:2018-10-30 06:31:27
【问题描述】:

Apple 的地图应用程序或 Google 地图等应用程序使用可滚动的底页叠加层来呈现额外的内容。虽然重建这种行为并不难,但我很难实现一个重要功能:

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

这是我的意思的示例视频:

Example Video:

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


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

  1. 我在滚动视图的 scrollViewDidScroll(_:) 委托方法中跟踪内容滚动视图的 contentOffset。然后我做

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

    防止内容滚动视图滚动到其内容的顶部。相反,我将它滚动到滚动整个底部工作表的超级滚动视图的 y 距离传递。

    1. 一旦内容滚动视图滚动到其顶部,我找到了一种方法来将滚动(平移)手势识别器的接收器从内容滚动视图更改为超级滚动视图。

    2. 我处理超级滚动视图中的所有内容。它通过委托协议询问其内容视图控制器是否要处理触摸,并且只有当它不想(因为它的内容滚动视图已到达顶部)时,超级滚动视图才会自行滚动。


虽然我已经设法实现了第一个变体(您在视频中看到的),但我更喜欢使用方法 2 或 3。让控制底部工作表的视图控制器管理起来是一种更简洁的方法所有滚动逻辑,而不暴露其内部。

不幸的是,我还没有找到一种方法来以某种方式将平移手势分成两个组件(一个控制接收器滚动视图,一个控制另一个滚动视图)

关于如何实现这种行为的任何想法?

【问题讨论】:

  • touchesBegan touchesMoved 和 touchesEnded 都会被调用,这样您就可以跟踪第一次触摸的起点,如果 contentOffset 击中目标,则从 maxContentOffset 点开始移动 currentTouch 的距离。然后,如果以另一种方式移动,您可以检查发生了多少翻译,然后在允许滚动之前向另一种方式移动。我确实认为滚动视图永远不会失去触摸,因为滚动指示器一直存在。我也想知道这一点,我说的是对还是错。如果内容偏移是如何锁定的那么好?

标签: ios scroll uiscrollview uigesturerecognizer bottom-sheet


【解决方案1】:

我对这个问题非常感兴趣,我希望通过提供我将如何实现它,它不会扼杀可能显示如何真正绕过响应者的答案。我认为我在 cmets 中使用的技巧是跟踪触摸。我忘记了滚动视图是如何吞噬这些的,但你可以使用 UIPanGesture。看看这是否接近您正在寻找的内容。我遇到的唯一可能需要更多考虑的情况是使用滚动来关闭底部视图。大部分代码都设置为在视图中获得一个工作滚动视图。我认为属性动画可能是最好的让它可中断,甚至是我个人最喜欢的 Facebook Pop 动画。为了简单起见,我只使用了 UIView 动画。让我知道这是否能解决您正在寻找的问题。 代码如下,结果如下 。滚动视图保持可滚动且处于活动状态。我为帧设置动画,但更新约束也可以。

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
        }
    }

【讨论】:

    猜你喜欢
    • 2023-03-15
    • 2012-11-03
    • 1970-01-01
    • 2012-03-17
    • 2013-01-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-12-21
    相关资源
    最近更新 更多