【问题标题】:How to make UIScrollView zoom in only one direction when using auto layout使用自动布局时如何使 UIScrollView 仅向一个方向缩放
【发布时间】:2016-05-16 00:57:10
【问题描述】:

我正在尝试使 UIScrollView 只允许在水平方向上缩放。滚动视图是使用纯自动布局方法设置的。通常的方法是在许多堆栈溢出答案(例如this one)和other 资源中所建议的。在自动布局存在之前,我已经在应用程序中成功使用了它。做法如下:在滚动视图的内容视图中,我覆盖setTransform(),并将传入的transform修改为只在x方向缩放:

override var transform: CGAffineTransform {
    get { return super.transform }
    set {
        var t = newValue
        t.d = 1.0
        super.transform = t
    }
}

我还重置了滚动视图的内容偏移量,使其在缩放期间不会垂直滚动:

func scrollViewDidZoom(scrollView: UIScrollView) {
    scrollView.contentSize = CGSize(width: scrollView.contentSize.width, height: scrollView.frame.height)
    scrollView.contentOffset = CGPoint(x: scrollView.contentOffset.x, y: 0.0)
}

这在不使用自动布局时效果很好:

但是,当使用自动布局时,内容偏移在缩放过程中最终会出错。虽然内容视图仅在水平方向缩放,但它会垂直移动:

我放了一个示例项目on GitHub(用于制作此问题中的视频)。它包含两个故事板,Main.storyboard 和 NoAutolayout.storyboard,除了 Main.storyboard 启用了自动布局而 NoAutolayout 没有之外,它们是相同的。通过更改主界面项目设置在它们之间切换,您可以看到两种情况下的行为。

我真的不想关闭自动布局,因为它以比手动布局所需的更好的方式解决了许多实现 UI 的问题。有没有办法在使用自动布局进行缩放时保持垂直内容偏移正确(即零)?

编辑:我在示例项目中添加了第三个故事板 AutolayoutVariableColumns.storyboard。这会添加一个文本字段来更改 GridView 中的列数,从而更改其固有内容大小。这更接近地显示了我在提示此问题的真实应用中需要的行为。

【问题讨论】:

    标签: ios swift uiscrollview autolayout zooming


    【解决方案1】:

    我不确定这是否满足您对 100% 自动布局的要求,但这是一种解决方案,其中 UIScrollView 设置为自动布局,gridView 添加为子视图。

    您的ViewController.swift 应如下所示:

    // Add an outlet for the scrollView in interface builder.
    @IBOutlet weak var scrollView: UIScrollView!
    
    // Remove the outlet and view for gridView.
    //@IBOutlet var gridView: GridView!
    
    // Create the gridView here:
    var gridView: GridView!
    
    // Setup some views
    override func viewDidLoad() {
        super.viewDidLoad()
        // The width for the gridView is set to 1000 here, change if needed.
        gridView = GridView(frame: CGRect(x: 0, y: 0, width: 1000, height: self.view.bounds.height))
        scrollView.addSubview(gridView)
        scrollView.contentSize = CGSize(width: gridView.frame.width, height: scrollView.frame.height)
        gridView.contentMode = .ScaleAspectFill
    }
    
    func viewForZoomingInScrollView(scrollView: UIScrollView) -> UIView? {
        return gridView
    }
    
    func scrollViewDidZoom(scrollView: UIScrollView) {
        scrollView.contentOffset = CGPoint(x: scrollView.contentOffset.x, y: 0.0)
        scrollView.contentSize = CGSize(width: scrollView.contentSize.width, height: scrollView.frame.height)
    }
    

    【讨论】:

    • 感谢您的精彩回答。不幸的是,我想使用自动布局的全部原因是“GridView”(实际上不像我的示例项目中的 GridView 那么简单)的大小会根据其内容而变化。 Autolayout 对intrinsicContentSize() 的支持非常适合这一点。没有它,视图必须随着内容的变化而手动调整大小,这是相当痛苦的。我实际上并不介意关闭 滚动视图 的自动布局,只要它的内容视图可以通过其固有内容大小调整大小。
    • 啊,明白了。如果我想办法在 gridView 上使用自动布局,我会告诉你的!
    【解决方案2】:

    尝试设置translatesAutoresizingMaskIntoConstraints

    func scrollViewDidZoom(scrollView: UIScrollView) {
        gridView.translatesAutoresizingMaskIntoConstraints = true
    }
    

    在示例项目中它可以工作。如果这还不够,请尝试在 scrollViewDidEndZooming: 中以编程方式创建新约束可能会有所帮助。

    另外,如果这没有帮助,请更新示例项目,以便我们可以使用变量 intrinsicContentSize() 重现问题

    Ole Begemann 的这篇文章对我帮助很大
    How I Learned to Stop Worrying and Love Cocoa Auto Layout

    WWDC 2015 视频
    Mysteries of Auto Layout, Part 1
    Mysteries of Auto Layout, Part 2

    幸运的是,有一个标志。它被称为 translatesAutoResizingMask IntoConstraints [无空格]。

    这有点拗口,但它几乎做到了它所说的。它使视图的行为方式与它们在 Legacy Layout 系统下但在 Auto Layout 世界中的行为方式相同。

    【讨论】:

    • 感谢您的回答。我不完全理解为什么会这样,但我认为它本质上是一个脆弱的黑客。如果您在viewDidLoad() 中打开translatesAutoresizingMaskIntoConstraints,您会立即得到一个冲突约束异常。在任何情况下,translatesAutoresizingMaskIntoConstraints 并不打算用于直接参与自动布局的视图,因为这里有 GridView。我将使用变量 intrinsicContentSize() 更新示例项目。
    • 我已经用故事板更新了示例项目,它允许更改网格视图中的列数,这会影响其内在内容大小。 (我还将项目移至 GitHub)。请注意,进行此更改后,您的解决方案将失效。如果缩放,然后更改列数,从自动调整大小掩码转换的宽度约束会导致 GridView 不调整大小以适应新的列数。
    • @Anna 的回答比尝试在旧版和自动布局系统之间切换要好得多。
    【解决方案3】:

    我想我明白了。您需要在转换中应用平移以抵消 UIScrollView 在缩放时尝试做的居中。

    将此添加到您的 GridView:

    var unzoomedViewHeight: CGFloat?
    override func layoutSubviews() {
        super.layoutSubviews()
        unzoomedViewHeight = frame.size.height
    }
    
    override var transform: CGAffineTransform {
        get { return super.transform }
        set {
            if let unzoomedViewHeight = unzoomedViewHeight {
                var t = newValue
                t.d = 1.0
                t.ty = (1.0 - t.a) * unzoomedViewHeight/2
                super.transform = t
            }
        }
    }
    

    要计算变换,我们需要知道视图的未缩放高度。在这里,我只是在 layoutSubviews() 期间抓取帧大小并假设它包含未缩放的高度。可能存在一些不正确的极端情况;在视图更新周期中计算高度可能是一个更好的地方,但这是基本思想。

    【讨论】:

    • 感谢 Anna,这是一个有趣的方法,而且看起来效果不错!我会试一试,然后报告。
    • 快速测试似乎表明这将起作用。明天我会尝试将它集成到我的实际项目中。再次感谢!
    • 当然!如果您对其进行了调整,请告诉我-我正在尝试解决相同的问题。 (并确保你接受我的回答 ;-)
    • 嗨,安娜,我无法让它在相反 (x) 维度上工作。我正在让它工作(基本上将 t.d 更改为 t.a),但是当我尝试进行第二次缩放时,它会在缩放之前跳回原始缩放级别。我猜它与我不完全理解的“t.y”有关,尽管我一直在尝试设置我的“t.x”。有任何想法吗。 (对不起,我认为我不能将我的代码作为评论发布..)
    • 我看到这个人有相同(或非常相似)的问题:stackoverflow.com/questions/7929237/…
    【解决方案4】:

    虽然@Anna 的解决方案有效,但UIScrollView 提供了一种使用自动布局 的方式。但由于滚动视图的工作方式与其他视图略有不同,因此对约束的解释也不同:

    • 滚动视图的边缘/边距与其内容之间的约束附加到滚动视图的内容区域
    • heightwidthcenters 之间的约束附加到滚动视图的 frame
    • 滚动视图和滚动视图之外的视图之间的约束就像普通视图一样工作。

    因此,当您将子视图添加到固定到其边缘/边缘的滚动视图时,该子视图将成为滚动视图的内容区域或内容视图

    Apple 在Working with Scroll Views 中建议采用以下方法:

    1. 将滚动视图添加到场景中。
    2. 像往常一样绘制约束来定义滚动视图的大小和位置。
    3. 向滚动视图添加视图。将视图的 Xcode 特定标签设置为 Content View。
    4. 将内容视图的顶部、底部、前导和后沿固定到滚动视图的相应边缘。内容视图现在定义 滚动视图的内容区域。
    5. (...)
    6. (...)
    7. 在内容视图中布置滚动视图的内容。使用约束在内容视图中正常定位内容。

    在您的情况下,GridView 必须在 content view 内:

    您可以保留您一直在使用但现在附加到内容视图的网格视图约束。对于仅水平缩放,请保持代码原样。您的 transform 覆盖处理得很好。

    【讨论】:

    • 我明白这一切。问题是关于仅水平 zooming 与仅水平滚动完全不同的野兽。在激发这个问题的真实应用程序中,滚动 应该在两个方向上工作,并且内容视图的高度是可变的。只有缩放应该限制在水平方向。
    • 没问题。水平滚动只是一个猜测。关于水平缩放,您已经解决了。剩下的就是通过自动布局解决水平缩放问题,使用该解决方案,您可以在没有 Anna 的情况下做到这一点。我测试过,效果很好。如果有不清楚的地方,请告诉我,我会编辑帖子。
    • Anna 的解决方案非常适合使用纯自动布局方法进行水平缩放。不过,如果您有不同的方法,我很想听听。您在这里的回答没有提到缩放。
    猜你喜欢
    • 2012-12-11
    • 2013-11-07
    • 2013-01-06
    • 2014-11-27
    • 2011-07-19
    • 2013-01-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多