Как сделать UIScrollView прокручиваемым только тогда, когда касания находятся внутри пользовательской формы?

Я работаю над созданием приложения для коллажей изображений. И у меня будет несколько 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 все еще доступна для касания? маски изменяют только то, что вы можете видеть, а не область, к которой вы можете прикоснуться. Итак, я создал подкласс 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: возвращает ноль.


person nibelungen    schedule 08.08.2015    source источник
comment
Можете ли вы просто изменить форму прокрутки, чтобы она соответствовала пользовательской форме?   -  person Kendel    schedule 09.08.2015
comment
Вы имеете в виду, что я применяю маски к представлениям прокрутки вместо их суперпредставлений? Если я это сделаю, это приведет к тому, что содержимое представлений прокрутки будет перекрываться при прокрутке, а также добавить пустые места в содержимое.   -  person nibelungen    schedule 09.08.2015


Ответы (1)


Я бы согласился с @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 формы, считайте, что он находится внутри вида; в противном случае считайте его вне вида».


Если представление прокрутки, которое маскирует себя, не подходит, вы все равно можете применить этот метод, используя ShapedScrollContainerView: UIView со свойством scrollView. Затем примените маску формы к контейнеру, как указано выше, и снова используйте pointInside(_:withEvent:), чтобы проверить, должен ли он реагировать на определенные точки касания.

person Stuart    schedule 09.08.2015
comment
Идеальный! Создание подклассов прокрутки не дало того, чего я хотел, но я создал подкласс ShapedScrollContainerView: UIView для контейнеров прокрутки. Я даже могу одновременно получать сенсорные события из представлений прокрутки. Благодарю вас! - person nibelungen; 09.08.2015