基于平移手势翻译的滚动scrollview

Posted

技术标签:

【中文标题】基于平移手势翻译的滚动scrollview【英文标题】:Scrolling scrollview based on pan gesture translation 【发布时间】:2020-09-24 18:44:24 【问题描述】:

我想知道如何将平移手势 y 翻译转换为相当于滚动视图内容 y 偏移量。

因此,在此我可以通过平移手势翻译滚动我的滚动视图,再现自然滚动视图滚动的相同效果。

let scrollYOffset: CGFloat = panGesture.translation(in: view).y

当我这样做时,它导致滚动非常不稳定,因为 scrollYOffset 值从 0 变为 54,然后在拖动时变为 124。

我想要的是使用平移手势平滑自然地滚动滚动视图。

你可能会问我为什么不使用自然滚动视图及其委托 -

func scrollViewDidScroll(_ scrollView: UIScrollView)

答案是我正在尝试构建一个应用程序,用户可以在其中拖动屏幕上的对象,并且它后面的滚动视图应该滚动那么多。

谁能帮我将平移手势 y 翻译转换为滚动视图 y 内容偏移量。

任何帮助将不胜感激。谢谢。

【问题讨论】:

不太清楚您要做什么...例如,您是否有水平滚动视图,并且当您左右拖动对象时,您希望滚动视图内容滚动?那么,将对象一直向左拖动,您会看到滚动内容的开头......一直向右拖动并看到滚动内容的结尾? 是的,这正是我们所需要的 【参考方案1】:

方法translation: 返回前一个迭代和当前迭代之间的差异,因此您应该将翻译添加到现有结果中,如下所示。

     @objc private func handleGesture(panGestureRecognizer: UIPanGestureRecognizer) 
         switch panGestureRecognizer.state 
         case .begin:
            break
         case .changed:
            let translation = panGestureRecognizer.translation(in: self.view)
            panGestureRecognizer.setTranslation(.zero, in: self.view)

            let newYOffset = currentOffset + translation.y
         case .ended:
            break
         
     

【讨论】:

有趣的解决方案,谢谢。但是在这种情况下滚动弹跳不起作用。【参考方案2】:

有多种方法可以获得“平滑的可拖动视图” --- 但是,为了您的目的,一个可能的选择是使用UISlider

如果需要,您可以通过设置拇指和轨迹图像来更改滑块的外观。

示例代码(用于该屏幕截图):

