前言

平时项目中视频播映器运用饺子播映器 ,但某次项目中,无法播映后台视频。猜想是视频格式问题,之后尝试了几种播映器后,终究决议运用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中需求留意下图红框部分控件

开发需求记录:自定义视频UI界面,全屏,倍速,手势控制(亮度,音量,进度),视频截图
框中的控件为,视频播映按钮(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。

开发需求记录:自定义视频UI界面,全屏,倍速,手势控制(亮度,音量,进度),视频截图
开发需求记录:自定义视频UI界面,全屏,倍速,手势控制(亮度,音量,进度),视频截图
上面特别的几个控件讲完,剩余的是需求自行处理的控件,例如下图中将视频操控界面分为四个部分。
开发需求记录:自定义视频UI界面,全屏,倍速,手势控制(亮度,音量,进度),视频截图
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效果与预期不一样可能这个问题导致的。

开发需求记录:自定义视频UI界面,全屏,倍速,手势控制(亮度,音量,进度),视频截图
发现这个问题是,在竖直笔直进展条的水平间隙为0的时分发现,当为0时分,描边有一部分超出了View,当把描边宽度也归入描边矩形坐标核算的时分才处理这个问题。上面代码中,注释的代码便是呈现描边制作问题的代码。对比图如下:

开发需求记录:自定义视频UI界面,全屏,倍速,手势控制(亮度,音量,进度),视频截图

在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应该显现什么内容。

开发需求记录:自定义视频UI界面,全屏,倍速,手势控制(亮度,音量,进度),视频截图
上图能够看到,手势View由一个ImageView两个TextView构成,音量与亮度运用一个ImageVIew与一个TextView,视频进展则是两个TextView。因而在手指移动时分需求考虑哪些View该显现或躲藏。
而且手势View在exo_playback_control_view.xml文件中。当手指接触屏幕时分,应该是xml文件内一切的View都显现。如下图箭头所指顶部与底部的View。

开发需求记录:自定义视频UI界面,全屏,倍速,手势控制(亮度,音量,进度),视频截图
因而手指接触导致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,并设置对应的内容。如下图红色箭头所指部分。

开发需求记录:自定义视频UI界面,全屏,倍速,手势控制(亮度,音量,进度),视频截图
设置界面呈现,是经过特点值动画设置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…