五一第一天在家休息,
看了一下视频播放的相关东西
写了一个简单的触摸视频播放器
我喜欢从自己的使用感受做些调整
所以在里面有一个 触摸平滑进度
的实现,具体看下面
Github源码地址: https://github.com/intbird/VideoPlayerLib
GitHub issues(持续维护,待开发):
https://github.com/intbird/VideoPlayerLib/issues/2
文章来自:http://blog.csdn.net/intbird 转载请说明出处
在屏幕中间滑动: 拖动进度(拖动时隐藏控制面板)
在屏幕中间点击: 切换播放/暂停
在屏幕左侧滑动: 控制亮度
在屏幕右侧滑动: 控制声音
点击锁定按钮: 锁定当前所有操作
点击上一个/下一个/播放/暂定/停止: 执行对应动作
Github源码地址: https://github.com/intbird/VideoPlayerLib
文章来自:http://blog.csdn.net/intbird 转载请说明出处
测试视频
0.权限检查: 读取媒体权限
1.播放器层: 播放器的接口 + 实现
2.触控层: 触摸区域识别 + 手势识别 + 触摸灵敏度和进度反馈
3.控制面板层: 可视UI按钮(上一个,下一个,播放/暂停/停止)
4.锁定层: 锁(锁播放器+ 锁触控 + 锁面板+ 锁屏幕方向等)
用mvc简单实现一下,有空了可以把view这层在做层封装,方便后续更换UI的最小代价
1.左侧滑动控制亮度
可调节值: 调节系统亮度值(-1.0 -1.0) 和 调节当前窗口(-1.0 - 1.0)
注意这里是( -1.0 - 1.0 ),UI进度一般为(0-100)不会有负数, 需要处理
这里有个问题,系统标示-1为不可用, 但调节时 0 是最小值,1是最大值
WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_OFF
WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_FULL
2.右侧音量调节
可调节值: 调节系统声音值(0-15)
注意这里是(0-15),如果0-1进度需要滑动过长,则进度笔记生硬
3.进度的百分比实时拖动时注意卡顿情况
1.如果需要实时预览要保证不卡顿(一些机器有些卡)
2.如果不实时预览,则需要考虑如何实现
private var allowXAlixRange: Rect? = null
private var allowYAlixRangeLeft: Rect? = null
private var allowYAlixRangeRight: Rect? = null
比如滑动多长距离才能对应1个音量或者1个进度的一个百分比
// 进度视差因子
private val parallaxX = 1f
// 音量视差因子
private val parallaxYVolume = 4.4f
// 亮度视差因子
private val parallaxYLight = 4.4f
1.比如音量是0-15,太长的屏幕滑动起来感觉不柔和,一次跳跃的距离有些长
2.亮度是( -1.0 - 1.0 ), UI进度一般为(0-100)不会有负数
所以也要转正( 0 - minValue) 并且 放大处理 (actuary = 100)
转正的意思是将 -1.0 - 1.0 变为 0.0 - 2.0,然后进行正数的UI放大100倍
这里有个问题,系统标示-1为不可用, 但调节时 0 是最小值,1是最大值
WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_OFF
WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_FULL
3.综上: AdjustInfo 对象内部参数调整为
音量UI:( 0 - 15 ) -> ( 0 - 1500 )
亮度UI: ( -1.0 - 1.0 ) -> (0 - 200 )
AdjustInfo:
private fun realValue() {
....
}
private fun absUIValue() {
val actuary = 100
if (minValue < 0) {
val diff = 0 - minValue
...
currentValueUI += ((currentValue + diff) * actuary).toInt()
} else {
...
currentValueUI += (currentValue * actuary).toInt()
}
}
/**
* 进度变更时,也可以监听实际值 去放大 UI值
* 这里使用了直接赋两个值(实际值和UI值),简单一些
**/
fun addIncrease(increaseRatio: Float) {
progress = MediaTimeUtil.adjustValueBoundF((currentValue + increaseRatio * maxValue), maxValue, minValue)
progressUI = MediaTimeUtil.adjustValueBoundF((currentValueUI + (increaseRatio * maxValueUI)), maxValueUI.toFloat(), minValueUI.toFloat()).toInt()
}
音量/亮度如何调节实现交给外部实现
后面时间多些了可以把view层也做一层抽离,目前问题也不大
class VideoPlayerActivity : Activity(), ILockExecute {
companion object {
var EXTRA_FILE_URLS = "videoUrls"
var EXTRA_FILE_INDEX = "videoIndex"
}
...
private var player: IPlayer? = null
private var locker: LockController? = null
private var videoTouchController: TouchController? = null
private var videoControlController: ControlController? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.lib_media_video_player_main)
// 锁
locker = LockController(ivPopLock)
// 播放器
player = PlayerImpl(playerCallback)
// 触控层
videoTouchController = TouchController(player, locker, touchCallback, layoutTouchPanel)
// 控制
videoControlController =
ControlController(player, locker, controlCallback, layoutControlPanel)
// 哪些操作可以被锁
locker?.addExecute(videoTouchController)
?.addExecute(videoControlController)
?.addExecute(this) // this 这里锁的是横竖屏状态
}
这里有个待实现的是监听OrientationEventListener,
类似iPad抖动一下屏幕恢复和手机一致的方向
private fun calScreenOrientation(activity: Activity): Int {
val display = activity.windowManager.defaultDisplay
return when (display.rotation) {
// 横屏
Surface.ROTATION_90, Surface.ROTATION_270 -> {
ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
}
else -> {
ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT
}
}
}
override fun executeLock(lock: Boolean) {
// 当前方向
val orientation:Int = calScreenOrientation(this)
// 方向锁定
if (lock) {
if (this.requestedOrientation != orientation) {
this.requestedOrientation = orientation
}
} else {
this.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR
}
// 是否禁用自动转屏
if (MediaLightUtils.checkSystemWritePermission(this))
Settings.System.putInt(contentResolver, Settings.System.ACCELEROMETER_ROTATION, if (lock) 0 else 1)
}
interface IPlayer {
/**
* 实际上这个通知由display调用,这里先简化一下
*/
fun available(display: Surface?)
fun prepare(mediaFileInfo: MediaFileInfo)
fun start()
fun seekTo(duration: Long, start: Boolean)
fun resume()
fun pause()
fun stop()
fun destroy()
fun isPlaying(): Boolean
fun getCurrentTime(): Long
fun getTotalTime(): Long
}
更多看源码吧:
Github源码地址: https://github.com/intbird/VideoPlayerLib
文章来自:http://blog.csdn.net/intbird 转载请说明出处
有空了可以把这里的UI抽出去,方便后面改动
其实点按监听 和 滑动监听 可以放在一个GestureDetector中
但是我想如果后面按touch挪会方便点,而且难免后面会有其他手势检测
一个类也不可能要承载那么多不同逻辑代码,放不放问题都不大
class TouchController(private val player: IPlayer?, private val iLockCall: ILockCallback?,
private val videoTouchCallback: IVideoTouchCallback,
private var viewImpl: View) : ILockExecute, ILandscapeExecute {
/**
* 点击手势解析, 用来点击控制 播放/暂停
*/
private var tapInterceptor = GestureDetector(videoTouchCallback.getContext(), PlayerTapInterceptor())
/**
* 触摸手势解析, 用来判断 滑动在屏幕左侧/右侧的纵向滑动, 还是在屏幕中间横向滑动
*/
private var touchInterceptor = PlayerTouchInterceptor()
private val mediaTotalTime
get() = player?.getTotalTime()?: 0L
private val mediaCurrentTime
get() = player?.getCurrentTime()?: 0L
init {
executeLock(false)
}
override fun executeLock(lock: Boolean) {
if (lock) {
viewImpl.setOnTouchListener { _, _ -> iLockCall?.needUnLock(); false }
} else {
viewImpl.setOnTouchListener { view, event -> touchInterceptor.onTouch(view, event) || tapInterceptor.onTouchEvent(event) }
}
}
override fun onLandscape() {
touchInterceptor.viewSizeChange()
}
override fun onPortrait() {
touchInterceptor.viewSizeChange()
}
inner class PlayerTapInterceptor : SimpleOnGestureListener() {
override fun onSingleTapConfirmed(e: MotionEvent?): Boolean {
videoTouchCallback?.onSingleTap()
return true
}
override fun onDoubleTap(e: MotionEvent?): Boolean {
videoTouchCallback?.onDoubleTap()
return true
}
}
data class PlayerMoveBound(val lowBound: Int, var upBound: Int)
inner class PlayerTouchInterceptor() : View.OnTouchListener {
// 触摸记录
private var lastTouchEventX: Float = 0f
private var lastTouchEventY: Float = 0f
private var lastTouchType: PlayerTouchType = PlayerTouchType.NONE
// 进度视差因子, 优化调节效果
private val parallaxX = 1f
// 音量视差因子, 优化调节效果
private val parallaxYVolume = 4.4f
// 亮度视差因子, 优化调节效果
private val parallaxYLight = 4.4f
// 回调进度阈值, 防止无效的重复调用
private val ratioThreshold = 0.01f
// 横向滑动控制范围
private var allowXAlixRange: Rect? = null
private var allowXAlixMoveBound: PlayerMoveBound? = PlayerMoveBound(20, 20)
// 纵向滑动控制范围
private var allowYAlixRangeLeft: Rect? = null
private var allowYAlixRangeRight: Rect? = null
private var allowYAlixMoveBound: PlayerMoveBound? = PlayerMoveBound(20, 20)
// 进度缓存
private var lastProgressInfo = ProgressInfo()
// 音量缓存
private var adjustVolumeInfo = AdjustInfo()
// 亮度缓存
private var adjustBrightnessInfo = AdjustInfo()
fun viewSizeChange() {
allowXAlixRange = null
allowYAlixRangeLeft = null
allowYAlixRangeRight = null
}
override fun onTouch(v: View?, event: MotionEvent?): Boolean {
val viewWidth = v?.width ?: 0
val viewHeight = v?.height ?: 0
// 不应用滑动
if (viewWidth == 0 || viewHeight == 0) {
return false
}
when (event?.actionMasked) {
MotionEvent.ACTION_DOWN -> {
lastTouchEventX = event.x
lastTouchEventY = event.y
handleTouchDown(viewWidth, viewHeight)
}
MotionEvent.ACTION_MOVE -> {
val distanceX = event.x - lastTouchEventX
val distanceY = event.y - lastTouchEventY
return handlerTouchMove(distanceX, distanceY, viewWidth, viewHeight, event)
}
MotionEvent.ACTION_UP -> {
releaseTouchHandler()
}
else -> {
}
}
return false
}
private fun handlerTouchMove(distanceX: Float, distanceY: Float, viewWidth: Int, viewHeight: Int, event: MotionEvent): Boolean {
return when (lastTouchType) {
PlayerTouchType.NONE -> {
if (isTouchProgress(distanceX, distanceY, viewWidth, event)) {
lastTouchType = PlayerTouchType.TOUCH_PROGRESS
videoTouchCallback.onBeforeDropSeek()
}
if (isTouchVolume(distanceX, distanceY, viewHeight, event)) {
lastTouchType = PlayerTouchType.TOUCH_VOLUME
}
if (isTouchLight(distanceX, distanceY, viewHeight, event)) {
lastTouchType = PlayerTouchType.TOUCH_LIGHT
}
return lastTouchType != PlayerTouchType.NONE
}
PlayerTouchType.TOUCH_PROGRESS -> {
touchProgress(distanceX, distanceY, viewWidth, event)
}
PlayerTouchType.TOUCH_VOLUME -> {
touchVolume(distanceX, distanceY, viewHeight, event)
}
PlayerTouchType.TOUCH_LIGHT -> {
touchLight(distanceX, distanceY, viewHeight, event)
}
}
}
private fun handleTouchDown(viewWidth: Int, viewHeight: Int) {
// 横向进度触摸范围
if (null == allowXAlixRange) {
allowXAlixRange = Rect(0, 0, viewWidth, viewHeight)
}
if (null == allowYAlixRangeLeft) {
allowYAlixRangeLeft = Rect(0, viewHeight / 6 * 1, viewWidth / 2, viewHeight / 6 * 5)
}
if (null == allowYAlixRangeRight) {
allowYAlixRangeRight = Rect(viewWidth / 2, viewHeight / 6 * 1, viewWidth, viewHeight / 6 * 5)
}
lastProgressInfo.available = false
adjustVolumeInfo.available = false
adjustBrightnessInfo.available = false
}
private fun isTouchProgress(distanceX: Float, distanceY: Float, viewWidth: Int, event: MotionEvent): Boolean {
return allowXAlixRange!!.contains(event.x.toInt(), event.y.toInt())
&& (abs(distanceY) < allowXAlixMoveBound!!.lowBound) && (abs(distanceX) > allowXAlixMoveBound!!.upBound)
}
private fun isTouchVolume(distanceX: Float, distanceY: Float, viewHeight: Int, event: MotionEvent): Boolean {
return allowYAlixRangeRight!!.contains(event.x.toInt(), event.y.toInt())
&& (abs(distanceX) < allowYAlixMoveBound!!.lowBound) && (abs(distanceY) > allowYAlixMoveBound!!.upBound)
}
private fun isTouchLight(distanceX: Float, distanceY: Float, viewHeight: Int, event: MotionEvent): Boolean {
return allowYAlixRangeLeft!!.contains(event.x.toInt(), event.y.toInt())
&& (abs(distanceX) < allowYAlixMoveBound!!.lowBound) && (abs(distanceY) > allowYAlixMoveBound!!.upBound)
}
private fun releaseTouchHandler() {
when (lastTouchType) {
PlayerTouchType.NONE -> {
}
PlayerTouchType.TOUCH_PROGRESS -> {
releaseProgressTouch()
}
PlayerTouchType.TOUCH_VOLUME -> {
releaseVolumeTouch()
}
PlayerTouchType.TOUCH_LIGHT -> {
releaseLightTouch()
}
}
lastTouchType = PlayerTouchType.NONE
}
private fun touchProgress(distanceX: Float, distanceY: Float, viewWidth: Int, event: MotionEvent): Boolean {
val radioX = distanceX / viewWidth // 滑动长度占比
// 阈值
if (abs(radioX) > 0.01) {
// 计算进度值
if (!lastProgressInfo.available) {
lastProgressInfo = ProgressInfo(0L, mediaTotalTime, mediaCurrentTime)
}
lastProgressInfo.addIncrease(radioX * parallaxX)
videoTouchCallback.onDroppingSeek(lastProgressInfo.progress)
// 播放控制
// videoTouchCallback?.notifyVideoProgressImpl(newVideoProgressTime, mediaTotalTime)
visibleProgressIndicator(true)
viewImpl.tvTouchCurrentProgress.text = MediaTimeUtil.formatTime(lastProgressInfo.progress)
viewImpl.tvTouchTotalProgress.text = MediaTimeUtil.formatTime(mediaTotalTime)
viewImpl.pbTouchProgress.progress = lastProgressInfo.progressUI
viewImpl.pbTouchProgress.max = lastProgressInfo.maxValueUI
}
return true
}
private fun releaseProgressTouch() {
visibleProgressIndicator(false)
videoTouchCallback.onAfterDropSeek()
}
private fun touchVolume(distanceX: Float, distanceY: Float, viewHeight: Int, event: MotionEvent): Boolean {
val ratioY = -distanceY / viewHeight // 滑动高度占比
//阈值
if (abs(ratioY) > ratioThreshold) {
if (!adjustVolumeInfo.available) {
adjustVolumeInfo = videoTouchCallback.getVolumeInfo()
}
adjustVolumeInfo.addIncrease(ratioY * parallaxYVolume)
// 音量调节实现让外部去做
videoTouchCallback.changeSystemVolumeImpl(adjustVolumeInfo.progress)
visibleAdjustIndicator(true)
// 调整UI
if (adjustVolumeInfo.progress <= 0) viewImpl.adjustIcon.setImageResource(R.drawable.icon_video_player_audio_off)
else viewImpl.adjustIcon.setImageResource(R.drawable.icon_video_player_audio_on)
viewImpl.adjustProgressBar.progress = adjustVolumeInfo.progressUI
viewImpl.adjustProgressBar.max = adjustVolumeInfo.maxValueUI
}
return true
}
private fun releaseVolumeTouch() {
visibleAdjustIndicator(false)
}
private fun touchLight(distanceX: Float, distanceY: Float, viewHeight: Int, event: MotionEvent): Boolean {
val ratioY = -distanceY / viewHeight // 滑动高度占比
//阈值
if (abs(ratioY) > ratioThreshold) {
if (!adjustBrightnessInfo.available) {
adjustBrightnessInfo = videoTouchCallback.getBrightnessInfo()
}
adjustBrightnessInfo.addIncrease(ratioY * parallaxYLight)
// 亮度调节实现让外部去做
videoTouchCallback.changeBrightnessImpl(adjustBrightnessInfo.progress)
visibleAdjustIndicator(true)
// 调整UI
if (adjustBrightnessInfo.progress <= 0) viewImpl.adjustIcon.setImageResource(R.drawable.icon_video_player_light_off)
else viewImpl.adjustIcon.setImageResource(R.drawable.icon_video_player_light_on)
viewImpl.adjustProgressBar.progress = adjustBrightnessInfo.progressUI
viewImpl.adjustProgressBar.max = adjustBrightnessInfo.maxValueUI
}
return true
}
private fun releaseLightTouch() {
visibleAdjustIndicator(false)
}
private fun visibleProgressIndicator(visible: Boolean) {
if (visible) {
if (viewImpl.llTimeIndicatorWrapper.visibility == View.INVISIBLE) {
viewImpl.llTimeIndicatorWrapper.visibility = View.VISIBLE
}
} else {
if (viewImpl.llTimeIndicatorWrapper.visibility == View.VISIBLE) {
viewImpl.llTimeIndicatorWrapper.visibility = View.INVISIBLE
}
}
}
private fun visibleAdjustIndicator(visible: Boolean) {
if (visible) {
if (viewImpl.llAdjustIndicatorWrapper.visibility == View.INVISIBLE) {
viewImpl.llAdjustIndicatorWrapper.visibility = View.VISIBLE
}
} else {
if (viewImpl.llAdjustIndicatorWrapper.visibility == View.VISIBLE) {
viewImpl.llAdjustIndicatorWrapper.visibility = View.INVISIBLE
}
}
}
}
fun destroy() {
}
}
enum class PlayerTouchType {
NONE, TOUCH_PROGRESS, TOUCH_LIGHT, TOUCH_VOLUME
}
private fun toggleVisibleAnimation(
visible: Boolean,
targetViews: Array,
animation: Boolean = true
) {
if (animation) {
for (view in targetViews) {
view.animate().alpha(if (visible) 1f else 0f)
.setDuration(if (visible) visibleDuration else inVisibleDuration)
.withEndAction {
view.visibility = if (visible) View.VISIBLE else View.INVISIBLE
}
}
} else {
for (view in targetViews) {
view.visibility = if (visible) View.VISIBLE else View.INVISIBLE
}
}
}
1.一定要注意这个 clip
2. seekbar样式兼容
1.低版本的兼容(6.0以下)gravity不生效
需要用图片或者自定义view啥的实现以下
2.高版本快捷修改bar颜色api:
后面想到什么再补充以下.
End.
Github源码地址: https://github.com/intbird/VideoPlayerLib
文章来自:http://blog.csdn.net/intbird 转载请说明出处