android media player实现一个可手势滑动控制 + 可以调节分辨率|字幕|倍速的视频播放器(MediaPlayer + ExoPlayer实现)
文章来自:http://blog.csdn.net/intbird 转载请说明出处
五一第一天在家休息(后续休息有空会进行升级)
看了一下视频播放的相关东西
写了一个简单的触摸视频播放器
(使用 MediaPlayer 和 ExoPlayer )
我喜欢从自己的使用感受做些调整
所以在里面有一个 触摸平滑进度
的实现
还有就是代码面向抽象,方便后续一些功能替换
有空的话我就会继续写写它
Github源码地址: https://github.com/intbird/VideoPlayerLib
GitHub issues(持续维护,待开发):
https://github.com/intbird/VideoPlayerLib/issues/2
目录
- 基础功能:
- 完成效果:
- 结构解析:
- 代码结构:
- 1.项目结构
- 1.api: 使用Fragment(推荐此方式)
- 2.api: 使用Activity(模块解耦实现)
- 3.总体结构
- 4.资源前缀
- 5.播放实现
- 6.自动旋屏
- 2. 大体思路
- 1. 不同播放器不同布局, 统一接口多个回调
- 1.面向接口, 抽象代码
- 2.面向复杂, 抽象配置
- 2. 动态修改播放参数,回传到播放器/界面等外部
- 1. IntentHelper
- 2. 实现代理方法
- 3. 参数更新
- 4. 处理更新
- 3.将显示拆分为 视频显示 和 字幕显示等
- 1. 视频显示
- 2. 字幕显示
- 4. touch里面的一些平滑实现
- 1.明确需求
- 2.触摸的范围检测:
- 3.视差因子(提升滑动体验):
- 4.提升触摸的UI体验(滑动平滑不生硬)
- 5.滑动只计算数值,实现交给外部
- 3. 部分实现
- 1. activity入口代码
- 2. 锁/解锁定当前屏幕方向
- 3. 接口和实现
- 4.触摸完整代码
- 5.控制面板消失时加一些动画过渡,效果稍微好些
- 4. 资源文件
- 1. seekbar样式自定义:
- 2. 水波纹效果的版本兼容
- 3. 格式问题
- 4. 沉浸式状态栏
基础功能:
播放不同的 分辨率
/字幕
/倍速
/初始进度
等
在屏幕中间双击: 切换播放
/暂停
在屏幕中间滑动: 拖动进度
在屏幕左侧滑动: 控制亮度
在屏幕右侧滑动: 控制声音
点击锁定按钮: 锁定
当前所有操作
点击上一个
/下一个
/播放
/暂定
/停止
: 执行对应动作
横竖屏方向自动切换
有时间的话会继续开发......
完成效果:
- 自动旋屏: https://blog.csdn.net/intbird/article/details/109077762
- 部分截图:
Github源码地址: https://github.com/intbird/VideoPlayerLib
文章来自:http://blog.csdn.net/intbird 转载请说明出处
结构解析:
0.权限检查: 读取媒体权限
1.播放器层: 播放器的接口 + 实现
2.触控层: 触摸区域识别 + 手势识别 + 触摸灵敏度和进度反馈
3.控制面板层: 可视UI按钮(上一个,下一个,播放/暂停/停止)
4.锁定层: 锁(锁播放器+ 锁触控 + 锁面板+ 锁屏幕方向等)
代码结构:
用mvc简单实现一下,有空了可以把view这层在做层封装,方便后续更换UI的最小代价
1.项目结构
1.api: 使用Fragment(推荐此方式)
VideoPlayerFragment.newInstance(array, index, style, autoPlay)
2.api: 使用Activity(模块解耦实现)
ServicesLoader.load(IVideoPlayer::class.java)?.startActivity(this,array, index, autoPlay)
显示声明外部api和实现默认的api
3.总体结构
逐步升级, 代码结构有些许调整
4.资源前缀
5.播放实现
- MediaPlayerImpl
intbird.soft.lib.video.player.main.player.player.MediaPlayerImpl
- ExoPlayerImpl(有空会继续实现)
intbird.soft.lib.video.player.main.player.player.ExoPlayerImpl
6.自动旋屏
自动旋转视频和抖动恢复屏幕方向 : https://blog.csdn.net/intbird/article/details/109077762
2. 大体思路
1. 不同播放器不同布局, 统一接口多个回调
1.面向接口, 抽象代码
private var playerCall: PlayerCallbacks? = nullprivate var playerView: MediaViewInfo<out View, out View>? = nullprivate var player: IPlayer? = nullprivate fun instanceMediaPlayer(mediaPlayerType: MediaPlayerType?) {when(mediaPlayerType) {MediaPlayerType.PLAYER_STYLE_1, MediaPlayerType.PLAYER_STYLE_2 -> {playerCall = PlayerCallbacks(playerCallback, videoPlayerCallback)playerView = MediaViewProvider(view).by(mediaPlayerType)player = MediaPlayerImpl(getInternalActivity(), playerView?.display as? TextureView, subtitleText, intentHelper, playerCall)}MediaPlayerType.PLAYER_STYLE_3 -> {playerCall = PlayerCallbacks(playerCallback, videoPlayerCallback)playerView = MediaViewProvider(view).by(mediaPlayerType)player = ExoPlayerImpl(getInternalActivity(), playerView?.display as? TextureView, playerCall)}}}
2.面向复杂, 抽象配置
通过一个UI的配置项,强制让不同类型的播放自己选择如何和界面关联
enum class MediaPlayerType(open val layoutDisplay: Int,open val viewDisplay: Int,open val layoutControl: Int,open val viewControl: Int
) {PLAYER_STYLE_1(R.layout.lib_media_player_diaplay_texture,R.id.textureView,R.layout.lib_media_player_control_style_1,R.id.mediaRootControl),PLAYER_STYLE_2(R.layout.lib_media_player_diaplay_texture,R.id.textureView,R.layout.lib_media_player_control_style_2,R.id.mediaRootControl),PLAYER_STYLE_3(R.layout.lib_media_player_diaplay_ijkplayerR.id.ijkplayer,R.layout.lib_media_player_control_style_2,R.id.mediaRootControl)
}
2. 动态修改播放参数,回传到播放器/界面等外部
1. IntentHelper
IntentHelper: 获取外部传入的Intent列表 的 某个Index
进而进行组装当前播放的数据
但:
我想如果一个player实现类,他的数据不是外部传入的
那么:
他也可以替换掉这个IntentHelper,在少量修改后要
能将 切换播放/上一首/下一首 等改为自己的内部方法
始终根据index获取数据
2. 实现代理方法
3. 参数更新
MediaItemInfoRecord: 通过它来保存播放状态,
以便在切换分辨率,字幕等需要强行进行状态改变后
继续操作前的进度进行播放
onReceivePlayFile: 这个在收到播放参数变更时会通知到播放器/控制层等等
4. 处理更新
override fun onReceivePlayFile(reload: Boolean, mediaFileInfo: MediaFileInfo?) {if (null == mediaFileInfo) returnif (reload) {player?.prepare(mediaFileInfo)} else {player?.onParamsChange(mediaFileInfo)}videoControlController?.onParamsChange(mediaFileInfo)}
3.将显示拆分为 视频显示 和 字幕显示等
1. 视频显示
MediaPlayer用surface作为显示器
但
: 如果用第三方播放器,显示可能为其他控件,比如一个webview播放器
所以
: 则要预留出接口, 方便替换
class TextureDisplay(private val iDisplay: IDisplay) : TextureView.SurfaceTextureListener {override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture?,width: Int,height: Int) {}override fun onSurfaceTextureUpdated(surface: SurfaceTexture?) {}override fun onSurfaceTextureDestroyed(surface: SurfaceTexture?): Boolean {return true}override fun onSurfaceTextureAvailable(surface: SurfaceTexture?, width: Int, height: Int) {iDisplay.displayStateChange(true)}
}
2. 字幕显示
MediaPlayer添加字幕为TimedText或者Subtitle
但
: 如果自己解析的话,则可能是另外一种实现方式
所以
: 将字幕显示操作绑定到不同的实现方式上,方便后续替换
可以: 从github找一个字幕实现库进行替换实现
4. touch里面的一些平滑实现
1.明确需求
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.如果不实时预览,则需要考虑如何实现
2.触摸的范围检测:
private var allowXAlixRange: Rect? = nullprivate var allowYAlixRangeLeft: Rect? = nullprivate var allowYAlixRangeRight: Rect? = null
3.视差因子(提升滑动体验):
比如滑动多长距离才能对应1个音量或者1个进度的一个百分比
// 进度视差因子private val parallaxX = 1f// 音量视差因子private val parallaxYVolume = 4.4f// 亮度视差因子private val parallaxYLight = 4.4f
4.提升触摸的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 = 100if (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()}
5.滑动只计算数值,实现交给外部
音量/亮度如何调节实现交给外部实现
后面时间多些了可以把view层也做一层抽离,目前问题也不大
3. 部分实现
1. activity入口代码
class VideoPlayerActivity : Activity(), ILockExecute {companion object {var EXTRA_PLAYER_TYPE = "videoType"var EXTRA_FILE_URLS = "videoUrls"var EXTRA_FILE_INDEX = "videoIndex"var EXTRA_PLAYER_AUTO_PLAY = "videoAutoPlay"fun newInstance(playList: ArrayList<MediaPlayItem>?, playIndex: Int, playerType: MediaPlayerType, autoPlay:Boolean): VideoPlayerFragment {val fragment = VideoPlayerFragment()var args = fragment.argumentsif (null == args) args = Bundle()args.putParcelableArrayList(EXTRA_FILE_URLS, playList)args.putInt(EXTRA_FILE_INDEX, playIndex)args.putSerializable(EXTRA_PLAYER_TYPE, playerType)args.putBoolean(EXTRA_PLAYER_AUTO_PLAY, autoPlay)fragment.arguments = argsreturn fragment}}![在这里插入图片描述](https://img-blog.csdnimg.cn/20200922162840799.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2ludGJpcmQ=,size_16,color_FFFFFF,t_70#pic_center)...private var player: IPlayer? = nullprivate var locker: LockController? = nullprivate var videoTouchController: TouchController? = nullprivate var videoControlController: ControlController? = nulloverride fun onViewCreated(view: View, savedInstanceState: Bundle?) {super.onViewCreated(view, savedInstanceState)intentHelper = MediaIntentHelper(arguments, intentCallback)instanceMediaPlayer(intentHelper?.mediaPlayerType)locker = LockController(ivPopLock)videoTouchController = TouchController(player, locker, touchCallback, layoutTouchPanel)videoControlController = ControlController(player, locker, controlCallback, playerView?.control)locker?.addExecute(videoTouchController)?.addExecute(videoControlController)?.addExecute(this)executeLock(false)handBackPressed()}private fun instanceMediaPlayer(mediaPlayerType: MediaPlayerType?) {when(mediaPlayerType) {MediaPlayerType.PLAYER_STYLE_1, MediaPlayerType.PLAYER_STYLE_2 -> {playerCall = PlayerCallbacks(playerCallback, videoPlayerCallback)playerView = MediaViewProvider(view).by(mediaPlayerType)player = MediaPlayerImpl(getInternalActivity(), playerView?.display as? TextureView, subtitleText, intentHelper, playerCall)}MediaPlayerType.PLAYER_STYLE_3 -> {playerCall = PlayerCallbacks(playerCallback, videoPlayerCallback)playerView = MediaViewProvider(view).by(mediaPlayerType)player = ExoPlayerImpl(getInternalActivity(), playerView?.display as? TextureView, playerCall)}}}
2. 锁/解锁定当前屏幕方向
这里有个待实现的是监听OrientationEventListener,
类似iPad抖动一下屏幕恢复和手机一致的方向
private fun calScreenOrientation(activity: Activity): Int {val display = activity.windowManager.defaultDisplayreturn 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(): Booleanfun getCurrentTime(): Longfun 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 mediaTotalTimeget() = player?.getTotalTime()?: 0Lprivate val mediaCurrentTimeget() = player?.getCurrentTime()?: 0Linit {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 = 0fprivate var lastTouchEventY: Float = 0fprivate 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? = nullprivate var allowXAlixMoveBound: PlayerMoveBound? = PlayerMoveBound(20, 20)// 纵向滑动控制范围private var allowYAlixRangeLeft: Rect? = nullprivate var allowYAlixRangeRight: Rect? = nullprivate var allowYAlixMoveBound: PlayerMoveBound? = PlayerMoveBound(20, 20)// 进度缓存private var lastProgressInfo = ProgressInfo()// 音量缓存private var adjustVolumeInfo = AdjustInfo()// 亮度缓存private var adjustBrightnessInfo = AdjustInfo()fun viewSizeChange() {allowXAlixRange = nullallowYAlixRangeLeft = nullallowYAlixRangeRight = null}override fun onTouch(v: View?, event: MotionEvent?): Boolean {val viewWidth = v?.width ?: 0val viewHeight = v?.height ?: 0// 不应用滑动if (viewWidth == 0 || viewHeight == 0) {return false}when (event?.actionMasked) {MotionEvent.ACTION_DOWN -> {lastTouchEventX = event.xlastTouchEventY = event.yhandleTouchDown(viewWidth, viewHeight)}MotionEvent.ACTION_MOVE -> {val distanceX = event.x - lastTouchEventXval distanceY = event.y - lastTouchEventYreturn 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_PROGRESSvideoTouchCallback.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 = falseadjustVolumeInfo.available = falseadjustBrightnessInfo.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.progressUIviewImpl.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)// 调整UIif (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.progressUIviewImpl.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)// 调整UIif (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.progressUIviewImpl.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不生效
androidx.appcompat.widget.AppCompatSeekBar
2.高版本快捷修改bar颜色api:
<style><item name="android:colorControlActivated">#1a237e</item><item name="android:colorControlNormal">#00b0ff</item></style>
2. 水波纹效果的版本兼容
3. 格式问题
MediaPlayer部分格式不支持,比如 av01
Exoplayer 文档: https://exoplayer.dev/hello-world.html
4. 沉浸式状态栏
override fun onWindowFocusChanged(hasFocus: Boolean) {super.onWindowFocusChanged(hasFocus)if (hasFocus) hideSystemUI()}private fun hideSystemUI() {window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_IMMERSIVE_STICKYor View.SYSTEM_UI_FLAG_LAYOUT_STABLEor View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATIONor View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN// Hide the nav bar and status baror View.SYSTEM_UI_FLAG_HIDE_NAVIGATIONor View.SYSTEM_UI_FLAG_FULLSCREEN)}private fun showSystemUI() {window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLEor View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATIONor View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)}
<style name="lib_media_playerNoTitleBar" parent="Theme.AppCompat.Light.NoActionBar"><item name="android:windowNoTitle">true</item><item name="android:windowActionBar">false</item><item name="android:windowFullscreen">true</item><item name="android:windowContentOverlay">@null</item></style>
后面想到什么再补充以下.
End.
Github源码地址: https://github.com/intbird/VideoPlayerLib
文章来自:http://blog.csdn.net/intbird 转载请说明出处
android media player实现一个可手势滑动控制 + 可以调节分辨率|字幕|倍速的视频播放器(MediaPlayer + ExoPlayer实现)相关推荐
- 酷播云html5倍速功能视频播放器,Chrome扩展推荐:一个能16倍速播放的免费视频倍速播放器...
Video Speed Controller 长时间观看看网课或者影视剧时,啰嗦的内容很容易令人疲劳. 因此许多人在观看视频时,常常习惯将播放速度提升至1.3~1.5倍来补偿视觉接收的差异. 虽然某些 ...
- android 播放器 wav 无法播放,对于Android媒体播放器mp3与wav(For android media player mp3 vs. wav)...
对于Android媒体播放器mp3与wav(For android media player mp3 vs. wav) 我想知道在Android媒体播放器上加载和播放小wav是否比较快的小文件更快. ...
- 带有SeekBar的Android Media Player歌曲
In this tutorial, we'll use the MediaPlayer class to implement a basic Audio Player in our Android A ...
- android vr播放器 开发,Android应用开发之Android VR Player(全景视频播放器)- ExoPlayer播放器MPEG-DASH视频播放...
本文将带你了解Android应用开发之Android VR Player(全景视频播放器)- ExoPlayer播放器MPEG-DASH视频播放,希望本文对大家学Android有所帮助. Androi ...
- Android视频播放器Google Exoplayer
最近开发的项目涉及音频.视频播放,搜索了解到ExoPlayer2.x可以很好的满足需求,现在跟大家分享一下. ExoPlayer库的核心是ExoPlayer接口.ExoPlayer 接口暴露了传统的 ...
- android music player实现一个可随机/顺序播放的可加载专辑图片的音频播放器(MediaPlayer)
音乐播放器 1. 媒体交互框架 1. 服务端 2. 客户端 3. 两端消息交互 2. 播放模式 1. 单曲循环和整体循环 2. 随机播放和顺序播放 3. 几个问题: 1. 合并播放模式 2. 播放指定 ...
- android media player setlooping,Android Mediaplayer-一次播放铃声
我发现了使用事件的简单解决方案-确认答案,因为它可以解决您的问题:) mediaPlayer.setOnSeekCompleteListener(new MediaPlayer.OnSeekCompl ...
- android体感游戏主机,体感手势引擎+智能电视机顶盒+3D深度摄像头,速盟打造基于Android的体感游戏平台#36氪开放日#...
相信36氪的读者对Kinect 一定有所了解.这是由微软开发,应用于 Xbox 360 主机的一种3D体感摄影机,利用即时动态捕捉.影像辨识.麦克风输入.语音辨识等功能带给玩家"免控制器的游 ...
- android能播放4k视频格式,安卓APP,无广告支持多种格式的万能视频播放器
原标题:安卓APP,无广告支持多种格式的万能视频播放器 万能视频播放器 万能视频播放器是一款专业的视频播放工具.它支持所有视频格式,支持 4K/超高清视频文件,并且能够高清播放.它是安卓手机和平板上欣 ...
最新文章
- clone database and rename
- JSONP跨域请求数据报错 “Unexpected token :”的解决办法
- bzoj2194 快速傅立叶之二
- poj3342Party at Hali-Bula(树形dp)
- OpenStack vlan教程 (操作篇)
- linux网络服务偶尔失效,判断linux下的网络服务是否正常启动
- python模块:运行机制与编写方法
- linux 运行jar main,Maven打包生成jar包并在linux下启动main方法
- 这届 360 公关不行
- 如何下载MySQL的驱动包
- 企业私有云搭建与作用
- 教学向|如何快速入门maya制作动画,萌新也能冲
- 机器学习经典模型简单使用及归一化(标准化)影响
- [BIM]BIM中IFD介绍
- 计算机网络英语作文150字,微信投票的英语,写一篇关于网络投票看法的英语作文150字左右...
- filebeat k8s健康探针
- OpenCV小案例(2)——判断一张图片中多少种颜色
- 用Python助女神发朋友圈
- 考研数学:罗尔定理的推论
- DevOps ACA 阿里云效持续交付流水线(十)
热门文章
- ArcGIS的符号选择器(Symbol Selector)为空的解决办法
- “消费盲返”为什么可以在短短几天迅速爆火?
- 企业服务器固态硬盘寿命,SSD固态硬盘使用寿命短?_企业存储技术与评测-中关村在线...
- php is numeric用法,PHP使用 is_numeric的实例解析
- 自信心受挫,该如何让项目团队成员重新振作起来
- 免疫系统与冠状病毒之争:抗体水平下降时,T细胞会支持你
- FlashDB移植与应用
- 爱奇艺体育获5亿元战略融资 ,IDG资本、汇盈博润领投
- 如何在DW中运行PHP文件
- hashcat破解WiFi显示No hashes loaded的解决方法