Попытка создать слайдер диапазона, похожий на приложение GIPHY (iOS, GIF creator) - PullRequest
0 голосов
/ 17 мая 2019

Невозможно правильно определить положение для выполнения рендеринга пользовательского интерфейса, особенно левого и правого слоев руля.

Я знаю, это звучит глупо, и это базовая математика, но я борюсь с этой задачей уже несколько дней. Почему-то я не могу понять, как рассчитать положение для каждого слоя в иерархии.

import UIKit
import SnapKit
import AVFoundation
import Photos

class EditorSlider2: UIControl {

    /// Thumbnails generated form any av asset are passed
    /// from the interactor.
    var thumbnails: [UIImage] = [] {
        didSet {
            self.thumbnails.forEach { (image) in

                let view = UIImageView(image: image)
                view.translatesAutoresizingMaskIntoConstraints = false
                view.contentMode = .scaleAspectFill

                self.stackView.addArrangedSubview(view)
            }

            self.layoutSubviews()
        }
    }

    /// Time range struct, which is similar to the
    /// CMTimeRange which defines the lower and upper
    /// bounds of a duration.
    struct TimeRange {
        var start: CGFloat
        var duration: CGFloat
        static var zero: TimeRange {
            return TimeRange(start: 0.0, duration: 0.0)
        }
        static func from(_ timeRange: CMTimeRange) -> TimeRange {
            return TimeRange(start: CGFloat(timeRange.start.seconds), duration: CGFloat(timeRange.duration.seconds))
        }

        func coreMediaTimeRange() -> CMTimeRange {
            return CMTimeRange(
                start: CMTime(seconds: Double(self.start), preferredTimescale: CMTimeScale(100)),
                duration: CMTime(seconds: Double(self.duration), preferredTimescale: CMTimeScale(100)))
        }
    }

    ///
    var totalTimeRange: TimeRange = TimeRange.zero {
        didSet {
            self.updateLayerFrames()
        }
    }
    var selectedTimeRange: TimeRange = TimeRange.zero {
        didSet {
            self.updateLayerFrames()
        }
    }
    var cursorTime: CGFloat = CGFloat.zero {
        didSet {
            self.updateLayerFrames()
        }
    }

    var minDistanceTime: CGFloat = CGFloat.zero {
        didSet {
            self.updateLayerFrames()
        }
    }

    private var previousLocation = CGPoint()

    private var handleHeight: CGFloat {
        return self.bounds.height
    }

    lazy var scrollView: ScrollView = {

        let view = ScrollView()
        view.translatesAutoresizingMaskIntoConstraints = false
        view.editorSlider = self
        view.backgroundColor = UIColor.clear
        view.bounces = false
        view.delegate = self
        view.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)

        return view
    }()

    lazy var stackView: UIStackView = {

        let view = UIStackView()
        view.distribution = .equalSpacing
        view.axis = .horizontal

        return view
    }()

    final class ScrollView: UIScrollView {

        weak var editorSlider: EditorSlider2?

        override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {

            // determine if any of the handle layer where hit
            guard let slider = self.editorSlider else { return self }
            guard !slider.hitTestHandleElements(point, with: event) else {
                return nil
            }

            return super.hitTest(point, with: event)
        }
    }

    class HandleLayer: CALayer {

        var isHighlighted: Bool = false
        weak var slider: EditorSlider2?

        lazy var shapeLayer: CAShapeLayer = {

            let layer = CAShapeLayer()
            layer.backgroundColor = UIColor.clear.cgColor
            layer.fillColor = UIColor.lightGray.cgColor
            layer.fillRule = CAShapeLayerFillRule.evenOdd
            layer.lineCap = CAShapeLayerLineCap.round

            return layer
        }()

        lazy var leftHandleLayer: CALayer = {

            let layer = CALayer()
            layer.backgroundColor = UIColor.white.cgColor

            return layer
        }()

        lazy var rightHandleLayer: CALayer = {

            let layer = CALayer()
            layer.backgroundColor = UIColor.white.cgColor

            return layer
        }()

        override init() {
            super.init()

            self.backgroundColor = UIColor.clear.cgColor
            self.addSublayer(self.shapeLayer)
            self.shapeLayer.addSublayer(self.leftHandleLayer)
            self.shapeLayer.addSublayer(self.rightHandleLayer)
        }

        required init?(coder aDecoder: NSCoder) {
            fatalError("init(coder:) has not been implementesd")
        }

        override func layoutSublayers() {
            super.layoutSublayers()

            let inset = self.bounds.width / 4
            let insetBounds = self.bounds.inset(by: UIEdgeInsets(top: 0, left: inset, bottom: self.bounds.height * 0.3, right: inset))
            let path = UIBezierPath()
            path.move(to: CGPoint(x: insetBounds.maxX, y: insetBounds.minY))
            path.addLine(to: CGPoint(x: insetBounds.maxX, y: insetBounds.maxY * 0.75))
            path.addLine(to: CGPoint(x: insetBounds.midX, y: insetBounds.maxY))
            path.addLine(to: CGPoint(x: insetBounds.minX, y: insetBounds.maxY * 0.75))
            path.addLine(to: CGPoint(x: insetBounds.minX, y: insetBounds.minY))
            path.close()

            self.shapeLayer.frame = self.bounds
            self.shapeLayer.path = path.cgPath

            self.leftHandleLayer.frame = CGRect(x: insetBounds.midX - 6, y: insetBounds.minY + 5, width: 3, height:  insetBounds.height / 3)
            self.leftHandleLayer.needsDisplay()
            self.rightHandleLayer.frame = CGRect(x: insetBounds.midX + 3, y: insetBounds.minY + 5, width: 3, height:  insetBounds.height / 3)
            self.rightHandleLayer.needsDisplay()
        }
    }

    lazy var frameLayer: CALayer = {

        let layer = CALayer()
        layer.backgroundColor = UIColor.clear.cgColor
        layer.borderColor = UIColor.white.cgColor

        return layer
    }()

    lazy var leftHandleLayer: HandleLayer = {

        let layer = HandleLayer()
        layer.slider = self

        return layer
    }()

    lazy var rightHandleLayer: HandleLayer = {

        let layer = HandleLayer()
        layer.slider = self

        return layer
    }()

    lazy var cursorLayer: CALayer = {

        let layer = CALayer()
        layer.backgroundColor = UIColor.white.cgColor

        return layer
    }()

    override init(frame: CGRect) {
        super.init(frame: frame)

        self.isMultipleTouchEnabled = true
        self.isUserInteractionEnabled = true

        self.setupViews()

        self.layer.addSublayer(self.frameLayer)
        self.layer.addSublayer(self.cursorLayer)
        self.layer.addSublayer(self.leftHandleLayer)
        self.layer.addSublayer(self.rightHandleLayer)
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func layoutSubviews() {
        super.layoutSubviews()

        let height = self.scrollView.frame.height
        let stackViewFrame = CGRect(x: 0.0, y: 0.0, width: height * (self.totalTimeRange.duration / 2), height: self.scrollView.frame.height)
        self.stackView.frame = stackViewFrame
        self.scrollView.contentSize = stackViewFrame.size

        self.updateLayerFrames()
    }

    private func setupViews() {

        self.backgroundColor = UIColor.clear

        self.addSubview(self.scrollView)
        self.scrollView.snp.makeConstraints { (make) in
            let inset = (UIScreen.main.bounds.width / 6) / 2
            make.leading.equalTo(self.snp.leading).inset(inset)
            make.top.equalTo(self.snp.top).offset(5)
            make.width.equalTo(self.snp.width).inset(inset)
            make.height.equalTo(self.snp.height).inset(5)
        }

        self.scrollView.addSubview(self.stackView)
    }

    override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
        self.previousLocation = touch.location(in: self)

        if self.leftHandleLayer.frame.contains(self.previousLocation) {
            self.leftHandleLayer.isHighlighted = true
        } else if self.rightHandleLayer.frame.contains(self.previousLocation) {
            self.rightHandleLayer.isHighlighted = true
        }

        return self.leftHandleLayer.isHighlighted || self.rightHandleLayer.isHighlighted
    }

    override func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
        let location = touch.location(in: self)

        // 1. Determine by how much the user has dragged
        let deltaLocation = CGFloat(location.x - self.previousLocation.x)
        let deltaValue = // @todo: What is the delta value?

        self.previousLocation = location
        // 2. Update the values
        if self.leftHandleLayer.isHighlighted {

            // @todo: how do I calcluate the change for the left value?
        } else if self.rightHandleLayer.isHighlighted {

            // @todo: how do I calcluate the change for the right value?
        }

        self.updateLayerFrames()

        return true
    }

    override func endTracking(_ touch: UITouch?, with event: UIEvent?) {
        self.leftHandleLayer.isHighlighted = false
        self.rightHandleLayer.isHighlighted = false
    }

    /// Renders each sublayer to the screen.
    func updateLayerFrames() {

        let contentOffset = self.scrollView.contentOffset
        let contentSize = self.scrollView.contentSize
        let frame = self.scrollView.frame
        let minX = frame.minX

        let factor = (contentSize.width - contentOffset.x) / contentSize.width

        // makes sure that the layout was defined first.
        guard !factor.isNaN else { return }

        // using core animation transaction to disable
        // any animation.
        CATransaction.begin()
        CATransaction.setDisableActions(true)

        // @todo: How to I position the left, right layer correctly?

        self.leftHandleLayer.frame = CGRect.zero // @todo: Frame for the left handle layer
        self.leftHandleLayer.needsDisplay()


        // curser does indicate where the user video position is on the screen.
        let curserWidth: CGFloat = 3.0
        let curserCenter = self.size(for: self.cursorTime) - contentOffset.x + minX - curserWidth

        self.cursorLayer.frame = CGRect(x: curserCenter, y: self.scrollView.frame.minY, width: curserWidth, height: self.scrollView.frame.height)
        self.cursorLayer.needsDisplay()

        CATransaction.commit()
    }

    func size(for duration: CGFloat) -> CGFloat {
        return ((self.scrollView.contentSize.width / self.totalTimeRange.duration) * duration) * 2
    }

    /// Determines if one of the handles where hit.
    func hitTestHandleElements(_ point: CGPoint, with event: UIEvent?) -> Bool {
        return self.leftHandleLayer.frame.contains(point) || self.rightHandleLayer.frame.contains(point)
    }
}

extension EditorSlider2: UIScrollViewDelegate {
    func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
        self.cursorLayer.opacity = 0.0
    }

    func scrollViewDidScroll(_ scrollView: UIScrollView) {

        let seek = scrollView.contentOffset.x / scrollView.contentSize.width

        self.selectedTimeRange = TimeRange(start: seek * self.totalTimeRange.duration, duration: seek * self.totalTimeRange.duration + 6.0)
        self.updateLayerFrames()
    }

    func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
        self.cursorLayer.opacity = 1.0
    }
}

Я ожидаю, что смогу правильно отобразить каждый дескриптор на экране, и, если он был перемещен пользователем, я смогу обновить выбранный диапазон времени.

Я сделал скриншот для редактирования в приложении GIPHY.

GIPHY APP

Range slider

...