16 changed files with 84 additions and 2553 deletions
-
52kplayer.xcodeproj/project.pbxproj
-
68kplayer/core/DatabaseManager.swift
-
2kplayer/core/MediaItem.swift
-
1kplayer/core/MediaModel.swift
-
4kplayer/detail/DetailViewController.swift
-
15kplayer/master/MasterViewController.swift
-
126kplayer/timeline/CenterLine.swift
-
451kplayer/timeline/FrameImagesView.swift
-
206kplayer/timeline/TimelineMeasure.swift
-
178kplayer/timeline/TimelineScroller.swift
-
460kplayer/timeline/TimelineView.swift
-
773kplayer/timeline/TrimView.swift
-
301kplayer/timeline/VideoTimelineView.swift
-
0kplayer/video/SVideoModel.swift
-
0kplayer/video/SVideoPlayer.swift
-
0kplayer/video/VideoPlayerView.swift
@ -1,126 +0,0 @@ |
|||
// |
|||
// CenterLine.swift |
|||
// Examplay |
|||
// |
|||
// Created by Tomohiro Yamashita on 2020/03/09. |
|||
// Copyright © 2020 Tom. All rights reserved. |
|||
// |
|||
|
|||
import UIKit |
|||
import AVFoundation |
|||
|
|||
class CenterLine: UIView { |
|||
|
|||
var mainView:VideoTimelineView! |
|||
|
|||
let timeLabel = UILabel() |
|||
var parentView:TimelineView? = nil |
|||
var duration:Float64 = 0 |
|||
var currentTime:Float64 = 0 |
|||
let margin:CGFloat = 6 |
|||
|
|||
override init (frame: CGRect) { |
|||
super.init(frame: frame) |
|||
|
|||
self.backgroundColor = .clear |
|||
self.isUserInteractionEnabled = false |
|||
} |
|||
|
|||
required init(coder aDecoder: NSCoder) { |
|||
fatalError("CenterLine init(coder:) has not been implemented") |
|||
} |
|||
|
|||
func configure(parent:TimelineView) { |
|||
parentView = parent |
|||
self.setNeedsDisplay() |
|||
|
|||
self.timeLabel.adjustsFontSizeToFitWidth = true |
|||
self.timeLabel.textAlignment = .center |
|||
|
|||
self.timeLabel.text = String("00:00.00") |
|||
|
|||
|
|||
self.addSubview(timeLabel) |
|||
} |
|||
|
|||
func update() { |
|||
self.setNeedsDisplay() |
|||
let textMargin = margin + 3 |
|||
self.timeLabel.frame.size.width = self.bounds.size.width - textMargin * 2 |
|||
self.timeLabel.frame.origin = CGPoint(x:textMargin,y:0) |
|||
self.timeLabel.textColor = UIColor(hue: 0.94, saturation:0.68, brightness:0.95, alpha: 0.94) |
|||
setTimeText() |
|||
timeLabel.font = UIFont(name:"HelveticaNeue-CondensedBold" ,size:timeLabel.frame.size.height * 0.9) |
|||
|
|||
} |
|||
|
|||
|
|||
override func draw(_ rect: CGRect) { |
|||
|
|||
|
|||
gradient() |
|||
|
|||
let context = UIGraphicsGetCurrentContext()! |
|||
context.saveGState() |
|||
context.setShadow(offset: CGSize(width: 0,height: 0), blur: 6, color: UIColor(hue: 0, saturation:0, brightness:0.0, alpha: 0.30).cgColor) |
|||
|
|||
UIColor(hue: 0, saturation:0, brightness:1, alpha: 0.92).setFill() |
|||
|
|||
let labelRect = CGRect(x: margin,y: 0.3,width: self.frame.size.width - margin * 2,height: timeLabel.frame.size.height + 1) |
|||
let rectPath = UIBezierPath(roundedRect:labelRect, cornerRadius:timeLabel.frame.size.height) |
|||
rectPath.fill() |
|||
context.restoreGState() |
|||
|
|||
|
|||
|
|||
let path = UIBezierPath() |
|||
path.move(to: CGPoint(x: self.frame.size.width / 2 , y:timeLabel.frame.size.height)) |
|||
path.addLine(to: CGPoint(x:self.frame.size.width / 2 , y:self.frame.size.height)) |
|||
path.lineWidth = 1.4 |
|||
UIColor(hue: 0, saturation:0, brightness:1.0, alpha: 0.7).setStroke() |
|||
path.stroke() |
|||
|
|||
} |
|||
|
|||
|
|||
func gradient() { |
|||
let width = self.frame.size.width |
|||
|
|||
|
|||
let context = UIGraphicsGetCurrentContext()! |
|||
|
|||
let startColor = UIColor(hue: 0, saturation:0, brightness:0.0, alpha: 0.06).cgColor |
|||
let endColor = UIColor.clear.cgColor |
|||
let colors = [startColor, endColor] as CFArray |
|||
|
|||
let locations = [0, 1] as [CGFloat] |
|||
|
|||
let space = CGColorSpaceCreateDeviceRGB() |
|||
|
|||
let gradient = CGGradient(colorsSpace: space, colors: colors, locations: locations)! |
|||
context.drawLinearGradient(gradient, start: CGPoint(x:(width / 2) - 0.7, y:0), end: CGPoint(x: (width / 2) - 4, y: 0), options: []) |
|||
context.drawLinearGradient(gradient, start: CGPoint(x:(width / 2) + 0.7, y:0), end: CGPoint(x: (width / 2) + 4, y: 0), options: []) |
|||
} |
|||
|
|||
var ignoreSendScrollToParent = false |
|||
func setScrollPoint(_ scrollPoint:CGFloat) { |
|||
currentTime = Float64(scrollPoint) * duration |
|||
setTimeText() |
|||
if !ignoreSendScrollToParent { |
|||
if let parent = parentView { |
|||
parent.moved(currentTime) |
|||
} |
|||
} |
|||
|
|||
mainView!.currentTime = currentTime |
|||
ignoreSendScrollToParent = false |
|||
} |
|||
|
|||
func setTimeText() { |
|||
let minute = Int(currentTime / 60) |
|||
let second = (currentTime - Float64(minute) * 60) |
|||
let milliSec = Int((second - Float64(Int(second))) * 100) |
|||
let text = String(format: "%02d:%02d.%02d", minute, (Int(second)), milliSec) |
|||
timeLabel.text = text |
|||
} |
|||
} |
|||
@ -1,451 +0,0 @@ |
|||
// |
|||
// FrameImagesView.swift |
|||
// Examplay |
|||
// |
|||
// Created by Tomohiro Yamashita on 2020/03/01. |
|||
// Copyright © 2020 Tom. All rights reserved. |
|||
// |
|||
|
|||
import Foundation |
|||
import UIKit |
|||
import AVFoundation |
|||
|
|||
|
|||
|
|||
class FrameImage: UIImageView { |
|||
var tolerance:Float64? = nil |
|||
} |
|||
|
|||
|
|||
// MARK: - FrameImagesView |
|||
class FrameImagesView: UIScrollView { |
|||
|
|||
|
|||
var mainView:VideoTimelineView! |
|||
|
|||
var frameImagesArray:[FrameImage] = [] |
|||
|
|||
|
|||
var thumbnailFrameSize:CGSize = CGSize(width: 640,height: 480) |
|||
let preferredTimescale:Int32 = 100 |
|||
var timeTolerance = CMTimeMakeWithSeconds(10 , preferredTimescale:100) |
|||
|
|||
var maxWidth:CGFloat = 0 |
|||
var minWidth:CGFloat = 0 |
|||
|
|||
var parentScroller:TimelineScroller? = nil |
|||
|
|||
override init (frame: CGRect) { |
|||
super.init(frame: frame) |
|||
self.isScrollEnabled = false |
|||
self.isUserInteractionEnabled = false |
|||
self.backgroundColor = UIColor(hue: 0, saturation:0, brightness:0.0, alpha: 0.02) |
|||
} |
|||
|
|||
required init(coder aDecoder: NSCoder) { |
|||
fatalError("FrameImagesView init(coder:) has not been implemented") |
|||
} |
|||
|
|||
|
|||
|
|||
|
|||
func reset() { |
|||
discardAllFrameImages() |
|||
cancelImageGenerator() |
|||
|
|||
prepareFrameViews() |
|||
layout() |
|||
requestVisible(depth:0, wide:2, direction:0) |
|||
} |
|||
|
|||
//MARK: - timer for animation |
|||
var animationTimer = Timer() |
|||
var animating = false |
|||
func startAnimation() { |
|||
animating = true |
|||
displayFrames() |
|||
animationTimer = Timer.scheduledTimer(timeInterval: 0.01, target: self, selector: #selector(self.animate(_:)), userInfo: nil, repeats: true) |
|||
RunLoop.main.add(animationTimer, forMode:RunLoop.Mode.common) |
|||
|
|||
} |
|||
|
|||
func stopAnimation() { |
|||
animationTimer.invalidate() |
|||
animating = false |
|||
|
|||
|
|||
if let parent = parentScroller { |
|||
frame.origin = CGPoint(x: parent.frame.size.width / 2, y: parent.measureHeight) |
|||
parent.addSubview(self) |
|||
} |
|||
displayFrames() |
|||
} |
|||
|
|||
@objc func animate(_ timer:Timer) { |
|||
if animating == false { |
|||
return |
|||
} |
|||
displayFrames() |
|||
} |
|||
|
|||
//MARK: - layout |
|||
|
|||
|
|||
var uponFrames = Set<Int>() |
|||
var belowFrames = Set<Int>() |
|||
var deepFrames = Set<Int>() |
|||
var hiddenFrames = Set<Int>() |
|||
|
|||
|
|||
func layout() { |
|||
setThumnailFrameSize() |
|||
let coordinated = coordinateFrames() |
|||
uponFrames = coordinated.upon |
|||
belowFrames = coordinated.below |
|||
hiddenFrames = coordinated.hidden |
|||
deepFrames = coordinated.deep |
|||
displayFrames() |
|||
} |
|||
|
|||
func displayFrames() { |
|||
|
|||
var baseView:UIView = self |
|||
var offset:CGPoint = CGPoint.zero |
|||
|
|||
if let parent = parentScroller { |
|||
let visibleHalf = parent.frame.size.width / 2 |
|||
|
|||
if animating { |
|||
if let layer = parent.layer.presentation() { |
|||
offset.x = visibleHalf - layer.bounds.origin.x |
|||
offset.y = parent.frame.origin.y + parent.measureHeight |
|||
frame.origin = offset |
|||
mainView!.timelineView.viewForAnimate.addSubview(self) |
|||
} |
|||
} |
|||
} |
|||
|
|||
let visibleSide = indexOfVisibleSide() |
|||
func locate(_ index:Int, visible:Bool) { |
|||
if frameImagesArray.count > index && index >= 0 { |
|||
let frameImg = frameImagesArray[index] |
|||
if index >= visibleSide.left && index <= visibleSide.right { |
|||
let position = positionWithIndex(index) |
|||
frameImg.frame = CGRect(x: position, y:0, width: thumbnailFrameSize.width, height: thumbnailFrameSize.height) |
|||
if visible { |
|||
self.addSubview(frameImg) |
|||
} else { |
|||
frameImg.removeFromSuperview() |
|||
} |
|||
} else { |
|||
frameImg.removeFromSuperview() |
|||
} |
|||
|
|||
} |
|||
} |
|||
for element in belowFrames {//do addSubview under the upon |
|||
locate(element, visible:true) |
|||
} |
|||
for element in uponFrames { |
|||
locate(element, visible:true) |
|||
} |
|||
for element in hiddenFrames {// includes deep |
|||
locate(element, visible:false) |
|||
} |
|||
|
|||
} |
|||
|
|||
func positionWithIndex(_ index:Int) -> CGFloat { |
|||
let position = frame.size.width * (CGFloat(index) / CGFloat(thumbnailCountFloat())) |
|||
return position |
|||
} |
|||
|
|||
func coordinateFrames() -> (upon:Set<Int>, below:Set<Int>, hidden:Set<Int>, deep:Set<Int>) { |
|||
|
|||
let keyDivision = Int(pow(2,Double(Int(log2(maxWidth * 2 / (self.frame.size.width + 1)))))) |
|||
if keyDivision <= 0 { |
|||
return (Set<Int>(), Set<Int>(), Set<Int>(), Set<Int>()) |
|||
} |
|||
let keyCount = ((frameImagesArray.count - 1) / keyDivision) + 1 |
|||
|
|||
|
|||
var visibleIndexes = Set<Int>() |
|||
var uponElements = Set<Int>() |
|||
for index in 0 ... (keyCount - 1) { |
|||
let uponIndex = index * keyDivision |
|||
uponElements.insert(uponIndex) |
|||
visibleIndexes.insert(uponIndex) |
|||
} |
|||
var belowElements = Set<Int>() |
|||
let belowDivision = keyDivision / 2 |
|||
if belowDivision >= 1 { |
|||
for index in 0 ..< (keyCount - 1) { |
|||
let belowIndex = (index * keyDivision) + (keyDivision / 2) |
|||
belowElements.insert(belowIndex) |
|||
visibleIndexes.insert(belowIndex) |
|||
} |
|||
} |
|||
var deepElements = Set<Int>() |
|||
let deepDivision = belowDivision / 2 |
|||
if deepDivision >= 1 { |
|||
if let max = visibleIndexes.max() { |
|||
for index in visibleIndexes { |
|||
if max > index { |
|||
deepElements.insert(index + deepDivision) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
var hiddenElements = Set<Int>() |
|||
for index in 0 ..< frameImagesArray.count { |
|||
if !visibleIndexes.contains(index) { |
|||
let hiddenIndex = index |
|||
hiddenElements.insert(hiddenIndex) |
|||
} |
|||
} |
|||
return (uponElements,belowElements,hiddenElements, deepElements) |
|||
} |
|||
|
|||
|
|||
func setThumnailFrameSize() { |
|||
if let asset = mainView!.asset { |
|||
var frameSize = CGSize(width: 640,height: 480) |
|||
let tracks:Array = asset.tracks(withMediaType:AVMediaType.video) |
|||
if tracks.count > 0 { |
|||
let track = tracks[0] |
|||
frameSize = track.naturalSize.applying(track.preferredTransform) |
|||
frameSize.width = abs(frameSize.width) |
|||
frameSize.height = abs(frameSize.height) |
|||
} |
|||
thumbnailFrameSize = mainView!.resizeHeightKeepRatio(frameSize, height:self.frame.size.height) |
|||
} |
|||
} |
|||
|
|||
func assetDuration() -> Float64? { |
|||
if let asset = mainView!.asset { |
|||
return CMTimeGetSeconds(asset.duration) |
|||
} |
|||
return nil |
|||
} |
|||
|
|||
func prepareFrameViews() { |
|||
frameImagesArray = [] |
|||
for _ in 0 ... Int(thumbnailCountFloat()) { |
|||
let view = FrameImage() |
|||
view.alpha = 0 |
|||
frameImagesArray += [view] |
|||
} |
|||
} |
|||
|
|||
|
|||
|
|||
func thumbnailCountFloat() -> CGFloat { |
|||
return maxWidth / thumbnailFrameSize.width |
|||
} |
|||
|
|||
|
|||
func indexWithTime(_ time:Float64) -> Int? { |
|||
if let assetDuration = assetDuration() { |
|||
let value = time / (assetDuration / Float64(thumbnailCountFloat())) |
|||
var intValue = Int(value) |
|||
if value - Float64(intValue) >= 0.5 { |
|||
intValue += 1 |
|||
} |
|||
return intValue |
|||
} |
|||
return nil |
|||
} |
|||
|
|||
func timeWithIndex(_ index:Int) -> Float64 { |
|||
if let assetDuration = assetDuration() { |
|||
return assetDuration * ((Float64(index)) / Float64(thumbnailCountFloat())) |
|||
} |
|||
return 0 |
|||
} |
|||
|
|||
|
|||
|
|||
|
|||
//MARK: - frame images |
|||
func cancelImageGenerator() { |
|||
if let asset = mainView!.asset { |
|||
let assetImgGenerate : AVAssetImageGenerator = AVAssetImageGenerator(asset: asset) |
|||
assetImgGenerate.cancelAllCGImageGeneration() |
|||
} |
|||
} |
|||
|
|||
func discardAllFrameImages() { |
|||
|
|||
for index in 0 ..< frameImagesArray.count { |
|||
let view = frameImagesArray[index] |
|||
UIView.animate(withDuration: 0.5,delay:Double(0.0),options:UIView.AnimationOptions.curveEaseOut, animations: { () -> Void in |
|||
|
|||
view.alpha = 0 |
|||
|
|||
},completion: { finished in |
|||
|
|||
view.image = nil |
|||
view.removeFromSuperview() |
|||
view.tolerance = nil |
|||
}) |
|||
} |
|||
frameImagesArray = [] |
|||
} |
|||
|
|||
|
|||
|
|||
func requestAll(){ |
|||
|
|||
var timesArray = [NSValue]() |
|||
for index in 0 ..< frameImagesArray.count { |
|||
timesArray += [NSValue(time:CMTimeMakeWithSeconds(timeWithIndex(index) , preferredTimescale:preferredTimescale))] |
|||
} |
|||
requestImageGeneration(timesArray:timesArray) |
|||
} |
|||
|
|||
|
|||
var requesting = Set<Int>() |
|||
func requestVisible(depth:Int, wide:Float, direction:Float) { |
|||
var timesArray = [NSValue]() |
|||
func request(_ index:Int) { |
|||
|
|||
if self.requesting.count > 0 && self.requesting.contains(index) { |
|||
return |
|||
} |
|||
|
|||
if frameImagesArray.count > index { |
|||
let imageView = frameImagesArray[index] |
|||
var needsUpdate = false |
|||
if let tolerance = imageView.tolerance { |
|||
if tolerance > CMTimeGetSeconds(timeTolerance) * 1.2 { |
|||
needsUpdate = true |
|||
} |
|||
} |
|||
if imageView.image == nil || needsUpdate { |
|||
timesArray += [NSValue(time:CMTimeMakeWithSeconds(timeWithIndex(index) , preferredTimescale:preferredTimescale))] |
|||
self.requesting.insert(index) |
|||
} |
|||
} |
|||
} |
|||
let visibleSide = indexOfVisibleSide() |
|||
let width = visibleSide.right - visibleSide.left |
|||
var additionLeft = Int(Float(width) * wide) |
|||
var additionRight = additionLeft |
|||
if direction > 0 { |
|||
additionLeft = Int(Float(-width) * direction) |
|||
} else if direction < 0 { |
|||
additionRight = Int(Float(width) * direction) |
|||
} |
|||
|
|||
|
|||
for index in uponFrames { |
|||
if index >= visibleSide.left - additionLeft && index <= visibleSide.right + additionRight { |
|||
request(index) |
|||
} |
|||
} |
|||
let belowAddLeft = Int(Float(additionLeft) * 0.5) - Int(Float(width) * 0.7) |
|||
let belowAddRight = Int(Float(additionRight) * 0.5) - Int(Float(width) * 0.7) |
|||
if depth > 0 { |
|||
for index in belowFrames { |
|||
if index >= visibleSide.left - belowAddLeft && index <= visibleSide.right + belowAddRight { |
|||
request(index) |
|||
} |
|||
} |
|||
} |
|||
let deepAddLeft = Int(Float(additionLeft) * 0.2) - Int(Float(width) * 0.4) |
|||
let deepAddRight = Int(Float(additionRight) * 0.2) - Int(Float(width) * 0.4) |
|||
if depth > 1 { |
|||
for index in deepFrames { |
|||
if index >= visibleSide.left - deepAddLeft && index <= visibleSide.right + deepAddRight { |
|||
request(index) |
|||
} |
|||
} |
|||
} |
|||
if timesArray.count > 0 { |
|||
requestImageGeneration(timesArray:timesArray) |
|||
} |
|||
} |
|||
|
|||
func indexOfVisibleSide() -> (left:Int, right:Int) { |
|||
func indexWithPosition(_ position:CGFloat) -> Int{ |
|||
let index = position * CGFloat(thumbnailCountFloat()) / frame.size.width |
|||
var indexInt = Int(index) |
|||
if index - CGFloat(indexInt) > 0.5 { |
|||
indexInt += 1 |
|||
} |
|||
return indexInt |
|||
} |
|||
var visibleLeft = indexWithPosition(-(parentScroller!.frame.width / 2) + parentScroller!.contentOffset.x - thumbnailFrameSize.width) - 1 |
|||
var visibleRight = indexWithPosition(parentScroller!.contentOffset.x + (parentScroller!.frame.size.width * 0.5) + thumbnailFrameSize.width) |
|||
if visibleLeft < 0 { |
|||
visibleLeft = 0 |
|||
} |
|||
let max = frameImagesArray.count - 1 |
|||
if visibleRight > max { |
|||
visibleRight = max |
|||
} |
|||
return (visibleLeft, visibleRight) |
|||
} |
|||
|
|||
func updateTolerance() { |
|||
if mainView!.asset == nil { |
|||
return |
|||
} |
|||
let thumbDuration = Float64(thumbnailFrameSize.width / self.frame.size.width) * mainView!.duration * 2 |
|||
timeTolerance = CMTimeMakeWithSeconds(thumbDuration , preferredTimescale:100) |
|||
|
|||
|
|||
} |
|||
|
|||
func requestImageGeneration(timesArray:[NSValue]) { |
|||
|
|||
if let asset = mainView!.asset { |
|||
let assetImgGenerate : AVAssetImageGenerator = AVAssetImageGenerator(asset: asset) |
|||
assetImgGenerate.appliesPreferredTrackTransform = true |
|||
let maxsize = CGSize(width: thumbnailFrameSize.width * 1.5,height: thumbnailFrameSize.height * 1.5) |
|||
assetImgGenerate.maximumSize = maxsize |
|||
assetImgGenerate.requestedTimeToleranceAfter = timeTolerance |
|||
assetImgGenerate.requestedTimeToleranceBefore = timeTolerance |
|||
|
|||
assetImgGenerate.generateCGImagesAsynchronously(forTimes: timesArray, |
|||
completionHandler: |
|||
{ time,resultImage,actualTime,result,error in |
|||
|
|||
let timeValue = CMTimeGetSeconds(time) |
|||
if let image = resultImage { |
|||
DispatchQueue.main.async { |
|||
self.setFrameImage(image:UIImage(cgImage:image), time:timeValue) |
|||
} |
|||
} |
|||
if let index = self.indexWithTime(timeValue) { |
|||
self.requesting.remove(index) |
|||
} |
|||
}) |
|||
} |
|||
} |
|||
|
|||
|
|||
func setFrameImage(image:UIImage, time:Float64) { |
|||
|
|||
if let index = indexWithTime(time) { |
|||
if frameImagesArray.count > index && index >= 0 { |
|||
let imageView = frameImagesArray[index] |
|||
|
|||
imageView.image = image |
|||
imageView.tolerance = CMTimeGetSeconds(timeTolerance) |
|||
UIView.animate(withDuration: 0.2,delay:Double(0),options:UIView.AnimationOptions.curveEaseOut, animations: { () -> Void in |
|||
|
|||
imageView.alpha = 1 |
|||
|
|||
},completion: { finished in |
|||
|
|||
|
|||
}) |
|||
imageView.alpha = 1 |
|||
imageView.backgroundColor = .red |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
|
|||
@ -1,206 +0,0 @@ |
|||
// |
|||
// TimelineMeasure.swift |
|||
// Examplay |
|||
// |
|||
// Created by Tomohiro Yamashita on 2020/03/08. |
|||
// Copyright © 2020 Tom. All rights reserved. |
|||
// |
|||
|
|||
import UIKit |
|||
|
|||
class TimelineMeasure: UIView { |
|||
|
|||
var unitSize:CGFloat = 100 |
|||
var frameImagesView:FrameImagesView? = nil |
|||
var parentScroller:TimelineScroller? = nil |
|||
var parentView:TimelineView? = nil |
|||
var stringColor:UIColor = UIColor(hue: 0.0, saturation:0.0, brightness:0.35, alpha: 1) |
|||
var animating = false |
|||
|
|||
override init (frame: CGRect) { |
|||
super.init(frame: frame) |
|||
|
|||
self.isMultipleTouchEnabled = true |
|||
self.isUserInteractionEnabled = true |
|||
} |
|||
|
|||
required init(coder aDecoder: NSCoder) { |
|||
fatalError("TimelineMeasure init(coder:) has not been implemented") |
|||
} |
|||
|
|||
|
|||
|
|||
override func draw(_ rect: CGRect) { |
|||
|
|||
if frameImagesView == nil { |
|||
return |
|||
} |
|||
|
|||
let max = frameImagesView!.maxWidth |
|||
var width = frameImagesView!.frame.size.width |
|||
if animating { |
|||
if let layer = frameImagesView!.layer.presentation() { |
|||
width = layer.frame.size.width |
|||
} |
|||
} |
|||
if width == 0 { |
|||
return |
|||
} |
|||
var unit = (width / max) * unitSize |
|||
|
|||
let unitWidth = CGFloat(80) |
|||
var division = 0 |
|||
while (unit <= unitWidth) { |
|||
unit *= 2 |
|||
division += 1 |
|||
if division > 1000 { |
|||
break |
|||
} |
|||
} |
|||
let unitLength = pow(2,Double(division + 1)) / 2 |
|||
func index(_ position:CGFloat) -> Int { |
|||
return Int(position / unit) |
|||
} |
|||
|
|||
let paragraphStyle = NSMutableParagraphStyle() |
|||
paragraphStyle.alignment = .center |
|||
|
|||
let attributes = [NSAttributedString.Key.font: UIFont(name: "HelveticaNeue", size: self.frame.size.height * 0.7)!, NSAttributedString.Key.paragraphStyle: paragraphStyle, NSAttributedString.Key.foregroundColor: stringColor] |
|||
|
|||
var string = "" |
|||
|
|||
var visibleRect = rect |
|||
if let parent = parentScroller { |
|||
|
|||
if animating { |
|||
if let layer = parent.layer.presentation() { |
|||
let offset = layer.bounds.origin |
|||
visibleRect.origin = offset |
|||
visibleRect.size = parent.frame.size |
|||
} else { |
|||
visibleRect = parent.visibleRect() |
|||
} |
|||
} else { |
|||
visibleRect = parent.visibleRect() |
|||
} |
|||
} |
|||
let startIndex = index(visibleRect.origin.x) - 10 * (division + 1) |
|||
let endIndex = index(visibleRect.origin.x + visibleRect.size.width) |
|||
|
|||
for index in startIndex ... endIndex { |
|||
if index < 0 { |
|||
continue |
|||
} |
|||
let position = CGFloat(index) * unit - visibleRect.origin.x + (visibleRect.size.width / 2) |
|||
if division == 0 { |
|||
let path = UIBezierPath() |
|||
path.move(to: CGPoint(x: position + (unit / 2 ) - (unitWidth / 4) , y:(self.frame.size.height / 2))) |
|||
path.addLine(to: CGPoint(x: position + (unit / 2) + (unitWidth / 4), y:(self.frame.size.height / 2))) |
|||
path.lineWidth = 1 |
|||
stringColor.setStroke() |
|||
path.stroke() |
|||
} else { |
|||
for i in 1 ... 3 { |
|||
let point = position + ((unit / 4 ) * CGFloat(i)) |
|||
let r:CGFloat = 0.8 |
|||
let pointRect = CGRect(x: point - r, y: (self.frame.size.height / 2) - r, width: r * 2,height: r * 2) |
|||
let path = UIBezierPath(ovalIn:pointRect) |
|||
stringColor.setFill() |
|||
path.fill() |
|||
} |
|||
} |
|||
|
|||
let length = unitLength * Double(index) |
|||
let minute = Int(length / 60) |
|||
let second = Int(length - Double(minute * 60)) |
|||
|
|||
string = String(format: "%02d:%02d", minute, (Int(second))) |
|||
|
|||
string.draw(with: CGRect(x: position - (unitWidth / 2), y:0, width: unitWidth, height: self.frame.size.height), options: .usesLineFragmentOrigin, attributes: attributes, context: nil) |
|||
|
|||
|
|||
} |
|||
} |
|||
|
|||
//MARK: - timer for animation |
|||
var animationTimer = Timer() |
|||
func startAnimation() { |
|||
animationTimer = Timer.scheduledTimer(timeInterval: 0.01, target: self, selector: #selector(self.animate(_:)), userInfo: nil, repeats: true) |
|||
RunLoop.main.add(animationTimer, forMode:RunLoop.Mode.common) |
|||
animating = true |
|||
} |
|||
|
|||
func stopAnimation() { |
|||
animating = false |
|||
self.setNeedsDisplay() |
|||
animationTimer.invalidate() |
|||
} |
|||
|
|||
@objc func animate(_ timer:Timer) { |
|||
if animating == false { |
|||
return |
|||
} |
|||
self.setNeedsDisplay() |
|||
} |
|||
|
|||
|
|||
|
|||
//MARK: - Touch Events |
|||
var allTouches = [UITouch]() |
|||
|
|||
override open func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) |
|||
{ |
|||
for touch in touches { |
|||
if !allTouches.contains(touch) { |
|||
allTouches += [touch] |
|||
} |
|||
if !parentView!.allTouches.contains(touch) { |
|||
parentView!.allTouches += [touch] |
|||
} |
|||
} |
|||
} |
|||
|
|||
|
|||
override open func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) |
|||
{ |
|||
if parentView!.allTouches.count == 2 { |
|||
if parentView!.pinching { |
|||
parentView!.updatePinch() |
|||
} else { |
|||
parentView!.startPinch() |
|||
} |
|||
} |
|||
} |
|||
|
|||
override open func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) |
|||
{ |
|||
for touch in touches { |
|||
if let index = allTouches.firstIndex(of:touch) { |
|||
allTouches.remove(at: index) |
|||
} |
|||
if let index = parentView!.allTouches.firstIndex(of:touch) { |
|||
parentView!.allTouches.remove(at: index) |
|||
} |
|||
} |
|||
if parentView!.pinching && parentView!.allTouches.count < 2 { |
|||
parentView!.endPinch() |
|||
} |
|||
} |
|||
|
|||
override open func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) |
|||
{ |
|||
for touch in touches { |
|||
if let index = allTouches.firstIndex(of:touch) { |
|||
allTouches.remove(at: index) |
|||
} |
|||
if let index = parentView!.allTouches.firstIndex(of:touch) { |
|||
parentView!.allTouches.remove(at: index) |
|||
} |
|||
} |
|||
|
|||
if parentView!.pinching { |
|||
parentView!.endPinch() |
|||
} |
|||
} |
|||
} |
|||
|
|||
@ -1,178 +0,0 @@ |
|||
// |
|||
// TimelineScroller.swift |
|||
// Examplay |
|||
// |
|||
// Created by Tomohiro Yamashita on 2020/03/01. |
|||
// Copyright © 2020 Tom. All rights reserved. |
|||
// |
|||
|
|||
import UIKit |
|||
|
|||
class TimelineScroller: UIScrollView { |
|||
|
|||
var parentView:TimelineView? = nil |
|||
let frameImagesView = FrameImagesView() |
|||
let measure = TimelineMeasure() |
|||
let trimView = TrimView() |
|||
|
|||
|
|||
override init (frame: CGRect) { |
|||
super.init(frame: frame) |
|||
|
|||
self.isScrollEnabled = true |
|||
self.isDirectionalLockEnabled = true |
|||
self.showsHorizontalScrollIndicator = false |
|||
self.showsVerticalScrollIndicator = false |
|||
self.bounces = false |
|||
self.decelerationRate = .fast |
|||
self.isMultipleTouchEnabled = true |
|||
self.delaysContentTouches = false |
|||
self.frameImagesView.parentScroller = self |
|||
self.addSubview(frameImagesView) |
|||
self.measure.parentScroller = self |
|||
self.measure.frameImagesView = self.frameImagesView |
|||
self.measure.backgroundColor = .clear |
|||
} |
|||
|
|||
required init(coder aDecoder: NSCoder) { |
|||
fatalError("TimelineScroller init(coder:) has not been implemented") |
|||
} |
|||
|
|||
func configure(parent:TimelineView) { |
|||
parentView = parent |
|||
trimView.configure(parent, scroller:self) |
|||
} |
|||
|
|||
func reset() { |
|||
frameImagesView.reset() |
|||
} |
|||
|
|||
var ignoreScrollViewDidScroll:Bool = false |
|||
func setContentWidth(_ width:CGFloat) { |
|||
setContentWidth(width, setOrigin:true) |
|||
} |
|||
|
|||
func setContentWidth(_ width: CGFloat, setOrigin:Bool) { |
|||
ignoreScrollViewDidScroll = true |
|||
self.contentSize = CGSize(width:width + self.frame.size.width, height:self.frame.size.height) |
|||
frameImagesView.frame.size.width = width |
|||
//measure.frame.size.width = width |
|||
|
|||
if setOrigin { |
|||
let halfVisibleWidth = self.frame.size.width / 2 |
|||
//frameImagesView.frame.size.height = self.frame.size.height |
|||
frameImagesView.frame.origin.x = halfVisibleWidth |
|||
} |
|||
//measure.frame.origin.x = halfVisibleWidth |
|||
measure.setNeedsDisplay() |
|||
frameImagesView.displayFrames() |
|||
} |
|||
|
|||
var measureHeight:CGFloat = 5 |
|||
func coordinate() { |
|||
|
|||
let measureMin:CGFloat = 10 |
|||
|
|||
let wholeHeight = self.frame.size.height |
|||
measureHeight = wholeHeight * 0.2 |
|||
if measureHeight < measureMin { |
|||
measureHeight = measureMin |
|||
} |
|||
frameImagesView.frame.size.height = wholeHeight - measureHeight |
|||
if frameImagesView.animating == false { |
|||
frameImagesView.frame.origin = CGPoint(x: self.frame.size.width / 2,y: measureHeight) |
|||
} |
|||
measure.frame.size.height = measureHeight |
|||
//frameImagesView.layout() |
|||
|
|||
trimView.frame = self.frame |
|||
|
|||
} |
|||
|
|||
func visibleRect() -> CGRect { |
|||
var visibleRect = frame |
|||
visibleRect.origin = contentOffset |
|||
if contentSize.width < frame.size.width { |
|||
visibleRect.size.width = contentSize.width |
|||
} |
|||
if contentSize.height < frame.size.height { |
|||
visibleRect.size.height = contentSize.height |
|||
} |
|||
if zoomScale != 1 { |
|||
let theScale = 1.0 / zoomScale; |
|||
visibleRect.origin.x *= theScale; |
|||
visibleRect.origin.y *= theScale; |
|||
visibleRect.size.width *= theScale; |
|||
visibleRect.size.height *= theScale; |
|||
} |
|||
return visibleRect |
|||
} |
|||
|
|||
|
|||
//MARK: - scroll |
|||
func setScrollPoint(_ scrollPoint:CGFloat) { |
|||
let offset = (scrollPoint * frameImagesView.frame.size.width) + (self.frame.size.width / 2) |
|||
|
|||
self.contentOffset.x = offset - (self.frame.size.width / 2) |
|||
} |
|||
|
|||
|
|||
//MARK: - Touch Events |
|||
var allTouches = [UITouch]() |
|||
|
|||
override open func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) |
|||
{ |
|||
for touch in touches { |
|||
if !allTouches.contains(touch) { |
|||
allTouches += [touch] |
|||
} |
|||
if !parentView!.allTouches.contains(touch) { |
|||
parentView!.allTouches += [touch] |
|||
} |
|||
} |
|||
|
|||
} |
|||
|
|||
override open func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) |
|||
{ |
|||
if parentView!.allTouches.count == 2 { |
|||
if parentView!.pinching { |
|||
parentView!.updatePinch() |
|||
} else { |
|||
parentView!.startPinch() |
|||
} |
|||
} |
|||
} |
|||
|
|||
override open func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) |
|||
{ |
|||
for touch in touches { |
|||
if let index = allTouches.firstIndex(of:touch) { |
|||
allTouches.remove(at: index) |
|||
} |
|||
if let index = parentView!.allTouches.firstIndex(of:touch) { |
|||
parentView!.allTouches.remove(at: index) |
|||
} |
|||
} |
|||
if parentView!.pinching && parentView!.allTouches.count < 2 { |
|||
parentView!.endPinch() |
|||
} |
|||
} |
|||
|
|||
override open func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) |
|||
{ |
|||
for touch in touches { |
|||
if let index = allTouches.firstIndex(of:touch) { |
|||
allTouches.remove(at: index) |
|||
} |
|||
if let index = parentView!.allTouches.firstIndex(of:touch) { |
|||
parentView!.allTouches.remove(at: index) |
|||
} |
|||
} |
|||
|
|||
if parentView!.pinching { |
|||
parentView!.endPinch() |
|||
} |
|||
} |
|||
} |
|||
|
|||
@ -1,460 +0,0 @@ |
|||
// |
|||
// TimelineView.swift |
|||
// Examplay |
|||
// |
|||
// Created by Tomohiro Yamashita on 2020/02/18. |
|||
// Copyright © 2020 Tom. All rights reserved. |
|||
// |
|||
|
|||
import Foundation |
|||
import UIKit |
|||
import AVFoundation |
|||
|
|||
class TimelineView: UIView, UIScrollViewDelegate { |
|||
var mainView:VideoTimelineView? = nil |
|||
let scroller = TimelineScroller() |
|||
let centerLine = CenterLine() |
|||
let viewForAnimate = UIScrollView() |
|||
|
|||
let durationPerHeight:Float64 = 0.35 |
|||
var animating = false |
|||
|
|||
override init (frame: CGRect) { |
|||
|
|||
super.init(frame: frame) |
|||
|
|||
self.backgroundColor = UIColor(hue: 0, saturation:0, brightness:0.0, alpha: 0.05) |
|||
|
|||
viewForAnimate.frame.origin = CGPoint.zero |
|||
viewForAnimate.isScrollEnabled = false |
|||
viewForAnimate.isUserInteractionEnabled = false |
|||
self.addSubview(viewForAnimate) |
|||
|
|||
self.addSubview(scroller) |
|||
scroller.delegate = self |
|||
|
|||
self.addSubview(scroller.measure) |
|||
scroller.configure(parent: self) |
|||
centerLine.configure(parent:self) |
|||
coordinate() |
|||
|
|||
self.addSubview(scroller.trimView) |
|||
self.addSubview(centerLine) |
|||
|
|||
scroller.measure.parentView = self |
|||
} |
|||
|
|||
required init(coder aDecoder: NSCoder) { |
|||
fatalError("TimelineView init(coder:) has not been implemented") |
|||
} |
|||
|
|||
|
|||
//MARK: - coordinate |
|||
func coordinate() { |
|||
if mainView == nil { |
|||
return |
|||
} |
|||
frame = mainView!.bounds |
|||
viewForAnimate.frame.size = self.frame.size |
|||
|
|||
scroller.frame = self.bounds |
|||
scroller.frameImagesView.frame.size.height = scroller.frame.size.height |
|||
scroller.measure.frame = scroller.frame |
|||
scroller.measure.frame.size.height = 20 |
|||
scroller.coordinate() |
|||
|
|||
centerLine.timeLabel.frame.size.height = scroller.measure.frame.size.height - 2 |
|||
let centerLineWidth:CGFloat = scroller.measure.frame.size.height * 5 |
|||
centerLine.frame = CGRect(x: (self.frame.size.width - centerLineWidth) / 2,y: 0,width: centerLineWidth,height: self.frame.size.height) |
|||
|
|||
centerLine.update() |
|||
|
|||
guard let view = (mainView) else { return } |
|||
guard let _ = (view.asset) else { return } |
|||
if scroller.frameImagesView.frame.size.width <= 0 { |
|||
return |
|||
} |
|||
let previousThumbSize = scroller.frameImagesView.thumbnailFrameSize |
|||
scroller.frameImagesView.setThumnailFrameSize() |
|||
let thumbSize = scroller.frameImagesView.thumbnailFrameSize |
|||
let unit = (thumbSize.height / CGFloat(durationPerHeight)) |
|||
scroller.measure.unitSize = unit |
|||
let contentMaxWidth = (unit * CGFloat(centerLine.duration)) |
|||
scroller.frameImagesView.maxWidth = contentMaxWidth |
|||
let defineMin = scroller.frame.size.width * 0.8 |
|||
var contentMinWidth:CGFloat |
|||
|
|||
if scroller.frameImagesView.maxWidth <= defineMin { |
|||
contentMinWidth = scroller.frameImagesView.maxWidth |
|||
} else { |
|||
contentMinWidth = snapWidth(defineMin, max:scroller.frameImagesView.maxWidth) |
|||
} |
|||
|
|||
scroller.frameImagesView.minWidth = contentMinWidth |
|||
|
|||
var currentWidth = snapWidth((scroller.frameImagesView.thumbnailFrameSize.width / previousThumbSize.width) * scroller.frameImagesView.frame.size.width, max:scroller.frameImagesView.maxWidth) |
|||
if currentWidth < scroller.frameImagesView.minWidth { |
|||
currentWidth = scroller.frameImagesView.minWidth |
|||
} else if currentWidth > scroller.frameImagesView.maxWidth { |
|||
currentWidth = scroller.frameImagesView.maxWidth |
|||
} |
|||
|
|||
scroller.setContentWidth(currentWidth) |
|||
|
|||
scroller.reset() |
|||
|
|||
scroller.trimView.layout() |
|||
if view.currentTime <= view.duration { |
|||
setCurrentTime(view.currentTime, force:false) |
|||
} |
|||
} |
|||
|
|||
func snapWidth(_ width:CGFloat, max:CGFloat) -> CGFloat { |
|||
let n = log2((2 * max) / width) |
|||
var intN = CGFloat(Int(n)) |
|||
if n - intN >= 0.5 { |
|||
intN += 1 |
|||
} |
|||
let result = (2 * max) / (pow(2,intN)) |
|||
return result |
|||
} |
|||
|
|||
func scrollPoint() -> CGFloat { |
|||
return scroller.contentOffset.x / scroller.frameImagesView.frame.size.width |
|||
} |
|||
|
|||
|
|||
|
|||
//MARK: new movie set |
|||
func newMovieSet() { |
|||
|
|||
coordinate() |
|||
if let asset = mainView!.asset{ |
|||
scroller.frameImagesView.setThumnailFrameSize() |
|||
|
|||
let duration = asset.duration |
|||
let durationFloat = CMTimeGetSeconds(duration) |
|||
centerLine.duration = durationFloat |
|||
|
|||
let detailThumbSize = scroller.frameImagesView.thumbnailFrameSize |
|||
|
|||
let unit = (detailThumbSize.height / CGFloat(durationPerHeight)) |
|||
scroller.measure.unitSize = unit |
|||
|
|||
|
|||
let contentMaxWidth = (unit * CGFloat(centerLine.duration)) |
|||
scroller.frameImagesView.maxWidth = contentMaxWidth |
|||
|
|||
|
|||
let defineMin = scroller.frame.size.width * 0.8 |
|||
var contentMinWidth:CGFloat |
|||
|
|||
if scroller.frameImagesView.maxWidth <= defineMin { |
|||
contentMinWidth = scroller.frameImagesView.maxWidth |
|||
} else { |
|||
contentMinWidth = snapWidth(defineMin, max:scroller.frameImagesView.maxWidth) |
|||
} |
|||
scroller.frameImagesView.minWidth = contentMinWidth |
|||
scroller.setContentWidth(scroller.frameImagesView.minWidth) |
|||
|
|||
scroller.reset() |
|||
scroller.trimView.reset(duration:durationFloat) |
|||
} |
|||
} |
|||
|
|||
//MARK: - currentTime |
|||
func setCurrentTime(_ currentTime:Float64, force:Bool) { |
|||
if inAction() && force == false { |
|||
return |
|||
} |
|||
if mainView!.asset == nil { |
|||
return |
|||
} |
|||
var scrollPoint:CGFloat = 0 |
|||
scrollPoint = CGFloat(currentTime / mainView!.duration) |
|||
|
|||
centerLine.ignoreSendScrollToParent = true |
|||
centerLine.setScrollPoint(scrollPoint) |
|||
scroller.ignoreScrollViewDidScroll = true |
|||
scroller.setScrollPoint(scrollPoint) |
|||
|
|||
scroller.frameImagesView.requestVisible(depth:0, wide:0, direction:0) |
|||
|
|||
scroller.frameImagesView.displayFrames() |
|||
scroller.measure.setNeedsDisplay() |
|||
} |
|||
|
|||
func moved(_ currentTime:Float64) { |
|||
mainView!.timelineIsMoved(currentTime, scrub:true) |
|||
} |
|||
|
|||
|
|||
//MARK: - TrimViews |
|||
func setTrimmerStatus(enabled:Bool) { |
|||
if enabled { |
|||
scroller.trimView.alpha = 1 |
|||
scroller.trimView.startKnob.isUserInteractionEnabled = true |
|||
scroller.trimView.alpha = 1 |
|||
scroller.trimView.endKnob.isUserInteractionEnabled = true |
|||
|
|||
} else { |
|||
scroller.trimView.alpha = 0.5 |
|||
scroller.trimView.startKnob.isUserInteractionEnabled = false |
|||
scroller.trimView.alpha = 0.5 |
|||
scroller.trimView.endKnob.isUserInteractionEnabled = false |
|||
} |
|||
} |
|||
|
|||
func setTrimmerVisible(_ visible:Bool) { |
|||
scroller.trimView.isHidden = !visible |
|||
} |
|||
|
|||
|
|||
func setTrim(start:Float64?, end:Float64?) { |
|||
var changed = false |
|||
if start != nil { |
|||
scroller.trimView.startKnob.knobTimePoint = start! |
|||
changed = true |
|||
} |
|||
if end != nil { |
|||
scroller.trimView.endKnob.knobTimePoint = end! |
|||
changed = true |
|||
} |
|||
if changed { |
|||
scroller.trimView.layout() |
|||
} |
|||
} |
|||
|
|||
func setTrimWithAnimation(trim:VideoTimelineTrim, time:Float64) { |
|||
scroller.trimView.moveToTimeAndTrimWithAnimation(time, trim:trim) |
|||
} |
|||
|
|||
|
|||
var manualScrolledAfterEnd = false |
|||
func setTrimViewInteraction(_ active:Bool) { |
|||
if mainView!.trimEnabled == false && active { |
|||
return |
|||
} |
|||
|
|||
scroller.trimView.startKnob.isUserInteractionEnabled = active |
|||
scroller.trimView.endKnob.isUserInteractionEnabled = active |
|||
|
|||
if active { |
|||
setManualScrolledAfterEnd() |
|||
} |
|||
} |
|||
|
|||
func setManualScrolledAfterEnd() { |
|||
let trim = currentTrim() |
|||
if mainView!.asset != nil { |
|||
let currentTime = mainView!.currentTime |
|||
if currentTime >= trim.end { |
|||
manualScrolledAfterEnd = true |
|||
} else { |
|||
manualScrolledAfterEnd = false |
|||
} |
|||
} |
|||
} |
|||
|
|||
func currentTrim() -> (start:Float64, end:Float64) { |
|||
var start = scroller.trimView.startKnob.knobTimePoint |
|||
var end = scroller.trimView.endKnob.knobTimePoint |
|||
if mainView!.asset != nil { |
|||
if end > mainView!.duration { |
|||
end = mainView!.duration |
|||
} |
|||
if start < 0 { |
|||
start = 0 |
|||
} |
|||
} |
|||
return (start, end) |
|||
} |
|||
|
|||
|
|||
func swapTrimKnobs() { |
|||
let knob = scroller.trimView.endKnob |
|||
scroller.trimView.endKnob = scroller.trimView.startKnob |
|||
scroller.trimView.startKnob = knob |
|||
} |
|||
|
|||
//MARK: - animation |
|||
func startAnimation() { |
|||
scroller.frameImagesView.startAnimation() |
|||
scroller.measure.startAnimation() |
|||
scroller.trimView.startAnimation() |
|||
} |
|||
|
|||
func stopAnimation() { |
|||
scroller.frameImagesView.stopAnimation() |
|||
scroller.measure.stopAnimation() |
|||
scroller.trimView.stopAnimation() |
|||
} |
|||
|
|||
|
|||
|
|||
|
|||
|
|||
//MARK: - Gestures |
|||
func inAction() -> Bool { |
|||
if allTouches.count > 0 || scroller.isTracking || scroller.isDecelerating { |
|||
return true |
|||
} else { |
|||
return false |
|||
} |
|||
} |
|||
|
|||
//MARK: - Scrolling |
|||
|
|||
var allTouches = [UITouch]() |
|||
var pinching:Bool = false |
|||
|
|||
func scrollViewDidScroll(_ scrollView:UIScrollView) { |
|||
scroller.trimView.layout() |
|||
if scroller.ignoreScrollViewDidScroll { |
|||
scroller.ignoreScrollViewDidScroll = false |
|||
return |
|||
} |
|||
scroller.measure.setNeedsDisplay() |
|||
scroller.frameImagesView.displayFrames() |
|||
setTrimViewInteraction(false) |
|||
let scrollPoint = scroller.contentOffset.x / scroller.frameImagesView.frame.size.width |
|||
self.centerLine.setScrollPoint(scrollPoint) |
|||
|
|||
guard let mView = (mainView) else { return } |
|||
if let receiver = mView.playStatusReceiver { |
|||
receiver.videoTimelineMoved() |
|||
} |
|||
} |
|||
|
|||
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { |
|||
scroller.frameImagesView.requestVisible(depth:0, wide:0, direction:0) |
|||
setTrimViewInteraction(true) |
|||
let scrollPoint = scroller.contentOffset.x / scroller.frameImagesView.frame.size.width |
|||
self.centerLine.setScrollPoint(scrollPoint) |
|||
} |
|||
|
|||
func scrollViewDidEndDragging(_ scrollView: UIScrollView, |
|||
willDecelerate decelerate: Bool) { |
|||
scroller.frameImagesView.requestVisible(depth:0, wide:0, direction:0) |
|||
if decelerate == false { |
|||
setTrimViewInteraction(true) |
|||
} |
|||
let scrollPoint = scroller.contentOffset.x / scroller.frameImagesView.frame.size.width |
|||
self.centerLine.setScrollPoint(scrollPoint) |
|||
} |
|||
|
|||
|
|||
|
|||
//MARK: - Zooming |
|||
|
|||
var pinchCenterInContent:CGFloat = 0 |
|||
var pinchStartDistance:CGFloat = 0 |
|||
var pinchStartContent:(x:CGFloat,width:CGFloat) = (0,0) |
|||
func pinchCenter(_ pointA:CGPoint, pointB:CGPoint) -> CGPoint { |
|||
return CGPoint(x: (pointA.x + pointB.x) / 2, y: (pointA.y + pointB.y) / 2) |
|||
} |
|||
func pinchDistance(_ pointA:CGPoint, pointB:CGPoint) -> CGFloat { |
|||
return sqrt(pow((pointA.x - pointB.x),2) + pow((pointA.y - pointB.y),2)); |
|||
} |
|||
func startPinch() { |
|||
pinching = true |
|||
scroller.isScrollEnabled = false |
|||
|
|||
let touch1 = allTouches[0] |
|||
let touch2 = allTouches[1] |
|||
let center = pinchCenter(touch1.location(in: self),pointB: touch2.location(in: self)) |
|||
|
|||
pinchStartDistance = pinchDistance(touch1.location(in: self),pointB: touch2.location(in: self)) |
|||
let framewidth = scroller.frame.size.width |
|||
pinchStartContent = ((framewidth / 2) - scroller.contentOffset.x,scroller.contentSize.width - framewidth) |
|||
pinchCenterInContent = (center.x - pinchStartContent.x) / pinchStartContent.width |
|||
} |
|||
|
|||
func updatePinch() { |
|||
let touch1 = allTouches[0] |
|||
let touch2 = allTouches[1] |
|||
let center = pinchCenter(touch1.location(in: self), pointB:touch2.location(in: self)) |
|||
var sizeChange = (1 * pinchDistance(touch1.location(in: self), pointB: touch2.location(in: self))) / pinchStartDistance |
|||
|
|||
var contentWidth = pinchStartContent.width * sizeChange |
|||
|
|||
let sizeMin = scroller.frameImagesView.minWidth |
|||
let sizeMax = scroller.frameImagesView.maxWidth |
|||
|
|||
if contentWidth < sizeMin { |
|||
let sizeUnit = sizeMin / pinchStartContent.width |
|||
sizeChange = ((pow(sizeChange/sizeUnit,2)/4) + 0.75) * sizeUnit |
|||
contentWidth = pinchStartContent.width * sizeChange |
|||
contentWidth = sizeMin |
|||
} else if contentWidth > sizeMax { |
|||
sizeChange = sizeMax |
|||
contentWidth = sizeMax |
|||
} else { |
|||
let startRatio = pinchStartContent.width / sizeMax |
|||
let currentRatio = startRatio * sizeChange |
|||
let effect = ((sin(CGFloat.pi * 2 * log2(2/currentRatio)) * 0.108) - (sin(CGFloat.pi * 6 * log2(2/currentRatio)) * 0.009)) * currentRatio |
|||
let resultWidth = sizeMax * (currentRatio + effect) |
|||
contentWidth = resultWidth |
|||
} |
|||
let contentOrigin = center.x - (contentWidth * pinchCenterInContent) |
|||
scroller.contentOffset.x = (scroller.frame.size.width / 2) - contentOrigin |
|||
scroller.setContentWidth(contentWidth) |
|||
scroller.frameImagesView.layout() |
|||
|
|||
scroller.frameImagesView.requestVisible(depth:2, wide:0, direction:0) |
|||
self.centerLine.setScrollPoint(scroller.contentOffset.x / scroller.frameImagesView.frame.size.width) |
|||
scroller.trimView.layout() |
|||
} |
|||
|
|||
func endPinch() { |
|||
scroller.frameImagesView.requestVisible(depth:0, wide:1, direction:0) |
|||
|
|||
let width = snapWidth(scroller.frameImagesView.frame.size.width, max:scroller.frameImagesView.maxWidth) |
|||
|
|||
let offset = self.resizedPositionWithKeepOrigin(width:scroller.frameImagesView.frame.size.width, origin:scroller.contentOffset.x, destinationWidth:width) |
|||
//startAnimation() |
|||
|
|||
UIView.animate(withDuration: 0.1,delay:Double(0.0),options:UIView.AnimationOptions.curveEaseOut, animations: { () -> Void in |
|||
|
|||
self.scroller.setContentWidth(width, setOrigin:false) |
|||
self.scroller.contentOffset.x = offset |
|||
self.scroller.frameImagesView.layout() |
|||
self.scroller.trimView.layout() |
|||
},completion: { finished in |
|||
self.pinching = false |
|||
self.scroller.isScrollEnabled = true |
|||
}) |
|||
|
|||
self.scroller.frameImagesView.updateTolerance() |
|||
|
|||
self.centerLine.setScrollPoint(scroller.contentOffset.x / scroller.frameImagesView.frame.size.width) |
|||
|
|||
setTrimViewInteraction(true) |
|||
|
|||
guard let mView = (mainView) else { return } |
|||
if let receiver = mView.playStatusReceiver { |
|||
receiver.videoTimelineMoved() |
|||
} |
|||
} |
|||
|
|||
func resizedPositionWithKeepOrigin(width:CGFloat, origin:CGFloat, destinationWidth:CGFloat) -> CGFloat { |
|||
let originPoint = origin / width |
|||
let result = originPoint * destinationWidth |
|||
return result |
|||
} |
|||
|
|||
|
|||
} |
|||
|
|||
|
|||
|
|||
|
|||
|
|||
|
|||
|
|||
|
|||
|
|||
|
|||
|
|||
|
|||
|
|||
@ -1,773 +0,0 @@ |
|||
// |
|||
// TrimView.swift |
|||
// Examplay |
|||
// |
|||
// Created by Tomohiro Yamashita on 2020/03/12. |
|||
// Copyright © 2020 Tom. All rights reserved. |
|||
// |
|||
|
|||
import UIKit |
|||
|
|||
class TrimView: UIView { |
|||
var mainView:VideoTimelineView! |
|||
|
|||
var timelineView:TimelineView! |
|||
var parentScroller:TimelineScroller! |
|||
var startKnob = TrimKnob() |
|||
var endKnob = TrimKnob() |
|||
var movieDuration:Float64 = 0 |
|||
|
|||
let canPassThroughEachKnobs = true |
|||
|
|||
override init (frame: CGRect) { |
|||
super.init(frame: frame) |
|||
self.isUserInteractionEnabled = false |
|||
self.backgroundColor = .clear |
|||
|
|||
} |
|||
|
|||
required init(coder aDecoder: NSCoder) { |
|||
fatalError("TrimView init(coder:) has not been implemented") |
|||
} |
|||
|
|||
func configure(_ timeline:TimelineView, scroller:TimelineScroller) { |
|||
timelineView = timeline |
|||
parentScroller = scroller |
|||
self.frame = timeline.frame |
|||
startKnob.configure(timeline, trimmer:self) |
|||
endKnob.configure(timeline, trimmer:self) |
|||
|
|||
timelineView.addSubview(startKnob) |
|||
timelineView.addSubview(endKnob) |
|||
} |
|||
|
|||
|
|||
func reset(duration:Float64) { |
|||
movieDuration = duration |
|||
startKnob.knobTimePoint = 0 |
|||
endKnob.knobTimePoint = 3 |
|||
if duration < endKnob.knobTimePoint { |
|||
endKnob.knobTimePoint = duration |
|||
} |
|||
layout() |
|||
} |
|||
|
|||
|
|||
|
|||
let knobWidth:CGFloat = 20 |
|||
let knobWidthExtend:CGFloat = 5 |
|||
func layout() { |
|||
if self.isHidden { |
|||
return |
|||
} |
|||
|
|||
swapKnobs() |
|||
|
|||
let knobPositions = knobPositionsAsVisible() |
|||
let startPosition = knobPositions.start |
|||
let endPosition = knobPositions.end |
|||
|
|||
startKnob.knobPositionOnScreen = startPosition |
|||
endKnob.knobPositionOnScreen = endPosition |
|||
startKnob.isOutOfScreen = knobPositions.startFixed |
|||
endKnob.isOutOfScreen = knobPositions.endFixed |
|||
|
|||
startKnob.frame = CGRect(x:startPosition - knobWidth - knobWidthExtend, y:self.frame.origin.y, width:knobWidth + knobWidthExtend * 2, height:self.frame.size.height) |
|||
endKnob.frame = CGRect(x:endPosition - knobWidthExtend, y:self.frame.origin.y, width:knobWidth + knobWidthExtend * 2, height:self.frame.size.height) |
|||
|
|||
self.setNeedsDisplay() |
|||
} |
|||
|
|||
//MARK: - draw |
|||
|
|||
override func draw(_ rect: CGRect) { |
|||
|
|||
|
|||
var startRect = startKnob.frame |
|||
var endRect = endKnob.frame |
|||
if startRect.size.height <= 0 { |
|||
return |
|||
} |
|||
if animating { |
|||
if let layer = startKnob.layer.presentation() { |
|||
startRect = layer.frame |
|||
} |
|||
if let layer = endKnob.layer.presentation() { |
|||
endRect = layer.frame |
|||
} |
|||
} |
|||
startRect.origin.x += knobWidthExtend |
|||
startRect.size.width -= knobWidthExtend * 2 |
|||
endRect.origin.x += knobWidthExtend |
|||
endRect.size.width -= knobWidthExtend * 2 |
|||
if startRect.origin.x > endRect.origin.x + endRect.size.width { |
|||
let swapRect = startRect |
|||
startRect = endRect |
|||
endRect = swapRect |
|||
} |
|||
|
|||
|
|||
let beamWidth:CGFloat = 3 |
|||
var outerRect = CGRect(x: startRect.origin.x,y: 0,width: endRect.origin.x + endRect.size.width - startRect.origin.x,height:startRect.size.height) |
|||
var innerRect = CGRect(x:startRect.origin.x + startRect.size.width,y:beamWidth,width: endRect.origin.x - startRect.origin.x - startRect.size.width,height:startRect.size.height - (beamWidth * 2)) |
|||
|
|||
let screenLeft = cgToTime(screenToTimelinePosition(0)) |
|||
let screenRight = cgToTime(screenToTimelinePosition(timelineView.frame.size.width)) |
|||
var color = UIColor(hue: 0.1, saturation:0.8, brightness:1, alpha: 1) |
|||
let outColor = UIColor(hue: 0.1, saturation:0.8, brightness:1, alpha: 0.3) |
|||
if (endKnob.knobTimePoint < screenLeft || startKnob.knobTimePoint > screenRight) { |
|||
color = outColor |
|||
} else { |
|||
let addition = knobWidth + 10 |
|||
let knobWidthTime = cgToTime(knobWidth) |
|||
if endKnob.knobTimePoint + knobWidthTime * 0.9 > screenRight { |
|||
outerRect.size.width += addition |
|||
innerRect.size.width += addition |
|||
let outRect = CGRect(x: timelineView.frame.size.width - knobWidth, y: beamWidth,width: knobWidth, height: endRect.size.height - beamWidth * 2) |
|||
let path = UIBezierPath(rect:outRect) |
|||
outColor.setFill() |
|||
path.fill() |
|||
} |
|||
if startKnob.knobTimePoint - knobWidthTime * 0.9 < screenLeft { |
|||
outerRect.origin.x -= addition |
|||
innerRect.origin.x -= addition |
|||
outerRect.size.width += addition |
|||
innerRect.size.width += addition |
|||
let outRect = CGRect(x: 0, y: beamWidth,width: knobWidth, height: endRect.size.height - beamWidth * 2) |
|||
let path = UIBezierPath(rect:outRect) |
|||
outColor.setFill() |
|||
path.fill() |
|||
} |
|||
} |
|||
|
|||
let path = UIBezierPath(roundedRect:outerRect, cornerRadius:5) |
|||
path.usesEvenOddFillRule = true |
|||
let innerPath = UIBezierPath(roundedRect:innerRect, cornerRadius:2) |
|||
path.append(innerPath) |
|||
color.setFill() |
|||
path.fill() |
|||
} |
|||
|
|||
|
|||
|
|||
//MARK: - timer for animation |
|||
var animationTimer = Timer() |
|||
var animating = false |
|||
func startAnimation() { |
|||
animationTimer = Timer.scheduledTimer(timeInterval: 0.01, target: self, selector: #selector(self.animate(_:)), userInfo: nil, repeats: true) |
|||
RunLoop.main.add(animationTimer, forMode:RunLoop.Mode.common) |
|||
animating = true |
|||
} |
|||
|
|||
func stopAnimation() { |
|||
animating = false |
|||
self.setNeedsDisplay() |
|||
animationTimer.invalidate() |
|||
} |
|||
|
|||
@objc func animate(_ timer:Timer) { |
|||
if animating == false { |
|||
return |
|||
} |
|||
self.setNeedsDisplay() |
|||
} |
|||
|
|||
|
|||
//MARK: - positioning |
|||
func swapKnobs() { |
|||
if startKnob.knobTimePoint > endKnob.knobTimePoint { |
|||
if canPassThroughEachKnobs { |
|||
//timelineView.swapTrimKnobs() |
|||
} else { |
|||
let start = endKnob.knobTimePoint |
|||
endKnob.knobTimePoint = startKnob.knobTimePoint |
|||
startKnob.knobTimePoint = start |
|||
} |
|||
} |
|||
} |
|||
|
|||
func anotherKnob(_ knob:TrimKnob) -> TrimKnob { |
|||
if knob == startKnob { |
|||
return endKnob |
|||
} |
|||
return startKnob |
|||
} |
|||
|
|||
func knobOnScreen(_ knob:TrimKnob) -> CGFloat { |
|||
let maxWidth = scrollMaxWidth() |
|||
let offset = scrollOffset() |
|||
let position = CGFloat(knob.knobTimePoint / movieDuration) * maxWidth |
|||
return position - offset + (timelineView.frame.size.width / 2) |
|||
} |
|||
|
|||
func knobsMinDistanceTime() -> Float64 { |
|||
return Float64(0.1) |
|||
} |
|||
|
|||
func knobsMinDistanceFloat() -> CGFloat { |
|||
return timeToCG(knobsMinDistanceTime()) |
|||
} |
|||
|
|||
func timeToCG(_ time:Float64) -> CGFloat { |
|||
return CGFloat(time / movieDuration) * scrollMaxWidth() |
|||
} |
|||
|
|||
func cgToTime(_ cgFloat:CGFloat) -> Float64 { |
|||
return Float64(cgFloat / scrollMaxWidth()) * movieDuration |
|||
} |
|||
|
|||
func screenToTimelinePosition(_ onScreen:CGFloat) -> CGFloat { |
|||
return onScreen + scrollOffset() - (timelineView.frame.size.width / 2) |
|||
} |
|||
|
|||
func timelineToScreenPosition(_ position:CGFloat) -> CGFloat { |
|||
return position - scrollOffset() + (timelineView.frame.size.width / 2) |
|||
} |
|||
|
|||
func knobPositionsAsVisible() -> (start:CGFloat, end:CGFloat, startFixed:Bool, endFixed:Bool) { |
|||
|
|||
let positionStart = knobOnScreen(startKnob) |
|||
let positionEnd = knobOnScreen(endKnob) |
|||
var resultStart = positionStart |
|||
var resultEnd = positionEnd |
|||
var startFixed:Bool = false |
|||
var endFixed:Bool = false |
|||
let screenRight = timelineView.frame.size.width// + offset |
|||
let minDistance = knobsMinDistanceFloat() |
|||
|
|||
if positionStart < knobWidth { |
|||
if (positionEnd - minDistance - knobWidth) < 0 { |
|||
resultStart = positionEnd - minDistance |
|||
} else { |
|||
resultStart = knobWidth |
|||
} |
|||
startFixed = true |
|||
} else if positionStart > screenRight { |
|||
resultStart = screenRight |
|||
startFixed = true |
|||
} |
|||
if positionEnd < 0 { |
|||
resultEnd = 0 |
|||
endFixed = true |
|||
} else if positionEnd + knobWidth > screenRight { |
|||
if positionStart + minDistance + knobWidth > screenRight { |
|||
resultEnd = positionStart + minDistance |
|||
} else { |
|||
resultEnd = screenRight - knobWidth |
|||
} |
|||
} |
|||
if true { |
|||
let distance = abs(resultStart - resultEnd) |
|||
if distance < minDistance { |
|||
let value = (minDistance - distance) / 2 |
|||
resultStart -= value |
|||
resultEnd += value |
|||
startFixed = true |
|||
endFixed = true |
|||
} |
|||
} |
|||
return (resultStart, resultEnd, startFixed, endFixed) |
|||
} |
|||
|
|||
func scrollOffset() -> CGFloat { |
|||
return parentScroller.contentOffset.x |
|||
} |
|||
|
|||
func scrollMaxWidth() -> CGFloat { |
|||
return parentScroller.frameImagesView.frame.size.width |
|||
} |
|||
|
|||
func knobTimeOnScreen(_ knob:TrimKnob) -> Float64 { |
|||
let offset = scrollOffset() |
|||
let position = offset + knob.knobPositionOnScreen - (timelineView.frame.size.width / 2) |
|||
let maxWidth = scrollMaxWidth() |
|||
let time = Float64(position / maxWidth) * movieDuration |
|||
return time |
|||
} |
|||
|
|||
|
|||
func knobMoveRange(_ knob:TrimKnob) -> (min:Float64, max:Float64) { |
|||
var min:Float64 = 0 |
|||
var max:Float64 = movieDuration |
|||
let minDistance = knobsMinDistanceTime() |
|||
if canPassThroughEachKnobs { |
|||
if knob == startKnob { |
|||
max -= minDistance |
|||
} else if knob == endKnob { |
|||
min += minDistance |
|||
} |
|||
} else { |
|||
if knob == startKnob { |
|||
max = endKnob.knobTimePoint - minDistance |
|||
} else if knob == endKnob { |
|||
min = startKnob.knobTimePoint + minDistance |
|||
} |
|||
} |
|||
return (min, max) |
|||
} |
|||
|
|||
func visibleKnobMoveLimit(_ knob:TrimKnob, margin:CGFloat) -> (min:Bool, max:Bool) { |
|||
let range = knobMoveRange(knob) |
|||
let minOnScreen = timelineToScreenPosition(timeToCG(range.min)) |
|||
let maxOnScreen = timelineToScreenPosition(timeToCG(range.max)) |
|||
var resultMin = false |
|||
var resultMax = false |
|||
if minOnScreen >= margin && minOnScreen <= timelineView.frame.width - margin { |
|||
resultMin = true |
|||
} |
|||
if maxOnScreen >= margin && maxOnScreen <= timelineView.frame.width - margin { |
|||
resultMax = true |
|||
} |
|||
return (resultMin , resultMax ) |
|||
} |
|||
|
|||
func directionReachesEnd(_ knob:TrimKnob, direction:CGFloat) -> Bool { |
|||
let visibleLimit = visibleKnobMoveLimit(knob, margin:knobWidth) |
|||
if direction > 0 { |
|||
if visibleLimit.max { |
|||
return true |
|||
} |
|||
} else if direction < 0 { |
|||
if visibleLimit.min { |
|||
return true |
|||
} |
|||
} |
|||
return false |
|||
} |
|||
|
|||
func fixKnobPoint(_ knob:TrimKnob, move:CGFloat, startKnobPoint:Float64) -> (knobPoint:Float64, fixed:Bool) { |
|||
|
|||
var timePoint = startKnobPoint + cgToTime(move) |
|||
|
|||
let moveRange = knobMoveRange(knob) |
|||
var fixed = false |
|||
if timePoint < moveRange.min { |
|||
timePoint = moveRange.min |
|||
fixed = true |
|||
} |
|||
if timePoint > moveRange.max { |
|||
timePoint = moveRange.max |
|||
fixed = true |
|||
} |
|||
return (timePoint, fixed) |
|||
} |
|||
|
|||
func updateKnob(_ knob:TrimKnob, timePoint:Float64) { |
|||
knob.knobTimePoint = timePoint |
|||
layout() |
|||
|
|||
timelineView.moved(knob.knobTimePoint) |
|||
} |
|||
|
|||
func resetSeek(_ time:Float64) { |
|||
if edgeScrolled { |
|||
moveToTimeWithAnimation(time) |
|||
} else { |
|||
mainView!.accurateSeek(time, scrub:true) |
|||
} |
|||
edgeScrolled = false |
|||
} |
|||
|
|||
func moveToTimeWithAnimation(_ time:Float64) { |
|||
moveToTimeAndTrimWithAnimation(time, trim:nil) |
|||
} |
|||
|
|||
func moveToTimeAndTrimWithAnimation(_ time:Float64, trim:VideoTimelineTrim?) { |
|||
let player = mainView!.player |
|||
if player == nil { |
|||
return |
|||
} |
|||
if player!.timeControlStatus == .playing { |
|||
player!.pause() |
|||
} |
|||
timelineView.animating = true |
|||
mainView!.isUserInteractionEnabled = false |
|||
timelineView.startAnimation() |
|||
|
|||
UIView.animate(withDuration: 0.2,delay:Double(0.0),options:UIView.AnimationOptions.curveEaseOut, animations: { () -> Void in |
|||
if let pinnedTrim = trim { |
|||
self.timelineView.setTrim(start:pinnedTrim.start,end:pinnedTrim.end) |
|||
} |
|||
self.timelineView.setCurrentTime(time,force:false) |
|||
},completion: { finished in |
|||
|
|||
self.mainView!.isUserInteractionEnabled = true |
|||
if self.mainView!.playing { |
|||
self.mainView!.accurateSeek(time, scrub:false) |
|||
player!.play() |
|||
} else { |
|||
self.mainView!.accurateSeek(time, scrub:true) |
|||
} |
|||
self.timelineView.animating = false |
|||
self.timelineView.setManualScrolledAfterEnd() |
|||
self.timelineView.stopAnimation() |
|||
if let receiver = self.mainView!.playStatusReceiver { |
|||
receiver.videoTimelineMoved() |
|||
} |
|||
}) |
|||
} |
|||
|
|||
//MARK: - edgeScroll |
|||
var edgeScrollTimer = Timer() |
|||
var edgeScrolled = false |
|||
var edgeScrollingKnob:TrimKnob? = nil |
|||
var edgeScrollStrength:CGFloat = 0 |
|||
var edgeScrollingKnobPosition:CGFloat = 0 |
|||
var edgeScrollLastChangedTime:Date = Date() |
|||
var edgeScrollLastChangedPosition:CGFloat = 0 |
|||
|
|||
|
|||
func updateEdgeScroll(_ knob:TrimKnob, strength:CGFloat, position:CGFloat) { |
|||
var changed = false |
|||
if edgeScrollStrength != strength { |
|||
edgeScrollStrength = strength |
|||
changed = true |
|||
} |
|||
if edgeScrolling() == false { |
|||
startEdgeScroll(knob) |
|||
edgeScrolled = true |
|||
} else if changed { |
|||
edgeScrollLastChangedPosition = scrollOffset() |
|||
edgeScrollLastChangedTime = Date() |
|||
} |
|||
edgeScrollingKnobPosition = position |
|||
edgeScrollingKnob = knob |
|||
} |
|||
|
|||
func startEdgeScroll(_ knob:TrimKnob) { |
|||
edgeScrollTimer = Timer.scheduledTimer(timeInterval: 0.01, target: self, selector: #selector(self.edgeScrollTimer(_:)), userInfo: nil, repeats: true) |
|||
RunLoop.main.add(edgeScrollTimer, forMode:RunLoop.Mode.common) |
|||
edgeScrollLastChangedTime = Date() |
|||
edgeScrollLastChangedPosition = scrollOffset() |
|||
} |
|||
|
|||
@objc func edgeScrollTimer(_ timer:Timer) { |
|||
if let knob = edgeScrollingKnob { |
|||
let movedPosition = currentEdgeScrollMovedPosition() |
|||
let destination = currentEdgeScrollPosition(movedPosition) |
|||
let moveRange = knobMoveRange(knob) |
|||
var knobPoint = cgToTime(edgeScrollingKnobPosition - (timelineView.frame.size.width / 2) + destination) |
|||
|
|||
var overLimit:Float64 = 0 |
|||
if knobPoint > moveRange.max { |
|||
overLimit = knobPoint - moveRange.max |
|||
knobPoint = moveRange.max |
|||
|
|||
} else if knobPoint < moveRange.min { |
|||
overLimit = knobPoint - moveRange.min |
|||
knobPoint = moveRange.min |
|||
|
|||
} |
|||
updateKnob(knob, timePoint:knobPoint) |
|||
if (knob == startKnob && knobPoint > endKnob.knobTimePoint) || (knob == endKnob && knobPoint < startKnob.knobTimePoint) { |
|||
timelineView.swapTrimKnobs() |
|||
} |
|||
timelineView.setCurrentTime(cgToTime(destination) - overLimit,force:true) |
|||
} |
|||
} |
|||
|
|||
func stopEdgeScrollTimer() { |
|||
edgeScrollTimer.invalidate() |
|||
edgeScrollStrength = 0 |
|||
edgeScrollingKnob = nil |
|||
} |
|||
|
|||
func edgeScrolling() -> Bool { |
|||
return edgeScrollTimer.isValid |
|||
} |
|||
|
|||
func currentEdgeScrollPosition(_ moved:CGFloat) -> CGFloat { |
|||
var result:CGFloat = 0 |
|||
|
|||
let maxWidth = scrollMaxWidth() |
|||
result = edgeScrollLastChangedPosition + moved |
|||
if result < 0 { |
|||
result = 0 |
|||
} else if result > maxWidth { |
|||
result = maxWidth |
|||
} |
|||
return result |
|||
} |
|||
|
|||
func currentEdgeScrollMovedPosition() -> CGFloat { |
|||
let pastTime = -edgeScrollLastChangedTime.timeIntervalSinceNow |
|||
return CGFloat(pastTime) * edgeScrollStrength * 5 |
|||
} |
|||
} |
|||
|
|||
|
|||
//MARK: - TrimKnob |
|||
class TrimKnob:UIView { |
|||
var timelineView:TimelineView! |
|||
var knobPositionOnScreen:CGFloat = 0 |
|||
var trimView:TrimView! |
|||
var knobTimePoint:Float64 = 0 |
|||
var isOutOfScreen:Bool = false |
|||
|
|||
|
|||
override init (frame: CGRect) { |
|||
super.init(frame: frame) |
|||
|
|||
self.isMultipleTouchEnabled = true |
|||
self.isUserInteractionEnabled = true |
|||
|
|||
} |
|||
|
|||
required init(coder aDecoder: NSCoder) { |
|||
fatalError("TrimKnob init(coder:) has not been implemented") |
|||
} |
|||
|
|||
func configure(_ timeline:TimelineView, trimmer:TrimView) { |
|||
timelineView = timeline |
|||
trimView = trimmer |
|||
} |
|||
|
|||
//MARK: - Touch Events |
|||
var allTouches = [UITouch]() |
|||
|
|||
override open func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) |
|||
{ |
|||
for touch in touches { |
|||
if !allTouches.contains(touch) { |
|||
allTouches += [touch] |
|||
} |
|||
if !timelineView!.allTouches.contains(touch) { |
|||
timelineView!.allTouches += [touch] |
|||
} |
|||
} |
|||
if timelineView!.allTouches.count == 1 && allTouches.count == 1 { |
|||
if dragging == false { |
|||
startDrag() |
|||
} |
|||
evaluateTap = true |
|||
} else { |
|||
evaluateTap = false |
|||
} |
|||
} |
|||
|
|||
|
|||
override open func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) |
|||
{ |
|||
if timelineView!.allTouches.count > 1 { |
|||
if dragging { |
|||
cancelDrag() |
|||
} |
|||
} |
|||
if timelineView!.allTouches.count == 2 { |
|||
if timelineView!.pinching { |
|||
timelineView!.updatePinch() |
|||
} else { |
|||
timelineView!.startPinch() |
|||
} |
|||
} |
|||
if dragging && timelineView!.allTouches.count == 1 && allTouches.count == 1 { |
|||
updateDrag() |
|||
} |
|||
evaluateTap = false |
|||
} |
|||
|
|||
override open func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) |
|||
{ |
|||
for touch in touches { |
|||
if let index = allTouches.firstIndex(of:touch) { |
|||
allTouches.remove(at: index) |
|||
} |
|||
if let index = timelineView!.allTouches.firstIndex(of:touch) { |
|||
timelineView!.allTouches.remove(at: index) |
|||
} |
|||
} |
|||
if timelineView!.pinching && timelineView!.allTouches.count < 2 { |
|||
timelineView!.endPinch() |
|||
} |
|||
if dragging { |
|||
endDrag() |
|||
} |
|||
|
|||
if evaluateTap && timelineView!.allTouches.count == 0 { |
|||
tapped() |
|||
} |
|||
evaluateTap = false |
|||
} |
|||
|
|||
override open func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) |
|||
{ |
|||
for touch in touches { |
|||
if let index = allTouches.firstIndex(of:touch) { |
|||
allTouches.remove(at: index) |
|||
} |
|||
if let index = timelineView!.allTouches.firstIndex(of:touch) { |
|||
timelineView!.allTouches.remove(at: index) |
|||
} |
|||
} |
|||
|
|||
if timelineView!.pinching { |
|||
timelineView!.endPinch() |
|||
} |
|||
if dragging { |
|||
endDrag() |
|||
} |
|||
evaluateTap = false |
|||
} |
|||
|
|||
|
|||
//MARK: - actions |
|||
|
|||
var dragging:Bool = false |
|||
var dragStartPoint = CGPoint.zero |
|||
var startKnobTimePoint:Float64 = 0 |
|||
var dragStartOffset:CGFloat = 0 |
|||
var startTimeOutOfScreen:Float64 = 0 |
|||
var scrolling = false |
|||
var startCurrentTime:Float64 = 0 |
|||
var evaluateTap:Bool = false |
|||
var ignoreEdgeScroll = false |
|||
|
|||
func startDrag() { |
|||
|
|||
dragging = true |
|||
let touch = allTouches[0] |
|||
dragStartPoint = touch.location(in: timelineView) |
|||
startKnobTimePoint = knobTimePoint |
|||
dragStartOffset = trimView.scrollOffset() |
|||
startTimeOutOfScreen = trimView.knobTimeOnScreen(self) - startKnobTimePoint |
|||
|
|||
|
|||
startCurrentTime = timelineView.mainView!.currentTime |
|||
|
|||
if edgeScrollStrength(dragStartPoint.x) != 0 { |
|||
ignoreEdgeScroll = true |
|||
} else { |
|||
ignoreEdgeScroll = false |
|||
} |
|||
} |
|||
|
|||
func updateDrag() { |
|||
let touch = allTouches[0] |
|||
let currentPoint = touch.location(in: timelineView) |
|||
let scrolled = trimView.scrollOffset() - dragStartOffset |
|||
let move = currentPoint.x - dragStartPoint.x + scrolled |
|||
let startKnobPoint = startKnobTimePoint + startTimeOutOfScreen |
|||
|
|||
let timePoint = startKnobPoint + trimView.cgToTime(move) |
|||
let dragPoint = trimView.cgToTime(trimView.screenToTimelinePosition(currentPoint.x)) |
|||
let onKnob = trimView.timeToCG(timePoint - dragPoint) |
|||
let anotherKnob = trimView.anotherKnob(self) |
|||
let anotherTimePoint = anotherKnob.knobTimePoint |
|||
let knobWidth = trimView.knobWidth |
|||
var swapping:CGFloat = 0 |
|||
|
|||
|
|||
if anotherTimePoint < timePoint && startKnobPoint < anotherTimePoint |
|||
{ |
|||
swapping = trimView.timeToCG(anotherTimePoint - timePoint) |
|||
if -swapping > knobWidth { |
|||
swapping = -knobWidth |
|||
} |
|||
} else if anotherTimePoint > timePoint && startKnobPoint > anotherTimePoint |
|||
{ |
|||
swapping = trimView.timeToCG(anotherTimePoint - timePoint) |
|||
if swapping > knobWidth { |
|||
swapping = knobWidth |
|||
} |
|||
} |
|||
var rangeOut = false |
|||
let range = trimView.knobMoveRange(self) |
|||
if timePoint > range.max || timePoint < range.min { |
|||
rangeOut = true |
|||
} |
|||
|
|||
if rangeOut == false && ((self == trimView.startKnob && dragPoint > anotherTimePoint) || (self == trimView.endKnob && dragPoint < anotherTimePoint)) { |
|||
timelineView.swapTrimKnobs() |
|||
} |
|||
|
|||
|
|||
let strength = edgeScrollStrength(currentPoint.x) |
|||
let reachedEnd = trimView.directionReachesEnd(self, direction:strength) |
|||
if strength != 0 && ignoreEdgeScroll == false && reachedEnd == false { |
|||
trimView.updateEdgeScroll(self, strength:strength, position:currentPoint.x + onKnob + swapping) |
|||
} else { |
|||
let fixedKnobPoint = trimView.fixKnobPoint(self, move:move + swapping, startKnobPoint:startKnobPoint) |
|||
|
|||
trimView.updateKnob(self, timePoint:fixedKnobPoint.knobPoint) |
|||
if strength == 0 { |
|||
ignoreEdgeScroll = false |
|||
|
|||
} |
|||
if strength == 0 || reachedEnd { |
|||
if trimView.edgeScrolling() { |
|||
trimView.stopEdgeScrollTimer() |
|||
} |
|||
} |
|||
} |
|||
|
|||
guard let mainView = (timelineView.mainView) else { return } |
|||
if let receiver = mainView.playStatusReceiver { |
|||
receiver.videoTimelineTrimChanged() |
|||
} |
|||
} |
|||
|
|||
func endDrag() { |
|||
let anotherKnob = trimView.anotherKnob(self) |
|||
let distance = abs(anotherKnob.knobTimePoint - knobTimePoint) |
|||
let minDistance = trimView.knobsMinDistanceTime() |
|||
if distance < minDistance { |
|||
if self == trimView.startKnob { |
|||
knobTimePoint -= (minDistance - distance) |
|||
} else if self == trimView.endKnob { |
|||
knobTimePoint += (minDistance - distance) |
|||
} |
|||
|
|||
trimView.timelineView.animating = true |
|||
trimView.startAnimation() |
|||
UIView.animate(withDuration: 0.2,delay:Double(0.0),options:UIView.AnimationOptions.curveEaseOut, animations: { () -> Void in |
|||
|
|||
self.trimView.layout() |
|||
|
|||
},completion: { finished in |
|||
self.trimView.stopAnimation() |
|||
self.timelineView.animating = false |
|||
}) |
|||
} |
|||
timelineView.setManualScrolledAfterEnd() |
|||
trimView.resetSeek(startCurrentTime) |
|||
dragging = false |
|||
trimView.stopEdgeScrollTimer() |
|||
|
|||
if evaluateTap == false { |
|||
guard let mainView = (timelineView.mainView) else { return } |
|||
if let receiver = mainView.playStatusReceiver { |
|||
receiver.videoTimelineTrimChanged() |
|||
} |
|||
} |
|||
} |
|||
|
|||
func cancelDrag() { |
|||
knobTimePoint = startKnobTimePoint |
|||
|
|||
timelineView.setManualScrolledAfterEnd() |
|||
dragging = false |
|||
trimView.layout() |
|||
trimView.resetSeek(startCurrentTime) |
|||
trimView.stopEdgeScrollTimer() |
|||
} |
|||
|
|||
func tapped() { |
|||
trimView.moveToTimeWithAnimation(knobTimePoint) |
|||
timelineView.setManualScrolledAfterEnd() |
|||
} |
|||
|
|||
|
|||
func edgeScrollStrength(_ position:CGFloat) -> CGFloat { |
|||
var strength:CGFloat = 0 |
|||
let edgeWidth:CGFloat = 40 |
|||
if position >= timelineView.frame.size.width - edgeWidth { |
|||
strength = position + edgeWidth - timelineView.frame.size.width |
|||
} else if position <= edgeWidth { |
|||
strength = position - edgeWidth |
|||
} |
|||
return strength |
|||
} |
|||
|
|||
|
|||
|
|||
} |
|||
@ -1,301 +0,0 @@ |
|||
// |
|||
// VideoTimelineView.swift |
|||
// VideoTimelineView |
|||
// |
|||
// Created by Tomohiro Yamashita on 2020/03/28. |
|||
// Copyright © 2020 Tom. All rights reserved. |
|||
// |
|||
|
|||
import UIKit |
|||
import AVFoundation |
|||
|
|||
protocol TimelinePlayStatusReceiver: class { |
|||
func videoTimelineStopped() |
|||
func videoTimelineMoved() |
|||
func videoTimelineTrimChanged() |
|||
} |
|||
|
|||
|
|||
struct VideoTimelineTrim { |
|||
var start:Float64 |
|||
var end:Float64 |
|||
} |
|||
|
|||
class VideoTimelineView: UIView { |
|||
|
|||
public private(set) var asset:AVAsset? = nil |
|||
var player:AVPlayer? = nil |
|||
|
|||
weak var playStatusReceiver:TimelinePlayStatusReceiver? = nil |
|||
|
|||
var repeatOn:Bool = false |
|||
|
|||
public private(set) var trimEnabled:Bool = false |
|||
|
|||
|
|||
|
|||
var currentTime:Float64 = 0 |
|||
public private(set) var duration:Float64 = 0 |
|||
|
|||
public private(set) var audioPlayer:AVPlayer! |
|||
public private(set) var audioPlayer2:AVPlayer! |
|||
|
|||
let timelineView = TimelineView() |
|||
|
|||
override init (frame: CGRect) { |
|||
super.init(frame: frame) |
|||
timelineView.mainView = self |
|||
timelineView.centerLine.mainView = self |
|||
timelineView.scroller.frameImagesView.mainView = self |
|||
timelineView.scroller.trimView.mainView = self |
|||
|
|||
self.addSubview(timelineView) |
|||
} |
|||
|
|||
required init(coder aDecoder: NSCoder) { |
|||
fatalError("MainView init(coder:) has not been implemented") |
|||
} |
|||
|
|||
|
|||
func viewDidLayoutSubviews() { |
|||
coordinate() |
|||
} |
|||
|
|||
func coordinate() { |
|||
timelineView.coordinate() |
|||
} |
|||
|
|||
func new(asset newAsset:AVAsset?) { |
|||
if let new = newAsset { |
|||
asset = new |
|||
duration = CMTimeGetSeconds(new.duration) |
|||
player = AVPlayer(playerItem: AVPlayerItem(asset: asset!)) |
|||
audioPlayer = AVPlayer(playerItem: AVPlayerItem(asset: asset!)) |
|||
audioPlayer.volume = 1.0 |
|||
audioPlayer2 = AVPlayer(playerItem: AVPlayerItem(asset: asset!)) |
|||
audioPlayer2.volume = 1.0 |
|||
timelineView.newMovieSet() |
|||
} |
|||
} |
|||
|
|||
|
|||
func setTrim(start:Float64, end:Float64, seek:Float64?, animate:Bool) { |
|||
|
|||
var seekTime = currentTime |
|||
if let time = seek { |
|||
seekTime = time |
|||
} |
|||
if animate { |
|||
timelineView.setTrimWithAnimation(trim:VideoTimelineTrim(start:start, end:end), time:seekTime) |
|||
} else { |
|||
timelineView.setTrim(start:start, end:end) |
|||
if seek != nil { |
|||
moveTo(seek!, animate:animate) |
|||
} |
|||
} |
|||
} |
|||
|
|||
func setTrimIsEnabled(_ enabled:Bool) { |
|||
trimEnabled = enabled |
|||
timelineView.setTrimmerStatus(enabled:enabled) |
|||
} |
|||
|
|||
func setTrimmerIsHidden(_ hide:Bool) { |
|||
timelineView.setTrimmerVisible(!hide) |
|||
} |
|||
|
|||
func currentTrim() -> (start:Float64, end:Float64) { |
|||
let trim = timelineView.currentTrim() |
|||
return (trim.start,trim.end) |
|||
} |
|||
|
|||
func moveTo(_ time:Float64, animate:Bool) { |
|||
if animate { |
|||
|
|||
} else { |
|||
accurateSeek(time, scrub:false) |
|||
timelineView.setCurrentTime(time, force:true) |
|||
} |
|||
} |
|||
|
|||
//MARK: - seeking |
|||
var previousSeektime:Float64 = 0 |
|||
func timelineIsMoved(_ currentTime:Float64, scrub:Bool) { |
|||
let move = abs(currentTime - previousSeektime) |
|||
let seekTolerance = CMTimeMakeWithSeconds(move, preferredTimescale:100) |
|||
|
|||
if player != nil { |
|||
player!.seek(to:CMTimeMakeWithSeconds(currentTime , preferredTimescale:100), toleranceBefore:seekTolerance,toleranceAfter:seekTolerance) |
|||
} |
|||
previousSeektime = currentTime |
|||
if scrub { |
|||
audioScrub() |
|||
} |
|||
} |
|||
|
|||
func accurateSeek(_ currentTime:Float64, scrub:Bool) { |
|||
previousSeektime = currentTime |
|||
timelineIsMoved(currentTime, scrub:scrub) |
|||
} |
|||
|
|||
var scrubed1 = Date() |
|||
var scrubed2 = Date() |
|||
var canScrub1 = true |
|||
var canScrub2 = true |
|||
func audioScrub() { |
|||
if player == nil { |
|||
return |
|||
} |
|||
if scrubed2.timeIntervalSinceNow < -0.16 && canScrub1 { |
|||
canScrub1 = false |
|||
self.scrubed1 = Date() |
|||
DispatchQueue.main.async { |
|||
if self.audioPlayer.timeControlStatus == .playing { |
|||
self.audioPlayer.pause() |
|||
self.canScrub1 = true |
|||
} else { |
|||
self.audioPlayer.seek(to: self.player!.currentTime()) |
|||
self.audioPlayer.play() |
|||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.22) { |
|||
self.audioPlayer.pause() |
|||
self.audioPlayer.seek(to: self.player!.currentTime()) |
|||
self.canScrub1 = true |
|||
} |
|||
} |
|||
|
|||
} |
|||
} |
|||
if scrubed1.timeIntervalSinceNow < -0.16 && canScrub2 { |
|||
canScrub2 = false |
|||
self.scrubed2 = Date() |
|||
DispatchQueue.main.async { |
|||
if self.audioPlayer2.timeControlStatus == .playing { |
|||
self.audioPlayer2.pause() |
|||
self.canScrub2 = true |
|||
} else { |
|||
self.audioPlayer2.seek(to: self.player!.currentTime()) |
|||
self.audioPlayer2.play() |
|||
|
|||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.22) { |
|||
self.audioPlayer2.pause() |
|||
self.audioPlayer2.seek(to: self.player!.currentTime()) |
|||
self.canScrub2 = true |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
|
|||
//MARK: - play |
|||
var playerTimer = Timer() |
|||
@objc dynamic var playing = false |
|||
func play() { |
|||
if asset == nil { |
|||
return |
|||
} |
|||
let currentTime = timelineView.centerLine.currentTime |
|||
let reached = timeReachesEnd(currentTime) |
|||
|
|||
if reached.trimEnd { |
|||
accurateSeek(timelineView.currentTrim().start, scrub:false) |
|||
timelineView.manualScrolledAfterEnd = false |
|||
} else if reached.movieEnd { |
|||
accurateSeek(0, scrub:false) |
|||
timelineView.manualScrolledAfterEnd = false |
|||
} |
|||
if player != nil { |
|||
player!.play() |
|||
} |
|||
playerTimer = Timer.scheduledTimer(timeInterval: 0.01, target: self, selector: #selector(self.playerTimerAction(_:)), userInfo: nil, repeats: true) |
|||
RunLoop.main.add(playerTimer, forMode:RunLoop.Mode.common) |
|||
playing = true |
|||
} |
|||
|
|||
func stop() { |
|||
playing = false |
|||
if asset == nil || player == nil { |
|||
return |
|||
} |
|||
player!.pause() |
|||
playerTimer.invalidate() |
|||
|
|||
if let receiver = playStatusReceiver { |
|||
receiver.videoTimelineStopped() |
|||
} |
|||
} |
|||
|
|||
var reachFlg = false |
|||
@objc func playerTimerAction(_ timer:Timer) { |
|||
if player == nil { |
|||
return |
|||
} |
|||
var currentPlayerTime = CMTimeGetSeconds(player!.currentTime()) |
|||
|
|||
let trim = timelineView.currentTrim() |
|||
let reached = timeReachesEnd(currentPlayerTime) |
|||
if timelineView.inAction() { |
|||
if player!.timeControlStatus == .playing { |
|||
player!.pause() |
|||
} |
|||
} else if reached.reached { |
|||
if repeatOn && reached.trimEnd { |
|||
|
|||
if player!.timeControlStatus == .playing { |
|||
player!.pause() |
|||
} |
|||
currentPlayerTime = trim.start |
|||
accurateSeek(currentPlayerTime, scrub:false) |
|||
reachFlg = true |
|||
|
|||
} else { |
|||
stop() |
|||
} |
|||
timelineView.setCurrentTime(currentPlayerTime,force:false) |
|||
timelineView.manualScrolledAfterEnd = false |
|||
} else if timelineView.animating == false { |
|||
timelineView.setCurrentTime(currentPlayerTime,force:false) |
|||
if player!.timeControlStatus == .paused { |
|||
player!.play() |
|||
} |
|||
if reachFlg { |
|||
if let receiver = playStatusReceiver { |
|||
receiver.videoTimelineMoved() |
|||
} |
|||
reachFlg = false |
|||
} |
|||
} |
|||
} |
|||
|
|||
func timeReachesEnd(_ time:Float64) -> (reached:Bool, trimEnd:Bool, movieEnd:Bool) { |
|||
var reached = false |
|||
var trimEnd = false |
|||
var movieEnd = false |
|||
if asset != nil { |
|||
let duration = CMTimeGetSeconds(asset!.duration) |
|||
let trimTimeEnd = timelineView.currentTrim().end |
|||
if (time >= trimTimeEnd && timelineView.manualScrolledAfterEnd == false && trimEnabled) { |
|||
trimEnd = true |
|||
reached = true |
|||
} |
|||
if time >= duration { |
|||
if trimTimeEnd < duration { |
|||
trimEnd = false |
|||
} |
|||
movieEnd = true |
|||
reached = true |
|||
} |
|||
} |
|||
return (reached, trimEnd, movieEnd) |
|||
} |
|||
|
|||
|
|||
//MARK: - |
|||
func resizeHeightKeepRatio(_ size:CGSize, height:CGFloat) -> CGSize { |
|||
var result = size |
|||
let ratio = size.width / size.height |
|||
result.height = height |
|||
result.width = height * ratio |
|||
return result |
|||
} |
|||
} |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue