引言
目前已经实现的播放器都是通过可视的按钮和进度条来操作视频的播放,暂停,快进等功能,但我们发现大多数播放器都可以通过双击屏幕,长按屏幕,拖拽屏幕,来简化播放器的操作。
本文将深入探讨iOS视频播放器中常见的手势功能,探讨它们的设计原理和实际运用,帮助开发者和用户更好地理解和利用这些便捷的操作方式,从而提升iOS视频观看体验的质量和乐趣。
功能实现
接下来我们就根据不同的手势比如,单击,双击,长按,和拖拽,来介绍手势在视频播放器中的应用,为了项目结构的清晰,我们把手势处理单独创建一个图层命名为PHGestureView,再稍微修改一下页面的图层结构,让PHControlView和PHGestureView可以同时相应用户的操作。
import UIKit
import AVFoundation
class PHPlayerView: UIView {
/// 控制图层
let controlView = PHControlView()
/// 手势图层
let gestureView = PHGestureView()
/// 重写layerClass方法,
override class var layerClass: AnyClass{
get {
return AVPlayerLayer.self
}
}
/// 重写init方法
///
/// - Parameters:
/// - player: 播放器
init(player:AVPlayer) {
super.init(frame: CGRectZero)
guard let playerLayer = self.layer as? AVPlayerLayer else { return }
playerLayer.player = player
self.addSubview(gestureView)
gestureView.controlView = controlView
gestureView.addSubview(controlView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
controlView.frame = self.bounds
gestureView.frame = self.bounds
}
}
单击
单击手势主要应用到播放器页面元素的隐藏和显示。为了给用户一个沉浸式的观看体验,通常播放器的页面其它元素都会在一定时间内自动隐藏,单击屏幕后显示。如果是在页面元素显示的状态单击屏幕则会隐藏页面元素。
单击-隐藏/显示
在PHGestureView中创建一个单击手势,和isMoveing属性用来判断页面元素是否正在进行隐藏或者显示的动画,避免出现重复操作。还需要引入一个PHControlView的对象,将手势的操作通过PHControlView传递到播放器。属性如下:
class PHGestureView: UIView {
weak var controlView:PHControlView?
/// 是否正在进行隐藏显示动画
var isMoveing = false
/// 单击手势
var singleTap:UITapGestureRecognizer!
.....
}
添加单击手势,在视图初始化的时候调用方法添加单击手势,注意需要将视图的允许用户响应熟悉设置为true。
override init(frame: CGRect) {
super.init(frame: frame)
addGestureRecognizers()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func addGestureRecognizers() {
addSingleTap()
}
// 单击
func addSingleTap() {
singleTap = UITapGestureRecognizer(target: self, action: #selector(singleOnclick))
self.addGestureRecognizer(singleTap)
self.isUserInteractionEnabled = true
}
单击手势实现,首先判断是否正在执行显示或者隐藏的动画,如果是则认为本次点击事件无效。
如果没有正在执行动画,则标记isMoveing为true表示正在进行动画,此时判断controlView的透明度,当透明度为0时,设置为1。当透明度为1时则设置为0。执行动画,动画结束后设置isMoveing为false。
// 单击
@objc func singleOnclick() {
guard let controlView = controlView else { return }
if isMoveing {
return
}
isMoveing = true
var alpha = 0.0
alpha = controlView.alpha == 1.0 ? 0.0 : 1.0
UIView.animate(withDuration: 0.2) {
controlView.alpha = alpha
} completion: { finish in
self.isMoveing = false
}
}
延迟自动隐藏
在延迟自动隐藏中我们创建了一个中间类,主要用于避免出现循环引用引起的延迟释放,它的实现不在本篇博客介绍的范围内,下面直接粘上相关代码。
import UIKit
class PHAfterTask: NSObject {
/// 延迟时间
var after:TimeInterval!
/// 原目标对象
weak var target:AnyObject!
/// 原对象要执行方法
var selector:Selector!
init(after: TimeInterval!, target: AnyObject!, selector: Selector!) {
super.init()
self.after = after
self.target = target
self.selector = selector
//使用perform延迟执行performMethod
self.perform(#selector(performMethod), with: nil, afterDelay: after)
}
@objc func performMethod() {
target.perform(selector, with: nil)
}
func cancel() {
//取消perform方法
NSObject.cancelPreviousPerformRequests(withTarget: self)
}
}
下面来使用这个PHAfterTask类实现页面元素的自动隐藏。
在手势视图初始化的时候,创建延迟对象,延迟执行已有方法,注意在deinit的时候手动取消延迟执行。
class PHGestureView: UIView {
weak var controlView:PHControlView?
/// 是否正在进行隐藏显示动画
var isMoveing = false
/// 延迟管理类
var afterTask:PHAfterTask?
/// 单击手势
var singleTap:UITapGestureRecognizer!
override init(frame: CGRect) {
super.init(frame: frame)
addGestureRecognizers()
startAfterTask()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func addGestureRecognizers() {
addSingleTap()
}
func startAfterTask() {
afterTask?.cancel()
afterTask = PHAfterTask(after: 2.0, target: self, selector: #selector(automaticHiddenControlView))
}
//自动隐藏
@objc func automaticHiddenControlView() {
guard let controlView = controlView else { return }
if controlView.alpha == 1.0 {
singleOnclick()
}
}
deinit {
afterTask?.cancel()
}
双击
双击通常用于播放器的播放和暂停操作。有了之前controlView的播放按钮作为基础,双击控制视频播放和暂停就容易许多,我们可以直接调用controlView的按钮点击事件,同时还可以同步按钮状态。
添加双击手势。这里需要注意的一点是,设置当双击手势失效时才响应单击手势,以解决手势冲突问题。
func addGestureRecognizers() {
...
addDoubleTap()
...
}
...
// 双击
func addDoubleTap() {
// 双击
doubleTap = UITapGestureRecognizer(target: self, action: #selector(doubleOnclick))
doubleTap.numberOfTapsRequired = 2
self.addGestureRecognizer(doubleTap)
singleTap.require(toFail: doubleTap)
}
双击事件。直接调用playbutton的点击方法,并将页面元素显示出来让用户明确当前播放器的状态。
// 双击
@objc func doubleOnclick() {
guard let controlView = controlView else { return }
let button = controlView.playButton
button.sendActions(for: .touchUpInside)
controlView.alpha = 1.0
if button.isSelected == false {
startAfterTask()
}
}
长按手势
长按手势通常用于视频的快进和快退功能,当点击屏幕的右半部分则快进,做半部分则快退,我们设置快进,快退的速度为原播放速度的2.0倍。
添加长按手势。设置最小长按时间为2.0秒,也需要注意与单击手势的冲突问题。
// 长按手势
func addLongTap() {
longTap = UILongPressGestureRecognizer(target: self, action: #selector(handleLong(_:)))
self.addGestureRecognizer(longTap)
longTap.minimumPressDuration = 2.0
singleTap.require(toFail: longTap)
}
长按手势实现。根据长按点位的x坐标来判断执行快进还是快退,当手势结束时则结束快进快退状态。
// 长按手势
@objc func handleLong(_ gesture:UILongPressGestureRecognizer) {
let location = gesture.location(in: self)
if location.y < 25.0 || location.y > self.bounds.height - 25.0 {
return
}
if gesture.state == .began {
if location.x > self.bounds.width * 0.5 {
//快进
controlView?.startAccelerate(forward: true)
} else {
//快退
controlView?.startAccelerate(forward: false)
}
}
if gesture.state == .ended {
//结束快进快退
controlView?.endAccelerate()
}
}
快进,快退和结束状态在controlView中的实现。
// 开始快进快退
@objc func startAccelerate(forward:Bool) {
guard let delegate = delegate else { return }
delegate.accelerate(rate: forward ? 2.0 : -2.0)
}
// 结束快进快退
func endAccelerate() {
guard let delegate = delegate else { return }
delegate.accelerate(rate: 1.0)
}
快进,快退和结束状态在播放器中的实现。到这可以看出其实就是设置AVPlayer的rate属性。
func accelerate(rate: Float) {
guard let player = player else { return }
player.rate = rate
}
拖拽手势
拖拽手势涉及的功能会稍微复杂一些,通常来讲分为横向的拖拽和纵向的拖拽。
横向拖拽又分为由左向右,快进视频。和由右向左,快退视频。
纵向的退拽又分为屏幕左侧和屏幕右侧。
屏幕左侧通常用于调节屏幕亮度, 又分为由上到下亮度变暗,和由下到上亮度变亮。
屏幕右侧通常用于调节播放器音量,又分为由上到下音量减小,
那么接下来我们开始分别介绍这些功能的实现。
横向拖拽
横向拖拽用于视频的快进和快退功能,首先我们需要定义出拖拽的距离与视频进度时间的关系,我采用了按照视频总长度比上屏幕宽度的方式来计算出了手指退拽单位距离所对应的时间。
添加手势。
// 拖拽手势
func addPanGesture() {
panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
self.addGestureRecognizer(panGesture)
singleTap.require(toFail: panGesture)
}
拖拽手势的实现。手势的方法中,通过x和y的偏移量来判断当前进行的横向拖拽还是纵向拖拽,我们暂且都聚集到横向拖拽,计算出横向移动的偏移量,传递到controlView中。
// 拖拽
@objc func handlePan(_ gesture:UIPanGestureRecognizer) {
guard let controlView = controlView else { return }
var distance = 0.0
if gesture.state == .began {
panStartPosition = gesture.location(in: self)
controlView.handleBeginPan()
} else if (gesture.state == .changed) {
let translation = gesture.translation(in: self)
let movePosition = gesture.location(in: self)
if abs(translation.x) > abs(translation.y) && !verticalPan {
print("水平拖拽")
//水平拖拽
distance = movePosition.x - panStartPosition.x
controlView.handlePanHorizontal(distance: Float(distance))
} else {
}
} else if (gesture.state == .ended) {
if verticalPan {
} else {
controlView.endHorizontalPan()
}
}
}
除了拖拽过程中计算偏移量以外,还需要注意的是将手势开始和手势结束的动作也同样都需要通知到controlView中,controlView中这三个方法的实现如下:
开始拖拽。记录当前视频进度值。
// 开始拖拽屏幕
func handleBeginPan() {
sliderValue = sliderView.value
}
拖拽过程。停止播放,根据手势移动的偏移量来计算视频快进的时间,注意临界值。
// 横向滑动屏幕
func handlePanHorizontal(distance:Float) {
guard let delegate = delegate else { return }
delegate.scrubbingDidStart()
//计算单位距离对应的时间
let unitTime = sliderView.maximumValue / (Float(self.bounds.size.width) - 100.0)
let moveTime = unitTime * distance
var currentTime = sliderValue + moveTime
if currentTime <= 1.0 {
currentTime = 1.0
} else if currentTime >= sliderView.maximumValue - 1.0 {
currentTime = sliderView.value - 1.0
}
sliderView.value = currentTime
}
结束拖拽。结束时将视频快进到指定时间。
// 结束横向滑动屏幕
func endHorizontalPan() {
guard let delegate = delegate else { return }
delegate.scrubbedDidEnd(time: TimeInterval(sliderView.value))
}
纵向拖拽
纵向的拖拽,我们根据手指的开始位置的X值是否大于屏幕中心位置的X,将纵向拖拽分成两部分。左侧的纵向拖拽用于调节屏幕亮度,右侧的纵向拖拽用户调节视频音量。
手势的实现代码如下。其中加了一个名为verticalPan的全局变量,目的是为了当发生纵向拖拽时,则屏蔽掉横向拖拽的方法。
// 拖拽
@objc func handlePan(_ gesture:UIPanGestureRecognizer) {
guard let controlView = controlView else { return }
var distance = 0.0
if gesture.state == .began {
panStartPosition = gesture.location(in: self)
controlView.handleBeginPan()
} else if (gesture.state == .changed) {
let translation = gesture.translation(in: self)
let movePosition = gesture.location(in: self)
if abs(translation.x) > abs(translation.y) && !verticalPan {
print("水平拖拽")
//水平拖拽
distance = movePosition.x - panStartPosition.x
controlView.handlePanHorizontal(distance: Float(distance))
} else {
verticalPan = true
distance = panStartPosition.y - movePosition.y
//垂直拖拽
if panStartPosition.x > self.bounds.width * 0.5 {
//右侧
controlView.handlePanRightVertical(distance: Float(distance))
} else {
//左侧
controlView.handlePanLeftVertical(distance: distance)
}
}
} else if (gesture.state == .ended) {
if verticalPan {
verticalPan = false
controlView.endVerticalPan()
} else {
controlView.endHorizontalPan()
}
}
}
接下来看一下纵向拖拽在controlView中的响应:
左侧纵向拖拽。将屏幕高度的80%当做亮度的最大值,通过手指移动距离/屏幕高度80%计算出,手指移动距离对应屏幕亮度的变化值。
/// 左侧纵向移动
func handlePanLeftVertical(distance:CGFloat) {
guard let delegate = delegate else { return }
let height = self.bounds.size.height * 0.8
let brightness = distance / height
delegate.sliderBrightness(brightness: brightness)
}
右侧纵向拖拽。将屏幕高度的80%当做声音的最大值,通过手指移动距离/屏幕高度80%计算出,手指移动距离对应音量的变化值。
/// 左侧纵向移动
func handlePanLeftVertical(distance:CGFloat) {
guard let delegate = delegate else { return }
let height = self.bounds.size.height * 0.8
let brightness = distance / height
delegate.sliderBrightness(brightness: brightness)
}
停止纵向拖拽。结束拖拽时,不需要任何多余的计算,直接传递到给播放控制器即可。
// 结束纵向退拽
func endVerticalPan() {
guard let delegate = delegate else { return }
delegate.verticalPanEnd()
}
调整亮度,音量方法在playerControler中的实现:
调节亮度。屏幕亮度的值通过设置UIScreen.main的brightness属性,熟悉值的范围是0.0-1.0。
其中的self.brightnes属性,为自定义的全局变量,在播放器创建的时候进行赋值。
/// 当前屏幕亮度
var brightness = UIScreen.main.brightness
/// 调整亮度
///
/// - Parameters:
/// - brightness: 亮度增量
func sliderBrightness(brightness: CGFloat) {
var currentbBrightness = self.brightness + brightness
currentbBrightness = min(1.0, currentbBrightness)
currentbBrightness = max(0.0, currentbBrightness)
UIScreen.main.brightness = currentbBrightness
let content = String(format: "亮度 %0.0f", currentbBrightness * 100)
playerView?.showTips(content: content)
}
调节音量。调节音量使用MPVolumeView类的sliderview来进行调节,可亮度一样,也需要在一开始获取初始音量的值。
findVolumeSlider()
volume = volumeSlider?.value
/// 调整音量
///
/// - Parameters:
/// - volume: 音量增量
func sliderVolume(volume: Float) {
var currentVolume = self.volume + volume
currentVolume = min(1.0, currentVolume)
currentVolume = max(0.0, currentVolume)
volumeSlider?.value = currentVolume
let content = String(format: "音量 %0.0f", currentVolume * 100)
playerView?.showTips(content: content)
}
func findVolumeSlider() {
let volumeView = MPVolumeView()
if let slider = volumeView.subviews.first(where: { $0 is UISlider }) as? UISlider {
self.volumeSlider = slider
}
}
结束拖拽后,将当前值赋值给全局变量,以便下次再此基础上进行修改。
/// 结束纵向拖拽
func verticalPanEnd() {
self.brightness = UIScreen.main.brightness
self.volume = volumeSlider?.value
playerView?.hiddenTips()
}
还有一点需要注意的时,当播放器销毁时将音量和亮度还原为原来的值。
deinit {
//移除播放结束 监听
self.removeItemEndObserverForPlayerItem()
UIScreen.main.brightness = originBrightness
volumeSlider?.value = originVolume
print("PHPlayerContoller-deinit")
}
结语
在本文中,我们介绍了iOS视频播放器中常见的手势控制功能,涵盖了滑动、单击、双击,拖拽等操作方式,以及它们在提升用户体验方面的作用。然而,要实现这些功能并不仅限于博客中所述的方法。iOS开发者可以根据自己的项目需求和用户反馈,采用不同的技术和实现方式来打造更加优秀的视频播放器。