仅当触摸位于自定义形状内时,如何使 UIScrollView 可滚动?

How do I make a UIScrollView scrollable only when touches are inside a custom shape?

我正在开发一个图像拼贴应用程序。我将有多个 UIScrollView。滚动视图将具有自定义形状的边界,用户将能够动态更改形状相交的角。滚动视图有 UIImageView 作为子视图。

滚动视图是其他 UIView 的子视图。我为每个 UIView 应用了 CAShapeLayer 掩码。这样我就可以毫无问题地屏蔽滚动视图。

但问题是,我只能滚动最后添加的滚动视图的内容。此外,我可以平移和缩放到遮罩边界之外。当我触摸作为遮罩的多边形边界内部时,我应该只能平移或缩放。

我试过了;

scrollView.clipsToBounds = true
scrollView.layer.masksToBounds = true

但是结果是一样的

不幸的是,我无法 post 屏幕截图,但是,这是我用来为 UIView 创建遮罩的代码:

func createMask(v: UIView, viewsToMask: [UIView], anchorPoint: CGPoint)
{
    let frame = v.bounds
    var shapeLayer = [CAShapeLayer]()
    var path = [CGMutablePathRef]()

    for i in 0...3 {
        path.append(CGPathCreateMutable())
        shapeLayer.append(CAShapeLayer())
    }

    //define frame constants
    let center = CGPointMake(frame.origin.x + frame.size.width / 2, frame.origin.y + frame.size.height / 2)
    let bottomLeft = CGPointMake(frame.origin.x, frame.origin.y + frame.size.height)
    let bottomRight = CGPointMake(frame.origin.x + frame.size.width, frame.origin.y + frame.size.height)

    switch frameType {
    case 1:
        // First view for Frame Type 1
        CGPathMoveToPoint(path[0], nil, 0, 0)
        CGPathAddLineToPoint(path[0], nil, bottomLeft.x, bottomLeft.y)
        CGPathAddLineToPoint(path[0], nil, anchorPoint.x, bottomLeft.y)
        CGPathAddLineToPoint(path[0], nil, anchorPoint.x, anchorPoint.y)
        CGPathCloseSubpath(path[0])

        // Second view for Frame Type 1
        CGPathMoveToPoint(path[1], nil, anchorPoint.x, anchorPoint.y)
        CGPathAddLineToPoint(path[1], nil, anchorPoint.x, bottomLeft.y)
        CGPathAddLineToPoint(path[1], nil, bottomRight.x, bottomRight.y)
        CGPathAddLineToPoint(path[1], nil, bottomRight.x, anchorPoint.y)
        CGPathCloseSubpath(path[1])

        // Third view for Frame Type 1
        CGPathMoveToPoint(path[2], nil, 0, 0)
        CGPathAddLineToPoint(path[2], nil, anchorPoint.x, anchorPoint.y)
        CGPathAddLineToPoint(path[2], nil, bottomRight.x, anchorPoint.y)
        CGPathAddLineToPoint(path[2], nil, bottomRight.x, 0)
        CGPathCloseSubpath(path[2])

    default:
        break
    }

    for (key, view) in enumerate(viewsToMask) {
        shapeLayer[key].path = path[key]
        view.layer.mask = shapeLayer[key]
    }

}

那么,我怎样才能使滚动视图的行为方式使其仅在相应掩码边界内发生触摸时才滚动或缩放内容?

编辑:

根据这个问题的回答:UIView's masked-off area still touchable?蒙版只修改你能看到的,而不是你能触摸到的区域。所以我将 UIScrollView 子类化并尝试像这样覆盖 hitTest:withEvent: 方法,

protocol CoolScrollViewDelegate: class {
    var scrollViewPaths: [CGMutablePathRef] { get set }
}

class CoolScrollView: UIScrollView
{
    weak var coolDelegate: CoolScrollViewDelegate?

    override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView?
    {
        if CGPathContainsPoint(coolDelegate?.scrollViewPaths[tag], nil, point, true) {
            return self
        } else {
            return nil
        }
    }

}

但是通过这个实现,我只能在放大时检查最后的滚动视图和路径边界的变化。例如,如果我放大图像 hitTest:withEvent: 方法 returns nil .

我同意@Kendel 在评论中的观点——首先创建一个 UIScrollView 知道如何用特定形状掩盖自身的子类可能是一种更简单的方法。将形状逻辑保持在滚动视图子类中将使事情保持整洁,并允许您轻松地将触摸限制在形状内(我稍后会谈到)。

从您的描述中很难确切地说出您的成形视图应该如何表现,但作为一个简短的例子,您的 ShapedScrollView 可能看起来像这样:

import UIKit

class ShapedScrollView: UIScrollView {

    // MARK: Types

    enum Shape {
        case First  // Choose a better name!
    }

    // MARK: Properties

    private let shapeLayer = CAShapeLayer()

    var shape: Shape = .First {
        didSet { setNeedsLayout() }
    }

    // MARK: Initializers

    init(frame: CGRect, shape: Shape = .First) {
        self.shape = shape
        super.init(frame: frame)
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }

    // MARK: Layout

    override func layoutSubviews() {
        super.layoutSubviews()
        updateShape()
    }

    // MARK: Updating the Shape

    private func updateShape() {

        // Disable core animation actions to prevent changes to the shape layer animating implicitly
        CATransaction.begin()
        CATransaction.setDisableActions(true)

        if bounds.size != shapeLayer.bounds.size {
            // Bounds size has changed, completely update the shape
            shapeLayer.frame = CGRect(origin: contentOffset, size: bounds.size)
            shapeLayer.path = pathForShape(shape).CGPath
            layer.mask = shapeLayer

        } else {
            // Bounds size has NOT changed, just update origin of shape path to
            // match content offset - makes it appear stationary as we scroll
            var shapeFrame = shapeLayer.frame
            shapeFrame.origin = contentOffset
            shapeLayer.frame = shapeFrame
        }

        CATransaction.commit()
    }

    private func pathForShape(shape: Shape) -> UIBezierPath {
        let path = UIBezierPath()

        switch shape {
        case .First:
            // Build the shape path, whatever that might be...
            // path.moveToPoint(...)
            // ...
        }

        return path
    }
}

所以让触摸只在指定的形状内起作用是比较容易的部分。我们已经有一个形状层的引用,它描述了我们想要限制触摸的形状。 UIView 提供了一种有用的命中测试方法,可让您指定特定点是否应被视为 "inside" 该视图:pointInside(_:withEvent:)。只需将以下覆盖添加到 ShapedScrollView:

override func pointInside(point: CGPoint, withEvent event: UIEvent?) -> Bool {
    return CGPathContainsPoint(shapeLayer.path, nil, layer.convertPoint(point, toLayer: shapeLayer), false)
}

这只是说:"If point (converted to the shape layer's coordinate system) is inside the shape's path, consider it to be inside the view; otherwise consider it outside the view."


如果屏蔽自身的滚动视图不合适,您仍然可以通过使用 ShapedScrollContainerView: UIViewscrollView 属性 来采用此技术。然后,如上所述将形状遮罩应用于容器,并再次使用 pointInside(_:withEvent:) 测试它是否应该响应特定的触摸点。