最近在做一个视频播放器
现在市场上,一个比较完善的视频播放器
大概具有以下功能:
快进、快退、声音、亮度控制
这一次就根据这几个基础的功能
通过系统的手势控制类GestureDetector来完成
做了一个实用的工具类
只需要简单的配置
就可以实现对视频播放器控件的手势进行监听
工具类内部实现了相关功能,不需要视频播放器自己写
实现了代码解耦,也方便复用
下面就开始介绍这个工具类的使用,以及实现的原理。
先把工具类的实际代码贴上来
public class VideoGestureControlUtil {
private static final String TAG = "VideoGestureControlUtil";
private Context mContext;
private GestureDetector mGestureDetector;//手势类
private MyGestureListener myGestureListener;//手势监听
private OnVideoControlListener videoControlListener;//视频View回调监听
private Rect VideoViewRect = null;//视频控件范围
//状态相关
private static int SCROLL_FLAG = 0;//记录状态
private static final int SCROLL_FLAG_RESET = 0;//初始状态,无任何操作
private static final int SCROLL_FLAG_TOPBOTTOM_LEFT = 1;//左边屏幕上下滑动
private static final int SCROLL_FLAG_TOPBOTTOM_RIGHT = 2;//右边屏幕上下滑动
private static final int SCROLL_FLAG_LEFTRIGHT = 3;//左右滑动
//拖动相关
protected static final float FLIP_DISTANCE = 50;//确定滑动方向的最小滑动距离
private int SCROLL_VIDEO_SCROLL_RANGE = 1000;//拖动范围 0~1000
private float SCROLL_VIDEO_PLAY_RANGE = 0.25f;//视频可拖动部分的范围
//视频进度相关
private float SCROLL_VIDEO_PLAY_INDEX = 0f;//拖动的视频进度
private double videoIndex = 0;//拖动的视频毫秒数
private double newVideoIndex;//拖动结束后视频的位置,单位毫秒
private double SCROLL_VIDEO_PLAYING_INDEX = 0;//视频播放位置
private double SCROLL_VIDEO_PLAYING_INDEX_CATCH = 0;//缓存的视频播放位置(手指按下去的视频播放位置)
private double SCROLL_VIDEO_LENTH = 0;//视频总长度 单位毫秒
//声音和亮度相关
private AudioManager systemService;
private int SCROLL_VIDEO_VOICE_INDEX = 0;//拖动的视频声音
private double SCROLL_VIDEO_LIGHT_INDEX = 0;//拖动的视频亮度
private int SCROLL_VOICE_MAX = 0;//声音总长度
private int SCROLL_LIGHT_MAX = 255;//亮度总长度
private int SCREEN_LIGHT = 0;//屏幕亮度
private int SCREEN_LIGHT_CATCH = 0;//缓存的屏幕亮度
private int SCREEN_VOICE = 0;//音量大小
//弹框部分
private Dialog indexDialog;//弹框
private View videoDialogView;//弹框View
private ImageView indeximg;
private LinearLayout indexll, voicell;
private TextView indextv;
private ImageView voiceLightImg;
private SeekBar voiceLightsb;
private WindowManager.LayoutParams indexDialogLp;
private DisplayMetrics displayMetrics;
public VideoGestureControlUtil(Context context, View view) {
myGestureListener = new MyGestureListener();
this.mContext = context;
systemService = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
SCROLL_VOICE_MAX = systemService.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
mGestureDetector = new GestureDetector(context, myGestureListener);
mGestureDetector.setOnDoubleTapListener(myGestureListener);
LayoutInflater layoutInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
videoDialogView = layoutInflater.inflate(R.layout.video_gesture_dialog, null);
indexll = videoDialogView.findViewById(R.id.indexll);
voicell = videoDialogView.findViewById(R.id.voicell);
indeximg = videoDialogView.findViewById(R.id.indeximg);
indextv = videoDialogView.findViewById(R.id.indextv);
voiceLightImg = videoDialogView.findViewById(R.id.voice_light_img);
voiceLightsb = videoDialogView.findViewById(R.id.voice_light_sb);
indexDialog = new Dialog(context);
/*随意定义个Dialog*/
Window dialogWindow = indexDialog.getWindow();
/*实例化Window*/
indexDialogLp = dialogWindow.getAttributes();
/*实例化Window操作者*/
indexDialogLp.x = 0; // 新位置X坐标
indexDialogLp.y = 0; // 新位置Y坐标
dialogWindow.setGravity(Gravity.CENTER);
dialogWindow.getDecorView().setBackground(null);
dialogWindow.setAttributes(indexDialogLp);
/*放置属性*/
indexDialog.setContentView(videoDialogView, new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.WRAP_CONTENT));
dialogWindow.setDimAmount(0);
dialogWindow.setBackgroundDrawableResource(android.R.color.transparent);
try {
int dividerID = indexDialog.getContext().getResources().getIdentifier("android:id/titleDivider", null, null);
View divider = indexDialog.findViewById(dividerID);
if (divider != null) {
divider.setBackgroundColor(Color.TRANSPARENT);
}
} catch (Exception e) {
//上面的代码,是用来去除Holo主题的蓝色线条
e.printStackTrace();
}
indexDialog.setCanceledOnTouchOutside(false);
}
public boolean touch(MotionEvent event) {
boolean detectedUp = event.getAction() == MotionEvent.ACTION_UP;
if (!mGestureDetector.onTouchEvent(event) && detectedUp) {
//手指抬起时触发
if (SCROLL_FLAG == SCROLL_FLAG_LEFTRIGHT) {
//设置当前播放进度
videoControlListener.setScrollVideoPlayingIndex(SCROLL_VIDEO_PLAYING_INDEX);
}
SCROLL_FLAG = SCROLL_FLAG_RESET;
VideoViewRect = null;
dismissIndexDialog();
} else if (event.getAction() == MotionEvent.ACTION_CANCEL) {
//熄屏时会触发
SCROLL_FLAG = SCROLL_FLAG_RESET;
VideoViewRect = null;
dismissIndexDialog();
}
return true;
}
/*
* 手势监听类
*/
class MyGestureListener extends GestureDetector.SimpleOnGestureListener {
public MyGestureListener() {
super();
}
@Override
public boolean onDoubleTap(MotionEvent e) {
Log.e(TAG, "双击");
videoControlListener.onDoubleTap();
return true;
}
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
Log.e(TAG, "单击");
videoControlListener.onSingleTap();
return true;
}
@Override
public boolean onDoubleTapEvent(MotionEvent e) {
Log.e(TAG, "onDoubleTapEvent");
return true;
}
@Override
public boolean onContextClick(MotionEvent e) {
Log.e(TAG, "onContextClick");
return true;
}
@Override
public boolean onDown(MotionEvent e) {
Log.e(TAG, "onDown");
return true;
}
@Override
public void onShowPress(MotionEvent e) {
Log.e(TAG, "onShowPress");
}
@Override
public boolean onSingleTapUp(MotionEvent e) {
Log.e(TAG, "onSingleTapUp");
return true;
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
switch (SCROLL_FLAG) {
case SCROLL_FLAG_RESET:
WindowManager windowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
displayMetrics = new DisplayMetrics();
windowManager.getDefaultDisplay().getRealMetrics(displayMetrics);
if (e1.getX() < FLIP_DISTANCE || e1.getX() > displayMetrics.widthPixels - FLIP_DISTANCE || e1.getY() > displayMetrics.heightPixels - FLIP_DISTANCE) {
Log.d(TAG, "onScroll: 无效动作" + e1.getX());
//这里主要是处理屏幕边缘滑动的问题,防止和边缘的滑动返回等操作冲突,所以屏蔽了边缘的部分滑动。
return true;
}
if (VideoViewRect == null) {
VideoViewRect = videoControlListener.getVideoViewRect();
Log.d(TAG, "高度: " + displayMetrics.heightPixels);
Log.d(TAG, "宽度: " + displayMetrics.widthPixels);
if (VideoViewRect != null) {
Log.d(TAG, "left: " + VideoViewRect.left);
Log.d(TAG, "right: " + VideoViewRect.right);
Log.d(TAG, "top: " + VideoViewRect.top);
Log.d(TAG, "bottom: " + VideoViewRect.bottom);
indexDialogLp.x = ((VideoViewRect.right - VideoViewRect.left) / 2) - (displayMetrics.widthPixels / 2);
indexDialogLp.y = ((VideoViewRect.bottom - VideoViewRect.top) / 2) - (displayMetrics.heightPixels / 2) + VideoViewRect.top;
}
}
//初始化,没有滑动方向
if (e1.getX() - e2.getX() > FLIP_DISTANCE || e2.getX() - e1.getX() > FLIP_DISTANCE) {
Log.i(TAG, "向左右滑...");
SCROLL_FLAG = SCROLL_FLAG_LEFTRIGHT;
SCROLL_VIDEO_PLAY_INDEX = 0f;
videoIndex = 0;
//得到视频长度和播放位置
SCROLL_VIDEO_LENTH = videoControlListener.getScrollVideoLenth();
SCROLL_VIDEO_PLAYING_INDEX_CATCH = SCROLL_VIDEO_PLAYING_INDEX = videoControlListener.getScrollVideoPlayingIndex();
indexll.setVisibility(View.VISIBLE);
voicell.setVisibility(View.GONE);
showIndexDialog();
return true;
} else if (e1.getY() - e2.getY() > FLIP_DISTANCE || e2.getY() - e1.getY() > FLIP_DISTANCE) {
if (e1.getX() < (displayMetrics.widthPixels / 2)) {
//左边上下滑动滑动
SCROLL_FLAG = SCROLL_FLAG_TOPBOTTOM_LEFT;
Log.i(TAG, "左屏幕向上下滑...");
SCROLL_VIDEO_LIGHT_INDEX = 0;
voiceLightImg.setImageResource(R.drawable.video_lightimg);
try {
//当前屏幕亮度
SCREEN_LIGHT_CATCH = SCREEN_LIGHT = Settings.System.getInt(mContext.getContentResolver(), Settings.System.SCREEN_BRIGHTNESS);
voiceLightsb.setMax(SCROLL_LIGHT_MAX);
voiceLightsb.setProgress(SCREEN_LIGHT);
} catch (Settings.SettingNotFoundException e) {
e.printStackTrace();
}
} else {
//右边上下滑动
SCROLL_FLAG = SCROLL_FLAG_TOPBOTTOM_RIGHT;
Log.i(TAG, "右屏幕向上下滑...");
SCROLL_VIDEO_VOICE_INDEX = 0;
//当前声音大小
SCREEN_VOICE = systemService.getStreamVolume(AudioManager.STREAM_MUSIC);
voiceLightImg.setImageResource(R.drawable.video_voiceimg);
voiceLightsb.setMax(SCROLL_VOICE_MAX);
voiceLightsb.setProgress(SCREEN_VOICE);
}
indexll.setVisibility(View.GONE);
voicell.setVisibility(View.VISIBLE);
showIndexDialog();
return true;
}
break;
case SCROLL_FLAG_TOPBOTTOM_LEFT:
//设置亮度
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (!Settings.System.canWrite(mContext)) {
Intent intent = new Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS);
intent.setData(Uri.parse("package:" + mContext.getPackageName()));
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
mContext.startActivity(intent);
} else {
//左屏幕上下滑动
SCROLL_VIDEO_LIGHT_INDEX += distanceY;
Log.e(TAG, "toponScroll:" + SCROLL_VIDEO_LIGHT_INDEX);
double lightIndex = (((double) SCROLL_VIDEO_LIGHT_INDEX / (double) SCROLL_VIDEO_SCROLL_RANGE)) * SCROLL_LIGHT_MAX;
// lightIndex = -lightIndex;//取反
double newLightIndex = lightIndex + SCREEN_LIGHT_CATCH;
if (newLightIndex > SCROLL_LIGHT_MAX) {
//说明拖动到末尾
SCREEN_LIGHT = SCROLL_LIGHT_MAX;
} else if (newLightIndex < 0) {
//说明拖动到开头
SCREEN_LIGHT = 0;
} else {
SCREEN_LIGHT = (int) newLightIndex;
}
// 申请权限后做的操作
// 设置系统亮度
Settings.System.putInt(mContext.getContentResolver(), Settings.System.SCREEN_BRIGHTNESS, SCREEN_LIGHT);
Log.d(TAG, "屏幕亮度: " + SCREEN_LIGHT);
voiceLightsb.setProgress(SCREEN_LIGHT);
}
}
return true;
// break;
case SCROLL_FLAG_TOPBOTTOM_RIGHT:
//右屏幕上下滑动 设置声音
SCROLL_VIDEO_VOICE_INDEX += distanceY;
// SCROLL_VIDEO_VOICE_INDEX = (int) distanceY;
double voiceIndex = (((double) SCROLL_VIDEO_VOICE_INDEX / (double) SCROLL_VIDEO_SCROLL_RANGE)) * SCROLL_VOICE_MAX;
// lightIndex = -lightIndex;//取反
if (voiceIndex > 1) {
voiceIndex = 1;
SCROLL_VIDEO_VOICE_INDEX = 0;
} else if (voiceIndex < -1) {
voiceIndex = -1;
SCROLL_VIDEO_VOICE_INDEX = 0;
} else {
voiceIndex = 0;
}
Log.d(TAG, "voiceIndex: " + voiceIndex);
double newVoiceIndex = voiceIndex + SCREEN_VOICE;
if (newVoiceIndex > SCROLL_VOICE_MAX) {
//说明拖动到末尾
SCREEN_VOICE = SCROLL_VOICE_MAX;
} else if (newVoiceIndex < 0) {
//说明拖动到开头
SCREEN_VOICE = 0;
} else {
SCREEN_VOICE = (int) newVoiceIndex;
}
Log.d(TAG, "结束声音大小: " + SCREEN_VOICE);
systemService.setStreamVolume(AudioManager.STREAM_MUSIC, SCREEN_VOICE, AudioManager.FLAG_PLAY_SOUND);
voiceLightsb.setProgress(SCREEN_VOICE);
return true;
// break;
case SCROLL_FLAG_LEFTRIGHT:
//左右滑动
if (Math.abs(distanceY) > 1) {
break;
}
SCROLL_VIDEO_PLAY_INDEX += distanceX;
//得到当前视频进度
videoIndex = (((double) SCROLL_VIDEO_PLAY_INDEX / (double) SCROLL_VIDEO_SCROLL_RANGE)) * (SCROLL_VIDEO_LENTH * SCROLL_VIDEO_PLAY_RANGE);
//说明是进度滑动
videoIndex = -videoIndex;//取反
newVideoIndex = videoIndex + SCROLL_VIDEO_PLAYING_INDEX_CATCH;
// Log.d("print", "videoIndex: " + videoIndex + "-----newVideoIndex:" + newVideoIndex);
if (newVideoIndex > SCROLL_VIDEO_LENTH) {
//说明拖动到末尾
SCROLL_VIDEO_PLAYING_INDEX = SCROLL_VIDEO_LENTH;
} else if (newVideoIndex < 0) {
//说明拖动到开头
SCROLL_VIDEO_PLAYING_INDEX = 0;
} else {
SCROLL_VIDEO_PLAYING_INDEX = newVideoIndex;
}
if ((videoIndex / 1000) > 0f) {
//快进
indeximg.setImageResource(R.drawable.indeximg_left);
} else {
//快退
indeximg.setImageResource(R.drawable.indeximg_right);
}
// Log.d("print", "滑动结束,拖动进度为" + videoIndex / 1000 + "秒");
// Log.d("print", "滑动结束,当前进度为" + SCROLL_VIDEO_PLAYING_INDEX / 1000 + "秒");
indextv.setText(parse2TimeStr(SCROLL_VIDEO_PLAYING_INDEX) + "/" + parse2TimeStr(SCROLL_VIDEO_LENTH));
return true;
// break;
}
return false;
}
@Override
public void onLongPress(MotionEvent e) {
Log.e(TAG, "onLongPress");
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
Log.e(TAG, "onFling");
return false;
}
}
private void showIndexDialog() {
indexDialog.getWindow().setFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
indexDialog.show();
}
private void dismissIndexDialog() {
if (indexDialog != null) {
indexDialog.dismiss();
}
}
public interface OnVideoControlListener {
//得到视频总长度
double getScrollVideoLenth();
//得到视频当前播放位置
double getScrollVideoPlayingIndex();
//设置视频当前播放位置
void setScrollVideoPlayingIndex(double playIndex);
//双击屏幕
void onDoubleTap();
//单击屏幕
void onSingleTap();
//得到视频控件的大小范围
Rect getVideoViewRect();
}
public void setOnVideoControlListener(OnVideoControlListener videoControlListener) {
this.videoControlListener = videoControlListener;
}
private String parse2TimeStr(double timeStr) {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("HH:mm:ss");
simpleDateFormat.setTimeZone(TimeZone.getTimeZone("GMT+00:00"));
if (timeStr / 1000 > 3600) {
//说明超过一小时
simpleDateFormat.applyPattern("HH:mm:ss");
} else {
//说明没超过一小时
simpleDateFormat.applyPattern("mm:ss");
}
return simpleDateFormat.format(timeStr);
}
}
使用起来其实也很简单,我们这里自定义了一个控件MyVideoView
假设这个MyVideoView就是我们的视频控件
那么如何让这个控件使用我们的工具类
从而实现手势控制播放进度,声音,亮度的功能呢?
看下面的代码
public class MyVideoView extends View {
private VideoGestureControlUtil videoGestureControlUtil;
private double playIndex = 0;//视频当前播放位置
public MyVideoView(Context context) {
this(context, null);
}
public MyVideoView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public MyVideoView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
private void init(Context context) {
videoGestureControlUtil = new VideoGestureControlUtil(context, this);
videoGestureControlUtil.setOnVideoControlListener(new VideoGestureControlUtil.OnVideoControlListener() {
@Override
public double getScrollVideoLenth() {
return 36 * 60 * 1000;//36分钟的视频
}
@Override
public double getScrollVideoPlayingIndex() {
return playIndex;
}
@Override
public void setScrollVideoPlayingIndex(double playIndex) {
MyVideoView.this.playIndex = playIndex;
}
@Override
public void onDoubleTap() {
Log.d("print", "onSingleTap: 双击");
}
@Override
public void onSingleTap() {
Log.d("print", "onSingleTap: 单击");
}
@Override
public Rect getVideoViewRect() {
Rect rect = new Rect();
boolean localVisibleRect = getGlobalVisibleRect(rect);
if (localVisibleRect) {
return rect;
}
return null;
}
});
}
@Override
public boolean onTouchEvent(MotionEvent event) {
return videoGestureControlUtil.touch(event);
}
}
可以看到实现起来其实很简单
核心在onTouchEvent里面
把控件的触摸监听反馈给这个工具类即可
同时需要实现五个接口
这五个接口
需要返回视频的时长,当前播放进度
同时拖动完成后,会返回当前应该播放的进度
单击,双击的监听也都已经实现
这边需要重点介绍getVideoViewRect这个方法
这个方法是用来返回这个控件在屏幕上的位置的
具体可以参考该篇博客
(转载)Android:6种方式让你高效 & 正确地获取View的坐标位置
为什么需要控件把位置传递过去呢?
那是因为我们的工具类里面
已经实现了手指拖动时播放进度、亮度,声音变化时的弹窗
需要根据用户的位置,来调整这个弹窗的位置
当然,也可以去掉这个功能,自己注释掉工具类的dialog代码即可
关于dialog弹窗位置如何设置,可以参考这篇博客或者百度搜索下即可
(转载)Android 关于dialog的显示位置设置
这样,这个视频播放器就实现了通过手势控制播放进度,亮度,声音,以及单击,双击的功能
说完了基本使用
再来说说原理
首先是手势的监听
可以参考这篇博客
(转载)Android GestureDetector详解
接下来分析我们工具类里的具体实现
parse2TimeStr方法主要是对时间进行格式化
方便弹窗的时候,播放进度的文字显示一致
touch方法主要是用来监听手指抬起(MotionEvent.ACTION_UP)
或者手指按住但此时息屏(MotionEvent.ACTION_CANCEL)的情况
这个时候需要关闭弹窗,同时根据拖动的距离,去回调方法
告诉视频播放器当前拖动到的播放进度
核心监听MyGestureListener类
就是对双击,单击,拖动进行处理的地方了
注意,这边将拖动进行了不同状态的划分
分别是
SCROLL_FLAG_RESET 初始状态,也就是手指没按下去的状态
SCROLL_FLAG_TOPBOTTOM_LEFT 左边屏幕上下滑动,控制亮度
SCROLL_FLAG_TOPBOTTOM_RIGHT 右边屏幕上下滑动,控制声音
SCROLL_FLAG_LEFTRIGHT 左右滑动,控制播放进度
手指拖动时会判断状态,抬起后状态复原
另外注意这段代码
if (e1.getX() < FLIP_DISTANCE || e1.getX() > displayMetrics.widthPixels - FLIP_DISTANCE || e1.getY() > displayMetrics.heightPixels - FLIP_DISTANCE) {
Log.d(TAG, "onScroll: 无效动作" + e1.getX());
//这里主要是处理屏幕边缘滑动的问题,防止和边缘的滑动返回等操作冲突,所以屏蔽了边缘的部分滑动。
return true;
}
主要来防止在屏幕边缘(比如底部或者两侧)进行拖动,不需要时可以注释掉
dialog的位置设置主要是以下代码,可以根据实际需要进行修改
indexDialogLp.x = ((VideoViewRect.right - VideoViewRect.left) / 2) - (displayMetrics.widthPixels / 2);
indexDialogLp.y = ((VideoViewRect.bottom - VideoViewRect.top) / 2) - (displayMetrics.heightPixels / 2) + VideoViewRect.top;
最后,因为弹窗需要用到一些图片什么的,这里也一并上传
在网盘里下载即可
链接:
https://pan.baidu.com/s/1_E8IeJhXz2jTwdigLGgkaA
提取码:
kkzu