1. VideoView简介
- Android实现视频播放主要是使用VideoView类来实现的。
- VideoView背后是使用MediaPlayer来对视频文件进行控制的。
- 只支持mp4、avi、3gp格式的视频,支持格式单一。
2. VideoView常用方法:
- setVideoPath:设置要播放的视频文件的位置
- start:开始或继续播放视频
- pause:暂停播放视频
- resume:将视频从头开始播放
- seekTo:从指定的位置开始播放视频
- isPlaying:判断当前是否正在播放视频
- getDuration:获取载入的视频文件的时长
3. VideoView播放视频的小栗子:
- 添加网络和SD卡权限:
- 添加VideoView布局:
- 添加ButterKnife:
compile 'com.jakewharton:butterknife:8.8.1'
annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1'
- 初始化权限:
private void requestSDpermission() {
if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1);
} else {
initVideoPath();
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
switch (requestCode) {
case 1:
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
initVideoPath();
} else {
Toast.makeText(this, "拒绝权限将无法使用程序", Toast.LENGTH_SHORT).show();
finish();
}
break;
default:
}
}
- 初始化VideoPath:
private void initVideoPath() {
File file = new File(Environment.getExternalStorageDirectory(), "vivo.mp4");
vvVideoView.setVideoPath(file.getPath());
}
- 完整代码:
/**
* VideoView
* fu kai qiang 2017/17/31
*/
public class MainActivity extends AppCompatActivity {
@BindView(R.id.vv_VideoView)
VideoView vvVideoView;
Unbinder mUnbinder;
@BindView(R.id.play)
Button play;
@BindView(R.id.pause)
Button pause;
@BindView(R.id.replay)
Button replay;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mUnbinder = ButterKnife.bind(this);
requestSDpermission();
initVideoPath();
}
private void requestSDpermission() {
if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1);
} else {
initVideoPath();
}
}
private void initVideoPath() {
File file = new File(Environment.getExternalStorageDirectory(), "vivo.mp4");
vvVideoView.setVideoPath(file.getPath());
}
@OnClick({R.id.play, R.id.pause, R.id.replay})
public void onViewClicked(View view) {
switch (view.getId()) {
case R.id.play:
if (!vvVideoView.isPlaying()) {
vvVideoView.start();
}
break;
case R.id.pause:
if (vvVideoView.isPlaying()) {
vvVideoView.pause();
}
break;
case R.id.replay:
if (vvVideoView.isPlaying()) {
vvVideoView.resume();
}
break;
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
switch (requestCode) {
case 1:
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
initVideoPath();
} else {
Toast.makeText(this, "拒绝权限将无法使用程序", Toast.LENGTH_SHORT).show();
finish();
}
break;
default:
}
}
@Override
protected void onDestroy() {
super.onDestroy();
mUnbinder.unbind();
if (vvVideoView != null) {
vvVideoView.suspend();
}
}
}
4. MediaController简介
- 从上文可知VideoView自身可以实现视频播放的逻辑,但是我们需要去写布局来操作视频的播放暂停等,那能不能不写布局,就能实现呢?当然可以:VideoView可以借助MediaController实现视频播放的逻辑。MediaController是一个多媒体的类,它提供了丰富的Api,支持快进、快退、上一个、下一个等多媒体操作。
5. MediaController小栗子
- 添加网络和SD卡权限:
- 添加VideoView布局:
- 初始化本地或网络播放路径
private void initVideoPath() {
String path_local = Environment.getExternalStorageDirectory().getAbsolutePath() + "/vivo.mp4";
//本地播放
mVvVideoView.setVideoPath(path_local);
//网络播放
// mVvVideoView.setVideoURI(Uri.parse(...);
}
提示:网络测试的话Tomcat下webapps下面放vivo.mp4
- videoView和MediaController进行绑定
private void initBind() {
MediaController mediaController = new MediaController(this);
mVvVideoView.setMediaController(mediaController);
mediaController.setMediaPlayer(mVvVideoView);
}
注意:必须互相设置进行绑定
- 暂时设置为横屏:
- Activity完整代码:
public class MainActivity extends AppCompatActivity {
@BindView(R.id.vv_videoView)
VideoView mVvVideoView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
initVideoPath();
initBind();
}
/**
* videoView和MediaController绑定
*/
private void initBind() {
MediaController mediaController = new MediaController(this);
mVvVideoView.setMediaController(mediaController);
mediaController.setMediaPlayer(mVvVideoView);
}
/**
* 初始化本地或网络播放路径
*/
private void initVideoPath() {
// mVvVideoView.setVideoPath(getLocalPath());
mVvVideoView.setVideoURI(Uri.parse("http://192.168.0.108:8080/video/vivo.mp4"));
}
/**
* 获取本地路径
*
* @return
*/
@NonNull
private String getLocalPath() {
return new File(Environment.getExternalStorageDirectory(), "vivo.mp4").getPath();
}
}
6. 自定义UI界面
VideoView需要添加播放暂停快进快退进度条等等按钮,而MdeiaController自带了播放暂停快进快退进度条等UI界面,但是这些都不是我们想要的,因为UI太过于简陋,所以为了美观,以及需求的多样化,我们需要定义我们自己的UI,通过VideoView自身的逻辑来实现视频播放。
修改布局:
-
-
-
-
7. 删除以下MediaController的逻辑:
/**
* videoView和MediaController绑定
*/
private void initBind() {
MediaController mediaController = new MediaController(this);
mVvVideoView.setMediaController(mediaController);
mediaController.setMediaPlayer(mVvVideoView);
}
8. 初始化控件:
//需要竖屏隐藏的音量title
@BindView(R.id.tv_vol_name)
TextView mTvVolName;
//徐奥竖屏隐藏的音量分割线
@BindView(R.id.v_line)
//最外层的布局
@BindView(R.id.rl_videolayout)
RelativeLayout mRlVideolayout;
//VideoView
@BindView(R.id.vv_videoView)
VideoView mVvVideoView;
//进程进度条
@BindView(R.id.sb_progress_seekbar)
SeekBar mSbProgressSeekbar;
//播放 暂停
@BindView(R.id.bt_start_pause)
Button mBtStartPause;
//现在的时间
@BindView(R.id.tv_time_current)
TextView mTvTimeCurrent;
//总共的时间
@BindView(R.id.tv_time_total)
TextView mTvTimeTotal;
//音量进度条
@BindView(R.id.sb_vol_seekbar)
SeekBar mSbVolSeekbar;
//全屏切换开关
@BindView(R.id.bt_switch)
Button mBtSwitch;
//控制区域
@BindView(R.id.ll_controllerBar_layout)
LinearLayout mLlControllerBarLayout;
//控制区域左半边
@BindView(R.id.ll_left_layout)
LinearLayout mLlLeftLayout;
//控制区域右半边
@BindView(R.id.ll_right_layout)
LinearLayout mLlRightLayout;
9. 播放和暂停逻辑
//控制视频的播放和暂停
case R.id.bt_start_pause:
if (mVvVideoView.isPlaying()) {
mBtStartPause.setText("Start");
mVvVideoView.pause();
} else {
mBtStartPause.setText("Pause");
mVvVideoView.start();
}
break;
10. 定义格式时间的方法
/**
* 时间的格式化
* @param textView
* @param millisecond
*/
public void updateTime(TextView textView, int millisecond) {
int second = millisecond / 1000; //总共换算的秒
int hh = second / 3600; //小时
int mm = second % 3600 / 60; //分钟
int ss = second % 60; //时分秒中的秒的得数
String str = null;
if (hh != 0) {
//如果是个位数的话,前面可以加0 时分秒
str = String.format("%02d:%02d:%02d", hh, mm, ss);
} else {
str = String.format("%02d:%02d", mm, ss);
}
textView.setText(str);
}
11. 自动刷新并设置当前视频时间和视频总时间及同步SeekBar进度
//刷新机制的标志
private static final int UPDATE_UI = 1;
/**
* 定义Handler刷新时间
* 得到并设置当前视频播放的时间
* 得到并设置视频播放的总时间
* 设置SeekBar总进度和当前视频播放的进度
* 并反复执行Handler刷新时间
* 指定标识用于关闭Handler
*/
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
if (msg.what == UPDATE_UI) {
int currentPosition = mVvVideoView.getCurrentPosition();
int totalduration = mVvVideoView.getDuration();
updateTime(mTvTimeCurrent, currentPosition);
updateTime(mTvTimeTotal, totalduration);
mSbProgressSeekbar.setMax(totalduration);
mSbProgressSeekbar.setProgress(currentPosition);
mHandler.sendEmptyMessageDelayed(UPDATE_UI, 500);
}
}
};
12. 初始化播放和刷新时间机制
private void initVideoPlay() {
mVvVideoView.start();
//第一个参数是标志,第二个参数是刷新间隔时间
mHandler.sendEmptyMessageDelayed(UPDATE_UI, 500);
}
13. 适时关闭和开启刷新机制
@OnClick(R.id.bt_start_pause)
public void onViewClicked(View view) {
switch (view.getId()) {
//控制视频的播放和暂停
case R.id.bt_start_pause:
if (mVvVideoView.isPlaying()) {
mBtStartPause.setText("Start");
mVvVideoView.pause();
//停止刷新UI
mHandler.removeMessages(UPDATE_UI);
} else {
mBtStartPause.setText("Pause");
mVvVideoView.start();
//开启刷新UI
mHandler.sendEmptyMessage(UPDATE_UI);
}
break;
}
}
@Override
protected void onPause() {
super.onPause();
//停止刷新UI
mHandler.removeMessages(UPDATE_UI);
}
14. 拖动SeekBar同步SeekBar和Time和VideoView
private void synchScrollSeekBarAndTime() {
mSbProgressSeekbar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
//进度改变的时候同步Time
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
updateTime(mTvTimeCurrent, progress);
}
//拖动的时候关闭刷新机制
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
mHandler.removeMessages(UPDATE_UI);
}
//拖动停止同步VideoView和开启刷新机制
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
int progress = seekBar.getProgress();
mVvVideoView.seekTo(progress);
mHandler.sendEmptyMessage(UPDATE_UI);
}
});
}
15. 自动横竖屏切换
- 删掉强制横屏的代码:
android:screenOrientation="landscape"
- 防止横竖屏切换重建Activity:配置文件添加
android:configChanges="orientation|screenSize|keyboard|keyboardHidden"
- 防止横屏的时候视频右边有大量的空白:
//定义两个变量:代表当前屏幕的宽和屏幕的高
private int screen_width, screen_height;
/**
* 获取屏幕的宽和屏幕的高
*/
private void initScreenWidthAndHeight() {
screen_width = getResources().getDisplayMetrics().widthPixels;
screen_height = getResources().getDisplayMetrics().heightPixels;
}
/**
* 设置VideoView和最外层相对布局的宽和高
* @param width : 像素的单位
* @param height : 像素的单位
*/
private void setVideoViewScale(int width, int height) {
//获取VideoView宽和高
ViewGroup.LayoutParams layoutParams = mVvVideoView.getLayoutParams();
//赋值给VideoView的宽和高
layoutParams.width = width;
layoutParams.height = height;
//设置VideoView的宽和高
mVvVideoView.setLayoutParams(layoutParams);
//同上
ViewGroup.LayoutParams layoutParams1 = mRlVideolayout.getLayoutParams();
layoutParams.width = width;
layoutParams.height = height;
mRlVideolayout.setLayoutParams(layoutParams1);
}
/**
* 监听屏幕方向的改变,横竖屏的时候分别做处理
*
* @param newConfig
*/
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
//当屏幕方向是横屏的时候,我们应该对VideoView以及包裹VideoView的布局(也就是对整体)进行拉伸
if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
setVideoViewScale(ViewGroup.LayoutParams.MATCH_PARENT,ViewGroup.LayoutParams.MATCH_PARENT);
}
//当屏幕方向是竖屏的时候,竖屏的时候的高我们需要把dp转为px
else {
setVideoViewScale(ViewGroup.LayoutParams.MATCH_PARENT,DensityUtils.dip2px(this,240));
}
}
/**
* Created by FuKaiqiang on 2018-01-06.
*/
public class DensityUtils {
/**
* 根据手机的分辨率从 dip 的单位 转成为 px(像素)
*/
public static int dip2px(Context context, float dpValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (dpValue * scale + 0.5f);
}
/**
* 根据手机的分辨率从 px(像素) 的单位 转成为 dp
*/
public static int px2dip(Context context, float pxValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (pxValue / scale + 0.5f);
}
/**
* 将px值转换为sp值,保证文字大小不变
*/
public static int px2sp(Context context, float pxValue) {
final float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
return (int) (pxValue / fontScale + 0.5f);
}
/**
* 将sp值转换为px值,保证文字大小不变
*/
public static int sp2px(Context context, float spValue) {
final float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
return (int) (spValue * fontScale + 0.5f);
}
}
16. 音量
//初始化音频管理器
private AudioManager mAudioManager;
/**
* 初始化音频管理器;获取设备最大音量和当前音量并设置
*/
private void initAudioManager() {
mAudioManager = (AudioManager) getSystemService(AUDIO_SERVICE);
int streamMaxVolume = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
int streamVolume = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
mSbVolSeekbar.setMax(streamMaxVolume);
mSbVolSeekbar.setProgress(streamVolume);
}
17. 手动横竖屏切换
// 定义一个横竖屏切换的变量
private boolean isFullScreen = false;
// 根据横竖屏的变化设置变量值
/**
* 监听屏幕方向的改变,横竖屏的时候分别做处理
*
* @param newConfig
*/
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
//当屏幕方向是横屏的时候,我们应该对VideoView以及包裹VideoView的布局(也就是对整体)进行拉伸
if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
setVideoViewScale(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
//横屏的时候显示
mTvVolName.setVisibility(View.VISIBLE);
mVLine.setVisibility(View.VISIBLE);
mSbVolSeekbar.setVisibility(View.VISIBLE);
//横屏的时候为true
isFullScreen = true;
}
//当屏幕方向是竖屏的时候,竖屏的时候的高我们需要把dp转为px
else {
setVideoViewScale(ViewGroup.LayoutParams.MATCH_PARENT, DensityUtils.dip2px(this, 240));
//竖屏的时候吟唱
mTvVolName.setVisibility(View.GONE);
mVLine.setVisibility(View.GONE);
mSbVolSeekbar.setVisibility(View.GONE);
//竖屏的时候为
isFullScreen = false;
}
}
//手动横竖屏切换
case R.id.bt_switch:
if (isFullScreen) {
//切换为竖屏
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
} else {
//切换为横屏
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
}
break;
18. 自定义VideoView
- 虽然可以实现了最外面的相对布局在横屏的时候全屏,但是VideoView并未全屏,原因是视频的宽度和高度并没有手机屏幕那么大,所以这个时候依然在VideoView宽留有空白区域,这个时候就应该自定义VideoView:
/**
* Created by FuKaiqiang on 2018-01-06.
*/
public class MyVideoView extends VideoView {
private int screen_width = 1920;
private int screen_height = 1080;
public MyVideoView(Context context) {
super(context);
}
public MyVideoView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyVideoView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//得到手机屏幕的宽和高
DisplayMetrics dm = new DisplayMetrics();
dm = getResources().getDisplayMetrics();
int screenWidth = dm.widthPixels; // 屏幕宽(像素,如:3200px)
int screenHeight = dm.heightPixels; // 屏幕高(像素,如:1280px)
//最大限度的展示宽和高
int width = getDefaultSize(screen_width, widthMeasureSpec);
int height = getDefaultSize(screen_height, heightMeasureSpec);
setMeasuredDimension(width, height);
}
}
onConfigurationChanged方法中:
//当横屏时主动取消半屏,该设置为全屏
getWindow().clearFlags((WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN));
getWindow().addFlags((WindowManager.LayoutParams.FLAG_FULLSCREEN));
//当竖屏时主动取消全屏,该设置为半屏
getWindow().clearFlags((WindowManager.LayoutParams.FLAG_FULLSCREEN));
getWindow().addFlags((WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN));
19. 手势调节音量和亮度
/**
* 初始化手势
*/
private void initGesture() {
mVvVideoView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
//现在的x,y坐标
float x = event.getX();
float y = event.getY();
switch (event.getAction()) {
//手指按下:
case MotionEvent.ACTION_DOWN:
lastX = x;
lastY = y;
break;
//手指移动:
case MotionEvent.ACTION_MOVE:
//偏移量
float moveX = x - lastX;
float moveY = y - lastY;
//计算绝对值
float absMoveX = Math.abs(moveX);
float absMoveY = Math.abs(moveY);
//手势合法性的验证
if (absMoveX > Num && absMoveY > Num) {
if (absMoveX < absMoveY) {
isEMove = true;
} else {
isEMove = false;
}
} else if (absMoveX < Num && absMoveY > Num) {
isEMove = true;
} else if (absMoveX > Num && absMoveY < Num) {
isEMove = false;
}
/**
* 区分手势合法的情况下,区分是去调节亮度还是去调节声音
*/
if (isEMove) {
//手势在左边
if (x < screen_width / 2) {
/**
* 调节亮度
*/
if (moveY > 0) {
//降低亮度
} else {
//升高亮度
}
changeBright(-moveY);
//手势在右边
} else {
Log.e("Emove", "onTouch: " + "手势在右边");
/**
* 调节音量
*/
if (moveY > 0) {
//减小音量
} else {
//增大音量
}
changeVolume(-moveY);
}
}
lastX = x;
lastY = y;
break;
//手指抬起:
case MotionEvent.ACTION_UP:
break;
}
return true;
}
});
}
/**
* 调节音量:偏移量和音量值的换算
*/
private void changeVolume(float moveY) {
int max = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
int current = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
int index = (int) (moveY / screen_height * max * 3);
int volume = Math.max(current + index, 0);
mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, volume, 0);
mSbVolSeekbar.setProgress(volume);
}
/**
* 调节亮度:
*/
private void changeBright(float moveY) {
WindowManager.LayoutParams layoutParams = getWindow().getAttributes();
mBrightness = layoutParams.screenBrightness;
float index = moveY / screen_height / 3;
mBrightness += index;
//做临界值的判断
if (mBrightness > 1.0f) {
mBrightness = 1.0f;
}
if (mBrightness < 0.01) {
mBrightness = 0.01f;
}
layoutParams.screenBrightness = mBrightness;
getWindow().setAttributes(layoutParams);
}
20. 亮度调节的显示
- 定义一个名为layout_progress的布局,位置放在屏幕中央
// 放置音量或者亮度的图片
// 音量或者亮度进度条的背景--进度条拿布局来写的,不是seekbar--都Ok的
// 音量或者亮度进度条的进度--进度条拿布局来写的,不是seekbar--都Ok的
@BindView(R.id.operation_percent)
View mOperationPercent;
@BindView(R.id.fl_content)
LinearLayout mFlContent;
- 调节音量方法changeVolume中添加:
if (mFlContent.getVisibility()==View.GONE) mFlContent.setVisibility(View.VISIBLE);
mOperationBg.setImageResource(R.mipmap.ic_vol);
ViewGroup.LayoutParams layoutParams = mOperationPercent.getLayoutParams();
layoutParams.width = (int) (DensityUtils.dip2px(this, 94) * (float) volume / max);
mOperationPercent.setLayoutParams(layoutParams);
- 调节亮度方法中添加:
if (mFlContent.getVisibility()==View.GONE) mFlContent.setVisibility(View.VISIBLE);
mOperationBg.setImageResource(R.mipmap.bright);
ViewGroup.LayoutParams layoutParams = mOperationPercent.getLayoutParams();
layoutParams.width = (int) (DensityUtils.dip2px(this, 94) *mBrightness);
mOperationPercent.setLayoutParams(layoutParams);
21. 不足:
- 音量的手势调节并非像亮度那般流畅,后续会优化这一部分。
- 横屏的时候,屏幕的高并没有填充整个屏幕。
22. Github下载地址,欢迎Star
- Star:https://github.com/OnlyYouMyLove/VideoView