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 轉載請說明出處

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章