【问题标题】:How do I make a UIScrollView scrollable only when touches are inside a custom shape?仅当触摸在自定义形状内时,如何使 UIScrollView 可滚动?
【发布时间】:2015-08-08 22:00:13
【问题描述】:

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

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

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

我试过了;

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

但结果是一样的。

很遗憾,我无法发布屏幕截图,但这里是我用来为 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: 方法返回 nil。

【问题讨论】:

  • 你能改变滚动视图的形状以匹配自定义形状吗?
  • 你的意思是我将掩码应用于滚动视图而不是它们的超级视图?如果我这样做,它会导致滚动视图的内容在我滚动时重叠,并且还会在内容中添加空格。

标签: swift uiscrollview mask cashapelayer


【解决方案1】:

我同意 cmets 中的 @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:)。只需将以下覆盖添加到ShapedScrollView

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

这只是说:“如果point(转换为形状图层的坐标系)在形状的path内,则认为它在视图内;否则认为它在视图外。”


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

【讨论】:

  • 完美!子类化滚动视图并没有达到我想要的效果,但我为滚动视图的容器创建了子类ShapedScrollContainerView: UIView。我什至可以同时从滚动视图接收触摸事件。谢谢!
猜你喜欢
  • 1970-01-01
  • 2013-06-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-01-08
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多