class SliderScrollViewController: UIViewController, UIScrollViewDelegate 
    
    let scrollView: UIScrollView = UIScrollView()
    let slider: UISlider = UISlider()
    
    override func viewDidLoad() 
        super.viewDidLoad()
        
        [scrollView, slider].forEach 
            view.addSubview($0)
            $0.translatesAutoresizingMaskIntoConstraints = false
        

        // horizontal stack view to hold scroll content
        let stack = UIStackView()
        stack.distribution = .fillEqually
        stack.spacing = 8
        stack.translatesAutoresizingMaskIntoConstraints = false

        // add 20 views to the stack view for horizontal scrolling
        for i in 1...20 
            let v = UILabel()
            v.textAlignment = .center
            v.backgroundColor = .yellow
            v.text = "View \(i)"
            stack.addArrangedSubview(v)
        
        stack.arrangedSubviews.first?.widthAnchor.constraint(equalToConstant: 120).isActive = true
        scrollView.addSubview(stack)
        
        let g = view.safeAreaLayoutGuide
        let svCLG = scrollView.contentLayoutGuide
        let svFLG = scrollView.frameLayoutGuide
        
        NSLayoutConstraint.activate([

            scrollView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
            scrollView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
            scrollView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
            scrollView.heightAnchor.constraint(equalToConstant: 120.0),
            
            slider.topAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: 20.0),
            slider.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
            slider.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
            
            stack.topAnchor.constraint(equalTo: svCLG.topAnchor, constant: 8.0),
            stack.leadingAnchor.constraint(equalTo: svCLG.leadingAnchor, constant: 8.0),
            stack.trailingAnchor.constraint(equalTo: svCLG.trailingAnchor, constant: -8.0),
            stack.bottomAnchor.constraint(equalTo: svCLG.bottomAnchor, constant: 8.0),

            stack.heightAnchor.constraint(equalTo: svFLG.heightAnchor, constant: -16.0),
            
        ])
        
        slider.addTarget(self, action: #selector(self.sliderChanged(_:)), for: .valueChanged)
        
        // so we can see the frame of the scroll view
        scrollView.backgroundColor = .green
        
        // set scrollView delegate
        scrollView.delegate = self
    
    
    func scrollViewDidScroll(_ scrollView: UIScrollView) 
        // we need to update the slider position when scroll view is manually scrolled
        let w = scrollView.contentSize.width - scrollView.frame.size.width
        let pct = scrollView.contentOffset.x / w
        slider.value = Float(pct)
    

    @objc func sliderChanged(_ sender: UISlider) -> Void 
        // set scroll view's content offset x based on position of slider
        let w = scrollView.contentSize.width - scrollView.frame.size.width
        let x = w * CGFloat(sender.value)
        scrollView.contentOffset.x = x
    
    


编辑

我想我会玩这个多一点...这个例子抓取滚动视图的内容为UIImage,将它用于滑块后面的“背景”图像,并为滑块创建一个轮廓图像拇指:

完整代码:

class SliderScrollViewController: UIViewController, UIScrollViewDelegate 
    
    let scrollView: UIScrollView = UIScrollView()
    let scrollContentView: UIView = UIView()
    let slider: UISlider = UISlider()
    let sliderBKG: UIImageView = UIImageView()
    
    override func viewDidLoad() 
        super.viewDidLoad()
        
        [scrollView, sliderBKG, slider].forEach 
            view.addSubview($0)
            $0.translatesAutoresizingMaskIntoConstraints = false
        
        
        // horizontal stack view to hold scroll content
        let stack = UIStackView()
        stack.distribution = .fillEqually
        stack.spacing = 8
        stack.translatesAutoresizingMaskIntoConstraints = false
        
        // add 20 views to the stack view for horizontal scrolling
        for i in 1...20 
            let v = UILabel()
            v.textAlignment = .center
            v.backgroundColor = UIColor(red: 0.0, green: 1.0, blue: 1.0 - (CGFloat(i) * 0.05), alpha: 1.0)
            v.text = "View \(i)"
            stack.addArrangedSubview(v)
        
        stack.arrangedSubviews.first?.widthAnchor.constraint(equalToConstant: 120).isActive = true
        
        scrollContentView.translatesAutoresizingMaskIntoConstraints = false
        scrollContentView.addSubview(stack)
        scrollView.addSubview(scrollContentView)
        
        let g = view.safeAreaLayoutGuide
        let svCLG = scrollView.contentLayoutGuide
        let svFLG = scrollView.frameLayoutGuide
        
        NSLayoutConstraint.activate([
            
            scrollView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
            scrollView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
            scrollView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
            scrollView.heightAnchor.constraint(equalToConstant: 120.0),
            
            slider.topAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: 20.0),
            slider.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
            slider.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
            slider.heightAnchor.constraint(equalToConstant: 40.0),
            
            sliderBKG.topAnchor.constraint(equalTo: slider.topAnchor, constant: 6.0),
            sliderBKG.leadingAnchor.constraint(equalTo: slider.leadingAnchor, constant: 0.0),
            sliderBKG.trailingAnchor.constraint(equalTo: slider.trailingAnchor, constant: 0.0),
            sliderBKG.bottomAnchor.constraint(equalTo: slider.bottomAnchor, constant: -6.0),
            
            scrollContentView.topAnchor.constraint(equalTo: svCLG.topAnchor, constant: 0.0),
            scrollContentView.leadingAnchor.constraint(equalTo: svCLG.leadingAnchor, constant: 0.0),
            scrollContentView.trailingAnchor.constraint(equalTo: svCLG.trailingAnchor, constant: 0.0),
            scrollContentView.bottomAnchor.constraint(equalTo: svCLG.bottomAnchor, constant: 0.0),
            
            scrollContentView.heightAnchor.constraint(equalTo: svFLG.heightAnchor, constant: 0.0),
            
            stack.topAnchor.constraint(equalTo: scrollContentView.topAnchor, constant: 8.0),
            stack.leadingAnchor.constraint(equalTo: scrollContentView.leadingAnchor, constant: 8.0),
            stack.trailingAnchor.constraint(equalTo: scrollContentView.trailingAnchor, constant: -8.0),
            stack.bottomAnchor.constraint(equalTo: scrollContentView.bottomAnchor, constant: -8.0),
            
        ])
        
        slider.addTarget(self, action: #selector(self.sliderChanged(_:)), for: .valueChanged)
        
        // set slider images to clear images
        slider.setThumbImage(UIImage(), for: [])
        slider.setMinimumTrackImage(UIImage(), for: [])
        slider.setMaximumTrackImage(UIImage(), for: [])
        
        // so we can see the frame of the scroll view
        scrollView.backgroundColor = .green
        scrollContentView.backgroundColor = .orange
        
        // set scrollView delegate
        scrollView.delegate = self
        
    
    
    override func viewDidAppear(_ animated: Bool) 
        super.viewDidAppear(animated)
        // after view has appeared (so we know auto-layout has finished)
        // grab the content of the scroll view as a UIImage
        let img = scvSnapshot()
        // set it to the "background" image view behind the slider
        sliderBKG.image = img
        updateThumbImage()
    
    override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) 
        super.viewWillTransition(to: size, with: coordinator)
        coordinator.animate(alongsideTransition:  _ in
        , completion: 
            _ in
            self.updateThumbImage()
        )
    
    func updateThumbImage() -> Void 
        // generate a proportional width bordered clear image
        let w = self.scrollView.frame.width * (self.scrollView.frame.width / self.scrollView.contentSize.width)
        let rect = CGRect(origin: .zero, size: CGSize(width: w, height: 40.0))
        let renderer = UIGraphicsImageRenderer(size: rect.size)
        let img = renderer.image  rc in
            rc.cgContext.setFillColor(UIColor.clear.cgColor)
            rc.cgContext.setStrokeColor(UIColor.red.cgColor)
            rc.cgContext.setLineWidth(2)
            rc.cgContext.addRect(rect)
            rc.cgContext.drawPath(using: .fillStroke)
        
        // set the slider's custom thumb image
        self.slider.setThumbImage(img, for: [])
    
    func scvSnapshot() -> UIImage 
        let renderer = UIGraphicsImageRenderer(bounds: scrollContentView.bounds)
        return renderer.image  rendererContext in
            scrollContentView.layer.render(in: rendererContext.cgContext)
        
    
    
    func scrollViewDidScroll(_ scrollView: UIScrollView) 
        // we need to update the slider position when scroll view is manually scrolled
        let w = scrollView.contentSize.width - scrollView.frame.size.width
        let pct = scrollView.contentOffset.x / w
        slider.value = Float(pct)
    
    
    @objc func sliderChanged(_ sender: UISlider) -> Void 
        // set scroll view's content offset x based on position of slider
        let w = scrollView.contentSize.width - scrollView.frame.size.width
        let x = w * CGFloat(sender.value)
        scrollView.contentOffset.x = x
    
    

【讨论】:

以上是关于基于平移手势翻译的滚动scrollview的主要内容,如果未能解决你的问题,请参考以下文章

是否可以禁用滑动手势并仅使平移手势在滚动视图中工作?

滚动视图平移防止捏手势?

带有平移手势冲突的滚动视图和侧边菜单

当翻译显示在标签中时,查看平移手势停止 - Xcode

如何将平移手势从滚动视图转发到另一个滚动视图

禁用 uicollectionview 水平滚动的外部平移手势