前言
平时项目中视频播映器运用饺子播映器 ,但某次项目中,无法播映后台视频。猜想是视频格式问题,之后尝试了几种播映器后,终究决议运用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…