android media player实现一个可手势滑动控制的视频播放器

五一第一天在家休息,
看了一下视频播放的相关东西
写了一个简单的触摸视频播放器
我喜欢从自己的使用感受做些调整
所以在里面有一个 触摸平滑进度 的实现,具体看下面

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.显示声明外部api(模块解耦)

在这里插入图片描述

2.对外api实现(模块解耦)

在这里插入图片描述

3.总体结构

在这里插入图片描述

4.资源前缀

在这里插入图片描述

2. 大体思路

1. touch里面的一些优化实现

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.如果不实时预览,则需要考虑如何实现

1.触摸的范围检测:
 private var allowXAlixRange: Rect? = null
 private var allowYAlixRangeLeft: Rect? = null
 private var allowYAlixRangeRight: Rect? = null
2.视差因子(提升滑动体验):

比如滑动多长距离才能对应1个音量或者1个进度的一个百分比

        // 进度视差因子
        private val parallaxX = 1f
        // 音量视差因子
        private val parallaxYVolume = 4.4f
        // 亮度视差因子
        private val parallaxYLight = 4.4f
3.提升触摸的UI体验(滑动平滑不生硬)

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()
    }
4.滑动只计算数值,实现交给外部

音量/亮度如何调节实现交给外部实现
后面时间多些了可以把view层也做一层抽离,目前问题也不大在这里插入图片描述

3. 代码实现

1. activity入口代码
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 这里锁的是横竖屏状态
    }
2. 锁/解锁定当前屏幕方向

这里有个待实现的是监听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)
    }

3. 播放器的接口,滑动接口,触摸等接口和实现分离
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 转载请说明出处

4.触摸完整代码

有空了可以把这里的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
}
5.控制面板消失时加一些动画过渡,效果稍微好些
private fun toggleVisibleAnimation(
            visible: Boolean,
            targetViews: Array<View>,
            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
            }
        }
    }

4. 资源文件

1. seekbar样式自定义:

1.一定要注意这个 clip

在这里插入图片描述
2. seekbar样式兼容
1.低版本的兼容(6.0以下)gravity不生效

  需要用图片或者自定义view啥的实现以下

2.高版本快捷修改bar颜色api:

   <style>
       <item name="android:colorControlActivated">#1a237e</item>
       <item name="android:colorControlNormal">#00b0ff</item>
   </style>
2. 水波纹效果的版本兼容

后面想到什么再补充以下.

End.
Github源码地址: https://github.com/intbird/VideoPlayerLib
文章来自:http://blog.csdn.net/intbird 转载请说明出处

猜你喜欢

转载自blog.csdn.net/intbird/article/details/105970536