代码之家  ›  专栏  ›  技术社区  ›  nibelungen

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

  •  2
  • nibelungen  · 技术社区  · 9 年前

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

    滚动视图是其他UIView的子视图。我申请了 CAShapeLayer 给每个人戴上面具 UIView 的。这样我就可以毫无问题地屏蔽滚动视图。

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

    我试过了;

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

    但结果是一样的。

    不幸的是,我无法发布屏幕截图,但下面是我用来为UIViews创建掩码的代码:

    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:带有事件: 方法返回nil。

    1 回复  |  直到 7 年前
        1
  •  1
  •   Stuart    9 年前

    我同意@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 提供了一种有用的命中测试方法,允许您指定是否应将某个特定点视为该视图的“内部”: pointInside(_:withEvent:) 。只需将以下覆盖添加到 形状滚动视图 :

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

    这只是说:“如果 point (转换为形状图层的坐标系)位于形状的 path ,认为它在视图中;否则,请将其置于视野之外。”


    如果隐藏自己的滚动视图不合适,您仍然可以通过使用 ShapedScrollContainerView: UIView 用一个 scrollView 所有物然后,如上所述将形状遮罩应用于容器,然后再次使用 内部点(_:带事件:) 以测试其是否应响应特定触摸点。