PQStuckPointCuttingView.swift 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458
  1. //
  2. // PQStuckPointCuttingView.swift
  3. // PQSpeed
  4. //
  5. // Created by SanW on 2021/5/8.
  6. // Copyright © 2021 BytesFlow. All rights reserved.
  7. //
  8. import UIKit
  9. import BFCommonKit
  10. class PQStuckPointCuttingView: UIView {
  11. // 视频时长
  12. var videoDuration: CGFloat = 0
  13. var lastVideoDuration:CGFloat = 0
  14. // 卡点开始时间 默认 0
  15. var stuckPointStartTime: CGFloat = 0
  16. // 卡点结束时间
  17. var stuckPointEndTime: CGFloat = 0
  18. // 裁剪开始时间 默认 0
  19. private var cutStartTime: CGFloat = 0
  20. // /// 裁剪结束最终的开始时间
  21. // private var cutFinishedStartTime: CGFloat {
  22. // (scrollView.contentOffset.x / perSecondWidth) + (((scrollView.frame.width - videoCropView.frame.width) / 2 + 15) / perSecondWidth) + cutStartTime
  23. // }
  24. //
  25. // /// 裁剪结束最终的结束时间
  26. // private var cutFinishedEndTime: CGFloat {
  27. // cutFinishedStartTime + (cutEndTime - cutStartTime)
  28. // }
  29. // 播放进度
  30. private var videoProgress: CGFloat = 0
  31. // 最小时长 默认 10s
  32. private var minCutTime: CGFloat = 10
  33. /// 时间间隔
  34. private var timeRange: CGFloat = cDefaultMargin
  35. /// 刻度高
  36. private var rateHeight: CGFloat = 23
  37. /// 时间线宽
  38. private var timeLineWidth: CGFloat = 35
  39. /// 时间线间隔
  40. private var timeLineMargin: CGFloat = 35
  41. /// 时间线高
  42. private var timeHeight: CGFloat = cDefaultMargin * 4
  43. /// 频率线宽
  44. private var frequencyWidth: CGFloat = adapterWidth(width: 1.5)
  45. /// 频率间隔
  46. private var frequencyMargin: CGFloat = adapterWidth(width: 3)
  47. /// 竖线和contentview 父视图的左右间隔
  48. private var margin: CGFloat = (cScreenWidth - adapterWidth(width: 250)) / 2
  49. /// 滑动区域大小
  50. private var contentWidth: CGFloat = 0
  51. // 竖线一个间隔代表多少 S 是动态的
  52. private var oneMarginTime: CGFloat = 0
  53. private var isDrawLine: Bool = false
  54. // 保存已经绘制的竖线用于变色使用
  55. var lineLayerArray: Array = Array<CAShapeLayer>.init()
  56. var lastDrawedLineIndex : Int = 0
  57. // 裁剪区的相素大小
  58. var cropViewWidth: CGFloat = adapterWidth(width: 250)
  59. /// 拖拽改变实时的回调
  60. var videoRangeDidChanged: ((_ startTime: CGFloat, _ endTime: CGFloat) -> Void)?
  61. /// 进度改变实时的回调
  62. var videoProgressDidChanged: ((_ progress: CGFloat) -> Void)?
  63. /// 拖缀结束的回调 type - 1-拖动左边裁剪结束 2--拖动右边裁剪结束 3-进度条拖动结束 4-滑动结束
  64. var videoDidEndDragging: ((_ type: Int, _ startTime: CGFloat, _ endTime: CGFloat, _ progress: CGFloat) -> Void)?
  65. // 开始划动
  66. var videoDidBeginDrag: (() -> Void)?
  67. // 选择区内的线个数
  68. var wavSelectCount: Int = 0
  69. // 整首歌的线的个数
  70. var wavTotalCount: Int = 0
  71. //推荐虚线的位置
  72. var startLineX:CGFloat = 0.0
  73. //如果是用户主动划动的 就不自动滚动到推荐位置了
  74. var isUserDrag:Bool = false
  75. // 推荐卡点起始时间
  76. var suggestRhythmStartTime:CGFloat = 0.0
  77. var suggestRhythmEndTime:CGFloat = 0.0
  78. /// 滚动视图
  79. lazy var scrollView: UIScrollView = {
  80. let scrollView = UIScrollView(frame: bounds)
  81. scrollView.showsVerticalScrollIndicator = false
  82. scrollView.showsHorizontalScrollIndicator = false
  83. scrollView.bounces = false
  84. scrollView.delegate = self
  85. if #available(iOS 11.0, *) {
  86. scrollView.contentInsetAdjustmentBehavior = .never
  87. } else {
  88. // automaticallyAdjustsScrollViewInsets = false
  89. }
  90. scrollView.backgroundColor = .clear
  91. return scrollView
  92. }()
  93. //
  94. lazy var rateView: UIView = {
  95. let rateView = UIView(frame: CGRect(x: 0, y: 22, width: scrollView.contentSize.width, height: rateHeight))
  96. rateView.backgroundColor = .clear
  97. return rateView
  98. }()
  99. // 总时长
  100. lazy var tatalTimeLabel: UILabel = {
  101. let tatalTimeLabel = UILabel()
  102. tatalTimeLabel.font = UIFont.systemFont(ofSize: 11)
  103. tatalTimeLabel.textAlignment = .right
  104. tatalTimeLabel.tag = 66
  105. tatalTimeLabel.textColor = UIColor.hexColor(hexadecimal: PQBFConfig.shared.styleColor.rawValue)
  106. return tatalTimeLabel
  107. }()
  108. // 显示选择框
  109. lazy var videoCropView: UIView = {
  110. let videoCropView: UIView = UIView(frame: CGRect(x: (cScreenWidth - cropViewWidth) / 2, y: 0, width: cropViewWidth, height: 80))
  111. videoCropView.isUserInteractionEnabled = false
  112. videoCropView.layer.borderColor = UIColor.hexColor(hexadecimal: PQBFConfig.shared.styleColor.rawValue).cgColor
  113. videoCropView.layer.borderWidth = 2
  114. videoCropView.layer.cornerRadius = 8
  115. return videoCropView
  116. }()
  117. //两边的mask 2 是裁剪区的边框
  118. lazy var leftMaskView: UIView = {
  119. let leftMaskView: UIView = UIView(frame: CGRect(x:0, y: 0, width: (cScreenWidth - cropViewWidth) / 2 - 2, height: 80))
  120. leftMaskView.backgroundColor = UIColor.white
  121. leftMaskView.alpha = 0.7
  122. return leftMaskView
  123. }()
  124. //右边的mask 2 是裁剪区的边框
  125. lazy var rightMaskView: UIView = {
  126. let rightMaskView: UIView = UIView(frame: CGRect(x:videoCropView.frame.maxX + 2, y: 0, width: (cScreenWidth - cropViewWidth) / 2, height: 80))
  127. rightMaskView.backgroundColor = UIColor.white
  128. rightMaskView.alpha = 0.7
  129. return rightMaskView
  130. }()
  131. private override init(frame: CGRect) {
  132. super.init(frame: frame)
  133. }
  134. required init?(coder _: NSCoder) {
  135. fatalError("init(coder:) has not been implemented")
  136. }
  137. init(frame: CGRect, duration: CGFloat, suggestRhythmStartTime: CGFloat) {
  138. super.init(frame: frame)
  139. videoDuration = duration
  140. self.suggestRhythmStartTime = suggestRhythmStartTime
  141. }
  142. /// 更新卡点值
  143. /// - Parameter endTime: endTime description
  144. /// - Returns: <#description#>
  145. func updateEndTime(startTime: CGFloat, endTime: CGFloat,
  146. suggestRhythmStartTime: CGFloat, suggestRhythmEndTime: CGFloat) {
  147. // videoDuration = duration
  148. self.suggestRhythmStartTime = suggestRhythmStartTime
  149. self.suggestRhythmEndTime = suggestRhythmEndTime
  150. startLineX = 0
  151. stuckPointStartTime = startTime
  152. stuckPointEndTime = endTime
  153. tatalTimeLabel.text = "\(Float64(stuckPointEndTime - stuckPointStartTime).formatDurationToHMS())"
  154. BFLog(1, message: "播放开始:\(stuckPointStartTime) 结束:\(stuckPointEndTime) 时长为:\(stuckPointEndTime - stuckPointStartTime); 音乐总时长为:\(videoDuration);推荐卡点开始:\(suggestRhythmStartTime) 结束:\(suggestRhythmEndTime)")
  155. backgroundColor = PQBFConfig.shared.styleBackGroundColor
  156. addSubview(scrollView)
  157. addSubview(videoCropView)
  158. addSubview(leftMaskView)
  159. addSubview(rightMaskView)
  160. videoCropView.addSubview(tatalTimeLabel)
  161. addData()
  162. videoCropView.frame = CGRect(x: (cScreenWidth - cropViewWidth) / 2, y: 0, width: cropViewWidth, height: 80)
  163. leftMaskView.frame = CGRect(x:0, y: 0, width: (cScreenWidth - cropViewWidth) / 2 - 2, height: 80)
  164. rightMaskView.frame = CGRect(x:videoCropView.frame.maxX + 2, y: 0, width: (cScreenWidth - cropViewWidth) / 2 - 2, height: 80)
  165. tatalTimeLabel.snp.remakeConstraints { make in
  166. make.width.equalTo(40)
  167. make.height.equalTo(15)
  168. make.top.equalTo(videoCropView.snp_top).offset(6)
  169. make.right.equalTo(videoCropView.snp_right).offset(-6)
  170. }
  171. }
  172. func addData() {
  173. // 1,选择区内的线个数 ,划动区域后 个数会变???
  174. wavSelectCount = Int(ceil((adapterWidth(width: 250) - frequencyWidth) / (frequencyWidth + frequencyMargin)) + 1)
  175. cropViewWidth = CGFloat(wavSelectCount) * (frequencyWidth + frequencyMargin) + frequencyWidth
  176. margin = (cScreenWidth - cropViewWidth) / 2.0
  177. // 2竖线一个间隔代表多少 S 是动态的
  178. oneMarginTime = (stuckPointEndTime - stuckPointStartTime) / CGFloat(wavSelectCount)
  179. // 如果视频结束时间点大于歌曲有效结束点,则拼接推荐的时间段直到满足视频播放
  180. var videoDurationTemp = suggestRhythmEndTime
  181. while stuckPointEndTime > videoDurationTemp {
  182. videoDurationTemp += (suggestRhythmEndTime - suggestRhythmStartTime)
  183. }
  184. videoDuration = videoDurationTemp
  185. // 3,一共绘制的竖线个数
  186. wavTotalCount = Int(ceil(videoDuration / oneMarginTime) + 1)
  187. timeRange = oneMarginTime * 10
  188. // 显示时间 label 的个数 , -1 不够整倍数就不显示时间了
  189. let timeLabelCount = Int(wavTotalCount / 10)
  190. contentWidth = CGFloat(wavTotalCount - 1) * (frequencyWidth + frequencyMargin) + frequencyWidth + (cScreenWidth - cropViewWidth)
  191. if contentWidth < scrollView.frame.width {
  192. contentWidth = scrollView.frame.width
  193. }
  194. scrollView.contentSize = CGSize(width: contentWidth, height: scrollView.frame.height)
  195. BFLog(1, message: "框内个数:\(wavSelectCount), 总线条数:\(wavTotalCount), 框宽:\(cropViewWidth), 最终音乐时长:\(videoDuration)")
  196. scrollView.subviews.forEach { lable in
  197. if lable is UILabel && lable.tag != 66 {
  198. lable.removeFromSuperview()
  199. }
  200. }
  201. for index in 0 ... timeLabelCount {
  202. // scrollView.viewWithTag(100 + index)?.removeFromSuperview()
  203. let titleLab = UILabel(frame: CGRect(x: CGFloat(index) * (frequencyWidth + frequencyMargin) * 10 + margin - timeLineWidth / 2, y: rateView.frame.maxY, width: timeLineWidth, height: 30))
  204. titleLab.font = UIFont.systemFont(ofSize: 11)
  205. titleLab.textAlignment = .center
  206. titleLab.numberOfLines = 1
  207. titleLab.tag = 100 + index
  208. titleLab.backgroundColor = .clear
  209. titleLab.textColor = UIColor.hexColor(hexadecimal: "#999999")
  210. titleLab.text = "\(Float64(Int(CGFloat(index) * timeRange)).formatDurationToHMS())"
  211. scrollView.addSubview(titleLab)
  212. }
  213. if oneMarginTime > 0 {
  214. // 1,处理音频频率
  215. configVoiceFrequency()
  216. // 2,滚动到推荐位置
  217. if(!isUserDrag){
  218. scrollView.contentOffset = CGPoint(x: startLineX - margin, y: 0)
  219. }
  220. scrollView.addSubview(rateView)
  221. }
  222. }
  223. /// 处理音频频率
  224. /// - Returns: <#description#>
  225. func configVoiceFrequency() {
  226. // 整倍数
  227. let waveTotalCount = Int(wavTotalCount) / cFrequency.count
  228. // 余多少个未画的
  229. var remainder = Int(wavTotalCount % cFrequency.count)
  230. var totalWave: [CGFloat] = Array<CGFloat>.init()
  231. // 1,先画整倍数个竖线
  232. for _ in 0 ..< waveTotalCount {
  233. totalWave = totalWave + cFrequency
  234. }
  235. if remainder > cFrequency.count - 1 {
  236. remainder = cFrequency.count - 1
  237. }
  238. // 1,再画余数个竖线
  239. if remainder > 0 {
  240. totalWave = totalWave + cFrequency[0 ... (remainder - 1)]
  241. }
  242. createWave(waveArr: totalWave)
  243. }
  244. /// 更新进度绘制不同色值
  245. /// progress <#progress description#>
  246. func updateProgress(progress: CGFloat) {
  247. if(progress <= 0 || lineLayerArray.count == 0 || progress.isNaN){
  248. BFLog(message: "progress is error ")
  249. return
  250. }
  251. let startIndex = scrollView.contentOffset.x / (frequencyWidth + frequencyMargin)
  252. lastDrawedLineIndex = max(lastDrawedLineIndex, Int(ceil(startIndex)))
  253. let selectIndex = Int(ceil(startIndex + progress * CGFloat(wavSelectCount)))
  254. while(selectIndex < lineLayerArray.count && selectIndex > lastDrawedLineIndex){
  255. let drawLayer:CAShapeLayer = lineLayerArray[lastDrawedLineIndex]
  256. if drawLayer.strokeColor != UIColor.hexColor(hexadecimal: PQBFConfig.shared.styleColor.rawValue).cgColor{
  257. // BFLog(1, message: "progress is \(progress) i \(lastDrawedLineIndex) 命中的位置:\(CGFloat(lastDrawedLineIndex) * oneMarginTime)")
  258. drawLayer.strokeColor = UIColor.hexColor(hexadecimal: PQBFConfig.shared.styleColor.rawValue).cgColor
  259. drawLayer.setNeedsDisplay()
  260. drawLayer.layoutIfNeeded()
  261. }
  262. lastDrawedLineIndex += 1
  263. }
  264. if(progress >= 0.999){
  265. BFLog(message: "播放完成 重新更新 UI ")
  266. resetDefaultsColor(clearData: false)
  267. }
  268. }
  269. // 竖线恢复到原有色值
  270. func resetDefaultsColor(clearData:Bool = true) {
  271. lastDrawedLineIndex = 0
  272. for layer in lineLayerArray {
  273. layer.strokeColor = UIColor.hexColor(hexadecimal: "#999999").cgColor
  274. layer.setNeedsDisplay()
  275. }
  276. if(clearData == true){
  277. lineLayerArray.removeAll()
  278. if(rateView.layer.sublayers != nil){
  279. for (_,layer) in rateView.layer.sublayers!.enumerated() {
  280. layer.removeFromSuperlayer()
  281. }
  282. }
  283. isUserDrag = false
  284. isDrawLine = false
  285. }
  286. }
  287. /// 生成波纹
  288. /// - Parameter waveArr: <#waveArr description#> // warning 有崩溃 _buffer _ArrayBuffer<CoreGraphics.CGFloat> wavearr为CoreGraph.CGFloat数组
  289. /// - Returns: <#description#>
  290. func createWave(waveArr: [CGFloat]) {
  291. for (i, power) in waveArr.enumerated() {
  292. // 画布高度
  293. let hight: CGFloat = rateView.frame.height
  294. // 开始 Y 值
  295. var startY: CGFloat = (hight - power) / 2.0
  296. if startY < 0 { startY = 0 }
  297. // 结束 Y 值
  298. var endY: CGFloat = startY + power
  299. if endY > CGFloat(hight) { endY = hight }
  300. // 线的路径
  301. let linePath = UIBezierPath()
  302. // 起点 timeLineWidth / 2 处理显示时间的 label中心为时间点
  303. let originX: CGFloat = CGFloat(Float(i) * Float(frequencyWidth + frequencyMargin)) + margin
  304. linePath.move(to: CGPoint(x: originX, y: startY))
  305. // 终点
  306. linePath.addLine(to: CGPoint(x: originX, y: endY))
  307. let lineLayer = CAShapeLayer()
  308. lineLayer.lineWidth = frequencyWidth
  309. lineLayer.strokeColor = UIColor.hexColor(hexadecimal: "#999999").cgColor
  310. lineLayer.path = linePath.cgPath
  311. lineLayer.fillColor = UIColor.black.cgColor
  312. // 推荐的开始起点是虚线 减0.0001因为精度问题
  313. // BFLog(1, message: "suggestRhythmStartTime is \(suggestRhythmStartTime)")
  314. if oneMarginTime * CGFloat(i) >= (suggestRhythmStartTime-0.0001) && !isDrawLine {
  315. isDrawLine = true
  316. linePath.move(to: CGPoint(x: originX, y: -10))
  317. // 终点
  318. linePath.addLine(to: CGPoint(x: originX, y: 30))
  319. lineLayer.path = linePath.cgPath
  320. lineLayer.lineDashPhase = 0
  321. lineLayer.lineDashPattern = [3, 3]
  322. }
  323. if startLineX == 0 && oneMarginTime * CGFloat(i) >= stuckPointStartTime{
  324. startLineX = originX
  325. }
  326. lineLayerArray.append(lineLayer)
  327. rateView.layer.insertSublayer(lineLayer, at: 0)
  328. }
  329. }
  330. deinit {
  331. BFLog(message: "卡点裁剪-裁剪视图销毁")
  332. }
  333. //划动结速后处理
  334. func moveEnd() {
  335. //最后一个竖线VIEW
  336. let lastLine:UIView = scrollView.viewWithTag(100 + Int(videoDuration / timeRange) - 1) ?? UIView.init()
  337. //移动后的开始时间
  338. let startTime = videoDuration / lastLine.frame.maxX * scrollView.contentOffset.x
  339. // let startTime = videoDuration * (margin + scrollView.contentOffset.x) / scrollView.contentSize.width
  340. //选中的时长
  341. let selectDuration:CGFloat = CGFloat(stuckPointEndTime - stuckPointStartTime)
  342. BFLog(message: "拖拽结束 - 回调\(scrollView.contentOffset) \(scrollView.contentSize) 开始时间为:\(startTime) 结束时间为:\(startTime + selectDuration)")
  343. stuckPointStartTime = startTime
  344. stuckPointEndTime = stuckPointStartTime + selectDuration
  345. if(videoDidEndDragging != nil){
  346. videoDidEndDragging!(1,startTime,startTime + CGFloat(stuckPointEndTime - stuckPointStartTime),0)
  347. }
  348. resetDefaultsColor(clearData: false)
  349. PQEventTrackViewModel.baseReportUpload(businessType: .bt_buttonClick, objectType: .ot_shanyinApp_musicVideoPreview_musicPeriodSelect, pageSource: .sp_shanyinApp_main, extParams: nil, remindmsg: "")
  350. }
  351. }
  352. // MARK: - scrollView滑动代理
  353. /// scrollView滑动代理
  354. extension PQStuckPointCuttingView: UIScrollViewDelegate {
  355. func scrollViewDidScroll(_: UIScrollView) {}
  356. func scrollViewWillBeginDragging(_ :UIScrollView){
  357. isUserDrag = true
  358. if(videoDidBeginDrag != nil){
  359. videoDidBeginDrag!()
  360. }
  361. }
  362. func scrollViewDidEndDecelerating(_: UIScrollView) {
  363. if !scrollView.isDragging, !scrollView.isDecelerating {
  364. DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.2) { [weak self] in
  365. self?.moveEnd()
  366. }
  367. }
  368. }
  369. func scrollViewDidEndDragging(_:UIScrollView,willDecelerate decelerate:Bool){
  370. if !decelerate, !scrollView.isDragging, !scrollView.isDecelerating {
  371. DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.2) { [weak self] in
  372. self?.moveEnd()
  373. }
  374. }
  375. }
  376. func scrollViewDidEndScrollingAnimation(_: UIScrollView) {
  377. BFLog(message: "scrollViewDidEndScrollingAnimation")
  378. }
  379. }