Невозможно правильно определить положение для выполнения рендеринга пользовательского интерфейса, особенно левого и правого слоев руля.
Я знаю, это звучит глупо, и это базовая математика, но я борюсь с этой задачей уже несколько дней. Почему-то я не могу понять, как рассчитать положение для каждого слоя в иерархии.
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