开发需求记录:自定义视频UI界面,全屏,倍速,手势控制(亮度,音量,进度),视频截图
作者:访客发布时间:2023-12-18分类:程序开发学习浏览:100
前言
平时项目中视频播放器使用饺子播放器 ,但某次项目中,无法播放后台视频。猜测是视频格式问题,之后尝试了几种播放器后,最终决定使用ExoPlayer实现。之后根据设计样式,对播放器UI进行修改,并添加视频全屏与非全屏切换功能。项目完成后想着进一步完善,方便下次使用。最终模仿常见的视频播放器功能,对自定义视频播放器功能进行补全。完善后视频播放器效果如下:
功能分析
- 视频播放器UI
- 视频全屏与非全屏切换
- 视频倍速控制
- 手势控制视频,亮度,音量,视频进度
- 视频设置界面
代码实现
下面讲解的示例代码大多是从完整代码中截取出来,并进行了一些修改,例如讲解某功能时候,与该功能不相关的代码一般采用//...或者注解来替换。完整代码可在Github中下载。文章末尾也会贴出部分功能实现的完整代码。
视频播放器UI
ExoPlayer自定义较简单UI样式。可以通过创建exo_playback_control_view.xml文件,将xml文件名添加使用PlayerView控件的controller_layout_id属性。该属性用于指定自定义控制器布局,代码如下:
<com.google.android.exoplayer2.ui.PlayerView
android:id="@+id/player_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:surface_type="texture_view"
app:controller_layout_id="@layout/exo_playback_control_view"/>
exo_playback_control_view.xml文件内则可以定义视频控制器相关控件,在该xml文件中设置视频播放/暂停按钮,视频进度条等内容。该xml中需要注意下图红框部分控件 框中的控件为,视频播放按钮(ImageView),视频暂停按钮(ImageView),视频当前进度(TextView),视频总进度(TextView),视频进度条(com.google.android.exoplayer2.ui.DefaultTimeBar)。上述控件特殊的点在于,控件的id与PlayerView关联的播放控件的id相同。 例如进度条控件为com.google.android.exoplayer2.ui.DefaultTimeBar,且id为exo_progress时。只要在xml设置了进度条的样式,ExoPlayer就会处理进度条逻辑和样式。不需要在Activity或Fragment写额外代码控制进度条显示逻辑。例如下面是一个exo_playback_control_view.xml文件内的com.google.android.exoplayer2.ui.DefaultTimeBar控件,该控件是ExoPlayer默认视频进度条控件,buffered_color设置缓冲部分颜色,played_color已播放部分颜色,unplayed_color未播放部分颜色。
<com.google.android.exoplayer2.ui.DefaultTimeBar
android:id="@id/exo_progress"
android:layout_width="0dp"
android:layout_height="26dp"
android:layout_weight="1"
app:buffered_color="@android:color/darker_gray"
app:layout_constraintTop_toTopOf="@+id/bottom_line"
app:layout_constraintBottom_toBottomOf="@+id/bottom_line"
app:layout_constraintLeft_toRightOf="@id/exo_duration"
app:layout_constraintRight_toLeftOf="@id/btn_volume"
app:played_color="#FFDE81"
app:unplayed_color="@android:color/black" />
之后ExoPlayer的PlayerView的controller_layout_id属性将exo_playback_control_view.xml文件添加上就好了。下面是一些ExoPlayer常见控件的id,可以根据需求使用。
id | 描述 |
---|---|
exo_play | 播放按钮 |
exo_pause | 暂停按钮 |
exo_rew | 快退按钮 |
exo_ffwd | 快进按钮 |
exo_prev | 上一个视频按钮 |
exo_next | 下一个视频按钮 |
exo_duration | 显示视频总时长的文本控件 |
exo_position | 显示当前播放时长的文本控件 |
exo_progress | 显示视频进度的进度条控件 |
exo_controller_placeholder | 放置自定义控制器的容器控件 |
想知道更多的话,可以下载源代码,找到exo_playback_control_view.xml,找到控件id为exo_xxx,这种格式的控件,之后鼠标停留到exo_xxx上,按住ctrl键,点击exo_xxx,进入到values.xml,里面有其他的默认视频控件id。
上面特殊的几个控件讲完,剩下的是需要自行处理的控件,例如下图中将视频控制界面分为四个部分。
1.视频顶部栏
退出键,视频标题,设置按钮。并且顶部栏还有从上到下,从黑到透明的装饰色背景
2.视频底部栏
视频播放,视频进度文本,视频总长度文本,进度条,音量,全屏按钮,并且底部栏还有从下到上,从黑到透明的装饰色背景
3.手势显示界面
根据手指划动的轨迹决定界面是显示控制视频亮度/音量/进度
4.视频设置界面
视频其他设置,目前是视频倍速和视频截图。
视频全屏与非全屏切换
全屏与非全屏切换是通过改变屏幕方向与PlayerView的宽高实现的。需要注意点是,进入全屏时候,保存非全屏状态下,PlayerView的宽高,保存好后将PlayerView的宽高设置为MATCH_PARENT来填充满父容器,退出全屏时候,将屏幕方向变为竖屏,且宽高恢复为之前保存的宽高,全屏与非全屏的实现代码如下:
/**
* 离开全屏
*/
private fun exitFullscreen() {
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
binding.apply {
//退出全屏时候,将宽高重新赋值回来
val layoutParams = playerView.layoutParams
layoutParams.width = originalWidth
layoutParams.height = originalHeight
playerView.layoutParams = layoutParams
}
}
/**
* 进入全屏
*/
private fun enterFullscreen() {
if(originalWidth == 0){
originalWidth = binding.playerView.width
originalHeight = binding.playerView.height
}
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
// 设置 ExoPlayerView 的全屏下相关设置
binding.apply {
val layoutParams = playerView.layoutParams
layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT
layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
playerView.layoutParams = layoutParams
}
}
视频倍速控制
视频倍速改变功能是通过PlayerView的playbackParameters类属性实现。该属性包含两个属性speed和pitch。分别表示播放速度和音频音调。更改播放与音频速度可通过playerView.playbackParameters = PlaybackParameters(2f),这样形式的代码更改。
更改倍速代码确定好后,下面对UI进行自定义。演示的GIF中可以看到,控制视频倍速的控件是一个组合控件,由一个垂直进度条和一个TexiView组成。该组合控件逻辑并不复杂,较复杂的是自定义垂直进度条控件实现。(组合控件和垂直进度条的完整代码会在文章末尾贴出,或者可以下载源码查看)。
垂直进度条
垂直进度条UI实现是通过,在onDraw方法方法中,按照白色实心圆角矩形,蓝色边框圆角矩形,蓝色实心进度条圆角矩形,大圆,小圆,上述顺序进行绘制。onDraw方法代码如下:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawRoundRect(progressStrokeRectLeft, progressStrokeRectTop, progressStrokeRectRight, progressStrokeRectBottom, rx, ry, fillRectPaint);
// canvas.drawRoundRect(progressStrokeRectLeft, progressStrokeRectTop, progressStrokeRectRight, progressStrokeRectBottom, rx, ry, strokeRectPint);
canvas.drawRoundRect(progressStrokeRectLeft + strokeWidth/2, progressStrokeRectTop + strokeWidth/2, progressStrokeRectRight - strokeWidth/2, progressStrokeRectBottom - strokeWidth/2, rx, ry, strokeRectPint);
canvas.drawRoundRect(progressStrokeRectLeft, progressTop, progressStrokeRectRight, progressStrokeRectBottom, rx, ry, progressPaint);
canvas.drawCircle(circleX, circleY, bigCircleRadius, bigCirclePaint);
canvas.drawCircle(circleX, circleY, smallCircleRadius, smallCirclePaint);
}
绘制需要注意点是,如果画笔的样式是(Paint.Style.STROKE)描边样式的话,画笔描边,是按照你指定的位置向两边开始绘制描边。例如画一条蓝色垂直的线从(x1,y1)点到(x2,y2)点。黑色线是将(x1,y1)点到(x2,y2)点的一条极细的线。当画笔描边的时候,是沿着这条线向两边开始延伸。有时画笔为描边样式,绘制的UI效果与预期不一样可能这个问题导致的。 发现这个问题是,在竖直垂直进度条的水平间隙为0的时候发现,当为0时候,描边有一部分超出了View,当把描边宽度也纳入描边矩形坐标计算的时候才解决这个问题。上面代码中,注释的代码就是出现描边绘制问题的代码。对比图如下:
在onDraw方法中使用到矩形和圆对应的坐标,坐标计算是在onTouchEvent方法内进行计算,在手指移动事件(ACTION_MOVE)里面的通过本次手指Y坐标-上次手指Y坐标,获取手指移动时在Y轴上的变换值,之后将Y轴变化值作用于,蓝色实心进度条圆角矩形的Top值,大圆的Y,小圆的Y。获取进度条进度的话,可以通过蓝色实心进度条圆角矩形的Bottom-Top/Height实现。对应代码如下
progress = (progressStrokeRectBottom - progressTop)/(progressStrokeRectBottom - progressStrokeRectTop);
之后在onTouchEvent方法的return前面添加postInvalidate()让View重绘。完整的onTouchEvent代码如下:
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
startY = event.getY();
handler.removeCallbacks(runnable);
break;
case MotionEvent.ACTION_MOVE:
endY = event.getY();
float changeY = endY - startY;
progressTop += changeY;
if (progressTop < progressStrokeRectTop) {
progressTop = progressStrokeRectTop;
} else if (progressTop > progressStrokeRectBottom) {
progressTop = progressStrokeRectBottom;
}
circleY = progressTop;
startY = endY;
progress = (progressStrokeRectBottom - progressTop)/(progressStrokeRectBottom - progressStrokeRectTop);
if (onListener!=null){
onListener.getProgress(progress);
}
break;
case MotionEvent.ACTION_UP:
if (onListener!=null){
onListener.onFingerUp(progress);
}
handler.postDelayed(runnable,DELAY_TIME);
break;
}
postInvalidate();
return true;
}
进度条适配倍速功能注意
在实现时候遇到下面一些问题,需要注意下。
长时间未操作进度条,隐藏进度条联合控件
垂直进度条在显示后,手指若未点击一段时后会消失,且若手指拖动了进度条,松手后一段时候后View消失。 该功能实现,是垂直进度条View内创建一个Handler和一个接口。代码如下:
public interface OnListener{
void getProgress(float progress);
void onHideParent();
default void onFingerUp(float progress) {
}
}
private OnListener onListener;
public void setOnListener(OnListener onListener){
this.onListener = onListener;
}
private Handler handler = new Handler();
private Runnable runnable = new Runnable() {
@Override
public void run() {
if (onListener!=null){
onListener.onHideParent();
}
}
};
如果看到垂直进度条的onTouchEvent方法,会发现在手指按下时候,会移除定时任务,手指抬起时执行定时任务。定时任务则是通知外部执行垂直进度条组合控件隐藏的代码。
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//...
handler.removeCallbacks(runnable);
break;
case MotionEvent.ACTION_MOVE:
//...
break;
case MotionEvent.ACTION_UP:
//...
handler.postDelayed(runnable,DELAY_TIME);
break;
}
postInvalidate();
return true;
}
ExoPlayer视频倍速不能频繁设置且不能设置为0倍速
该问题容易解决,可以只使用松手时候的进度条进度,以及接收到0进度时,转换成最小的视频倍速即可。
手势控制视频,亮度,音量,视频进度
该部分功能可以分成两部分,一个是手势判断,一个是视频亮度,音量,进度的控制。比较简单的部分是控制视频亮度等功能,下面先从该部分实现开始。
视频亮度,音量,进度的控制
控制视频亮度
首先xml中PlayerView的surface_type属性设置为texture_view。(在xml将surface_type属性设置为texture_view原因是PlayerView的surface_type默认为surface_view,而surface_view不能通过设置透明度的方式改变视频亮度),之后在Activity/Fragment内对PlayeView的videoSurfaceView的alpha设置透明度,透明度范围0f~1f。示例代码如下:
Xml代码
<com.google.android.exoplayer2.ui.PlayerView
android:id="@+id/player_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:resize_mode="fit"
app:controller_layout_id="@layout/exo_playback_control_view"
app:surface_type="texture_view" />
Activity/Fragment内代码
private var brightness = 0f//亮度
//...
binding.playerView.videoSurfaceView?.alpha = brightness
控制视频音量
通过设置ExoPlayer的PlayerView的volume属性控制视频音量。volume的范围是0f~1f。
控制视频进度
通过设置ExoPlayer的PlayerView的seekTo()方法控制视频播放进度,seekTo()方法接收以毫秒为单位的Long类型数据。示例代码如下:
private var seekToPosition = 0L//视频要跳转到的位置
//...
exoPlayer?.seekTo(seekToPosition)
手势判断
该功能需要自己实现PlayerView的触摸事件,在触摸事件中处理手势判断。
在介绍功能前,需要说明下触摸事件使用到的常量
companion object {
const val TAG = "ExoVideoActivity"
const val VOLUME = "volume"
const val BRIGHTNESS = "brightness"
const val PLAY_SPEED = "play_speed"
const val VIDEO_POSITION = "video_id_position"
const val GESTURE_TYPE_VOLUME = "video_volume"//音量修改
const val GESTURE_TYPE_BRIGHTNESS = "video_brightness"//亮度修改
const val GESTURE_TYPE_PROGRESS = "video_progress"//视频进度修改
const val GESTURE_TYPE_NULL = "null"
const val GESTURE_RESPONSE_VIEW_WIGHT_PERCENT = 0.8f//手水平划动时,控制视频进度变化的虚拟的进度条长度占屏幕宽度的百分比 。虚拟的进度条想象中的,实际不存在
const val MAX_PLAY_SPEED = 2f//视频最快倍速 目前2倍速
const val DEF_TIME = 100L//防止手指快速按下抬起 对 手势判断影响的时间
const val MIN_MOVE_DISTANCE = 30//判断手指移动方向最小距离
}
下面简单说下实现思路,首先为了防止手指短暂触摸屏幕导致误触,需要记录手指按下与手指移动时的时间,当小于一定指定时间时,不进行手势判断。示例代码如下:
when (action) {
MotionEvent.ACTION_DOWN -> {
//...
fingerDownTime = System.currentTimeMillis()
}
MotionEvent.ACTION_MOVE -> {
//...
fingerMoveTIme = System.currentTimeMillis()
//防止手指按下抬起过快,对手势行为判断造成误判
if (fingerMoveTIme - fingerDownTime > DEF_TIME){
//手势类型判断 亮度 音量 进度
}
//...
}
MotionEvent.ACTION_UP -> {
//...
}
}
}
若触摸时间大于指定时间。则开始判断手指按下与手指移动时,x轴与y轴哪个先达到指定的变化值。若先是手指按下X-手指移动X>指定值,则手势类型判断为控制视频进度,若先是手指按下Y-手指移动Y>指定值,在根据手指按下时的X坐标判断在屏幕左边还是右边。在屏幕左边为控制亮度,屏幕右边控制音量。示例代码如下:
when (action) {
MotionEvent.ACTION_DOWN -> {
//..
downX = event.x
downY = event.y
gestureType = GESTURE_TYPE_NULL
fingerDownTime = System.currentTimeMillis()
}
MotionEvent.ACTION_MOVE -> {
moveX = event.x
moveY = event.y
fingerMoveTIme = System.currentTimeMillis()
//防止手指按下抬起过快,对手势行为判断造成误判
if (fingerMoveTIme - fingerDownTime > DEF_TIME){
//判断当前是什么事件 进度 音量 亮度
when (gestureType) {
GESTURE_TYPE_VOLUME -> {
//手势为控制音量时,对应代码
}
GESTURE_TYPE_BRIGHTNESS -> {
//手势为控制亮度时,对应代码
}
GESTURE_TYPE_PROGRESS -> {
//手势为控制视频进度时,对应代码
}
else -> {
//通过X和Y轴移动距离判断是那种手势方式
if(abs(downX - moveX) > MIN_MOVE_DISTANCE){//左右划
gestureType = GESTURE_TYPE_PROGRESS//视频进度
}else if(abs(downY - moveY) > MIN_MOVE_DISTANCE){//上下划
if (startX < playerViewWidth / 2){
gestureType = GESTURE_TYPE_BRIGHTNESS//视频亮度
}else{
gestureType = GESTURE_TYPE_VOLUME//视频音量
}
}
}
}
}
//...
}
MotionEvent.ACTION_UP -> {
//...
}
}
视频音量
当判断好手势类型后,之后在手指移动事件中计算对于事件需要的属性值。其中音量变化与亮度变化类似,下面以音量变化为例。首先看下相关的代码:
when (action) {
MotionEvent.ACTION_DOWN -> {
startX = event.x
startY = event.y
//...
playerViewWidth = binding.playerView.width
playerViewHeight = binding.playerView.height
gestureType = GESTURE_TYPE_NULL
//...
fingerDownTime = System.currentTimeMillis()
}
MotionEvent.ACTION_MOVE -> {
endX = event.x
endY = event.y
//...
fingerMoveTIme = System.currentTimeMillis()
//防止手指按下抬起过快,对手势行为判断造成误判
if (fingerMoveTIme - fingerDownTime > DEF_TIME){
//判断当前是什么事件 进度 音量 亮度
when (gestureType) {
GESTURE_TYPE_VOLUME -> {
volumeChange = (startY - endY) / (playerViewHeight.toFloat() / 2f)//音量变化
volume += volumeChange
if (volume > 1f){
volume = 1f
}else if (volume < 0f){
volume = 0f
}
player?.volume = volume
tvMsg1.text = "${(volume*100).toInt()}%"
imgGestureType.setImageResource(R.mipmap.volume_64_white)
gestureViewSet(gestureType)
}
GESTURE_TYPE_BRIGHTNESS -> {
//...
}
GESTURE_TYPE_PROGRESS -> {
//...
}
else -> {
//...
}
}
}
//下面将startX重新赋值
startX = endX
startY = endY
}
MotionEvent.ACTION_UP -> {
//...
}
}
根据上面代码可以看出,当手势为控制音量,每次手指移动时,音量变化百分比 = ( 手指上次移动Y - 手指本次Y ) / PlayerView高度一半。对应代码:
volumeChange = (startY - endY) / (playerViewHeight.toFloat() / 2f)//音量变化
之后将变化的音量作用于视频音量,当视频音量大于1f,将视频音量改为1f,小于0f,则改为0f。之后将音量作用于PlayerView,并设置与手势相关View显示的文本内容和图片。对应代码如下:
volume += volumeChange
if (volume > 1f){
volume = 1f
}else if (volume < 0f){
volume = 0f
}
player?.volume = volume
tvMsg1.text = "${(volume*100).toInt()}%"
imgGestureType.setImageResource(R.mipmap.volume_64_white)
gestureViewSet(gestureType)
gestureViewSet()方法是根据手势类型,决定视频中间的手势View应该显示什么内容。
上图可以看到,手势View由一个ImageView两个TextView构成,音量与亮度使用一个ImageVIew与一个TextView,视频进度则是两个TextView。因此在手指移动时候需要考虑哪些View该显示或隐藏。
并且手势View在exo_playback_control_view.xml文件中。当手指触摸屏幕时候,应该是xml文件内所有的View都显示。如下图箭头所指顶部与底部的View。
因此手指触摸导致PlayerView的controlView显示时候(controlView就是exo_playback_control_view.xml内对应的所有View),需要隐藏顶部与底部的View,显示中间的手势View。gestureViewSet()方法对应代码如下:
/** 手势View该显示内容设置 */
private fun gestureViewSet(type:String) {
if (!isVisibleGestureView) {
clGestureType.visibility = View.VISIBLE
when(type){
GESTURE_TYPE_BRIGHTNESS, GESTURE_TYPE_VOLUME->{
imgGestureType.visibility = View.VISIBLE
tvMsg1.visibility = View.VISIBLE
}
GESTURE_TYPE_PROGRESS ->{
imgGestureType.visibility = View.GONE
tvMsg1.visibility = View.VISIBLE
tvMsg2.visibility = View.VISIBLE
}
}
if (!controlView.isVisible) {
controlView.show()
clVideoTop.visibility = View.GONE
clVideoBottom.visibility = View.GONE
}
isVisibleGestureView = true
}
}
当松手时候,就需要将controlView的顶部与底部的View变为可见。同时隐藏手势View。若controlView的顶部与底部的View不可见,轻触PlayerView是不会显示的。对应代码如下:
MotionEvent.ACTION_UP -> {
when(gestureType){//松手时候改变视频进度,若放在移动中,触发比较频繁。可根据需求修改
GESTURE_TYPE_PROGRESS ->{
exoPlayer?.seekTo(seekToPosition)
}
}
//手指抬起时候,一些界面消失
if (isVisibleSetView) {//设置界面存在,先处理设置界面
videoSetViewSwitch(1000)
} else if (vpLayout.visibility == View.VISIBLE){
vpLayout.visibility = View.GONE
} else if(isVisibleGestureView){//手势控件
if (clVideoTop.visibility == View.GONE){//若顶部栏原本不可见,后面隐藏视频控制界面
controlView.hide()
}
clVideoTop.visibility = View.VISIBLE //这里将顶部与底部控件可见,为了手势操作完后,点击屏幕显示顶部与底部视频栏
clVideoBottom.visibility = View.VISIBLE
clGestureType.visibility = View.GONE
imgGestureType.visibility = View.GONE
tvMsg1.visibility = View.GONE
tvMsg2.visibility = View.GONE
isVisibleGestureView = false
} else {
if (controlView.isVisible) {
controlView.hide()
} else {
controlView.show()
}
}
}
视频倍速
视频倍速功能,是手指松手后改变视频播放位置,因此需要记录手指按下时候视频进度。之所以手指松开才设置视频进度,是担心频繁使用PlayerView的seekTo()方法会出问题。之后手指移动时候,需要考虑哪些View该显示隐藏(调用gestureViewSet()方法)。并计算出,需要跳至哪个时刻,以及具体跳过的时间。同时对跳至的时刻做判断。时刻不能是负数也不能超过视频长度。手指松开时候,在使用seekTo()方法跳到指定时刻。对应代码如下:
event?.apply {
when (action) {
MotionEvent.ACTION_DOWN -> {
downX = event.x
downY = event.y
//...
fingerDownVideoPosition = (player?.currentPosition ?: 0L)
skipVideoDuration = 0L
fingerDownTime = System.currentTimeMillis()
}
MotionEvent.ACTION_MOVE -> {
//...
moveX = event.x
moveY = event.y
fingerMoveTIme = System.currentTimeMillis()
//防止手指按下抬起过快,对手势行为判断造成误判
if (fingerMoveTIme - fingerDownTime > DEF_TIME){
//判断当前是什么事件 进度 音量 亮度
when (gestureType) {
GESTURE_TYPE_VOLUME -> {
//...
}
GESTURE_TYPE_BRIGHTNESS -> {
//...
}
GESTURE_TYPE_PROGRESS -> {
gestureViewSet(gestureType)
//变化进度View宽度
skipVideoDuration = ((moveX - downX) / (playerViewWidth * GESTURE_RESPONSE_VIEW_WIGHT_PERCENT) * (player?.duration ?: 0L)).toLong()
seekToPosition = fingerDownVideoPosition + skipVideoDuration
if (seekToPosition > (player?.duration ?: 0L)){
seekToPosition = player?.duration ?: 0L
skipVideoDuration = (player?.duration ?: 0L) - fingerDownVideoPosition
}else if (seekToPosition < 0L){
seekToPosition = 0
skipVideoDuration = fingerDownVideoPosition
}
tvMsg1.text = "${TimeUtil.formatMillisToHMS(seekToPosition)}/${TimeUtil.formatMillisToHMS(player?.duration ?: 0L)}"
if (downX - moveX > 0) {
tvMsg2.text = "-${TimeUtil.formatMillisToHMS(abs(skipVideoDuration))}"
} else {
tvMsg2.text = "+${TimeUtil.formatMillisToHMS(abs(skipVideoDuration))}"
}
}
else -> {
//...
}
}
}
//...
}
MotionEvent.ACTION_UP -> {
when(gestureType){//松手时候改变视频进度,若放在移动中,触发比较频繁。可根据需求修改
GESTURE_TYPE_PROGRESS ->{
exoPlayer?.seekTo(seekToPosition)
}
}
//... View显示或隐藏相关操作
}
}
}
跳过时长计算规则可以简述为下面的形式。
跳过时长 = ((手指移动X - 手指按下X) / (PlayeView宽度 * 百分比) * 视频时长)
对应代码如下
skipVideoDuration = ((moveX - downX) / (playerViewWidth * GESTURE_RESPONSE_VIEW_WIGHT_PERCENT) * (player?.duration ?: 0L)).toLong()
视频设置界面
视频设置界面实现原理是,可见界面的右侧设置一个GroupView,并设置对应的内容。如下图红色箭头所指部分。 设置界面出现,是通过属性值动画设置ViewGroup的x坐标实现设置界面移动。当点击设置按钮时候,先判断动画是否存在或者动画是否运行中,动画若不存在或未运行,才开始动画相关设置并开启动画。点击设置按钮时,对应的代码如下:
/**
* 视频设置界面切换
* duration 动画持续事件
*/
private fun videoSetViewSwitch(duration: Long) {
findViewById<LinearLayout>(R.id.cl_video_set).apply {
playerViewWidth = binding.playerView.width
if (valueAnimator == null || valueAnimator?.isRunning == false) {//动画不存在或动画未运行
valueAnimator = ValueAnimator.ofInt(
if (isVisibleSetView) this.width else 0,
if (isVisibleSetView) 0 else this.width
)
valueAnimator?.addUpdateListener {
x = (playerViewWidth - it.animatedValue as Int).toFloat()
}
valueAnimator?.duration = duration
valueAnimator?.start()
isVisibleSetView = !isVisibleSetView
}
//此处设置点击方法,防止覆盖的设置按钮点击事件影响。点击拦截
setOnClickListener {
}
}
}
需要注意的是需要重写ViewGroup的点击方法,来拦截点击事件。不然如果设置界面弹出后,如果在点击设置按钮对应位置,会发现又执行了设置按钮点击事件内相关操作。
如果看运行效果图,设置界面,是三个视频截面的按钮,分别使用MediaMetadataRetriever,FFmpeg,和Glide截图。
有三种截图方式原因是,一开始实现截图功能是通过MediaMetadataRetriever实现,这样不需要引入第三方依赖。但这种方式截图,安卓版本大于等于27的可以精确截图,27以下截图不准确。发现不精准后,后想到曾使用Glide做视频预览图,能做预览图,应该就能获取到Bitmap并保存为图片文件。但实现后发现也有低版本安卓截取不准确的问题。最终使用FFmpeg实现视频截图功能。
对三种截图方式做总结的话,MediaMetadataRetriever不需要引入第三方依赖,但有低版本安卓截图不准确问题。Glide本身不是做视频截图,只是能实现功能,这这里仅记录。FFmpeg可以实现高低版本精确截图,但需要导入第三方依赖,且视频截图时间比前两者时间长。
下面就只介绍FFmpeg方式实现截图。 首先导入依赖
implementation 'com.arthenica:ffmpeg-kit-full:5.1'
截图代码
/** 截图 - 通过FFmpeg 为了适配低版本Android */
private fun screenshotByFFmpeg(url: String, timeInMillis: Long, outputPath: String) {
val time = TimeUtil.formatMillisToHMSM(timeInMillis)
val command = "-ss $time -i $url -vframes 1 -q:v 2 $outputPath"
val session = FFmpegKit.execute(command)
if (ReturnCode.isSuccess(session.returnCode)) {
//success
ToastUtil.show("截图成功")
CustomLog.d(TAG, "成功")
} else if (ReturnCode.isCancel(session.returnCode)) {
//cancel
ToastUtil.show("截图失败")
CustomLog.d(TAG, "取消")
} else {
//fail
CustomLog.d(TAG, "失败")
}
}
上面代码中,FFmpeg命令解释如下:
"-ss", timeSeconds.toString(), // 设置截图时间点
"-i", inputVideoPath, // 输入视频文件路径
"-vframes", "1", // 截取一帧图像
"-q:v", "2", // 设置图像质量(可选)
outputImagePath // 输出截图文件路径
其中设置视频截图时间点不能是Long类型数据,而是00:00:00或者00:00:00:000格式。第一个是时:分:秒,第二个是时:分:秒:毫秒。截图使用第二种格式。
总结
自定义视频播放界面,比较多的时间花在了UI和手势判断上。最开始实现手势判断的时候,是使用模拟器测试,鼠标触摸PlayerView的时候,鼠标点一下是手指按下,鼠标移动才能触发手指移动事件。就没有考虑触摸时间问题,手势判断最开始是没有if (fingerMoveTIme - fingerDownTime > DEF_TIME)这个判断。但真机测试时候,手指按下是会触发手指按下与手指移动的。毕竟现实中手指与屏幕接触是一个面,而不是一个点,现实中手指按下时,ACTION_DOWN与ACTION_MOVE事件都触发的。这就导致明明只是想点下屏幕,显示下控制视频的相关控件,结果却是手势响应了。之后将触摸时间考虑进去后,就解决这个问题。还有些其他开发遇到问题。一般我会在代码中注释写出来。
代码地址
GitHub:github.com/SmallCrispy…
- 程序开发学习排行
-
- 1鸿蒙HarmonyOS:Web组件网页白屏检测
- 2HTTPS协议是安全传输,为啥还要再加密?
- 3HarmonyOS鸿蒙应用开发——数据持久化Preferences
- 4记解决MaterialButton背景颜色与设置值不同
- 5鸿蒙HarmonyOS实战-ArkUI组件(RelativeContainer)
- 6鸿蒙HarmonyOS实战-ArkUI组件(Stack)
- 7[Android][NDK][Cmake]一文搞懂Android项目中的Cmake
- 8鸿蒙HarmonyOS实战-ArkUI组件(mediaquery)
- 9鸿蒙HarmonyOS实战-ArkUI组件(GridRow/GridCol)
- 最近发表
-
- WooCommerce最好的WordPress常用插件下载博客插件模块的相关产品
- 羊驼机器人最好的WordPress常用插件下载博客插件模块
- IP信息记录器最好的WordPress常用插件下载博客插件模块
- Linkly for WooCommerce最好的WordPress常用插件下载博客插件模块
- 元素聚合器Forms最好的WordPress常用插件下载博客插件模块
- Promaker Chat 最好的WordPress通用插件下载 博客插件模块
- 自动更新发布日期最好的WordPress常用插件下载博客插件模块
- WordPress官方最好的获取回复WordPress常用插件下载博客插件模块
- Img to rss最好的wordpress常用插件下载博客插件模块
- WPMozo为Elementor最好的WordPress常用插件下载博客插件模块添加精简版