五一第一天在家休息,
看了一下視頻播放的相關東西
寫了一個簡單的觸摸視頻播放器
我喜歡從自己的使用感受做些調整
所以在裏面有一個 觸摸平滑進度
的實現,具體看下面
可觸控的播放器完整代碼
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 轉載請說明出處