Demo地址:https://github.com/CarGuo/GSYVideoPlayer,读者可以下载该demo后,感受它的效果,再来读这篇文章:
demo使用的是别人的demo,主要是进行一些原理分析,主要分析一下几点:
1.GSYVideoPlayer的初始化
2.视频播放实现
3.全屏实现
4.滑动时,小窗口出现和消失实现逻辑
5.小窗口视频创建
6.小窗口视频恢复为列表视频
7.边缓存边播放实现
8.弹幕效果实现
一.初始化分析
1.初始化主要是构造了一个StandardGSYVideoPlayer
2.设置全屏布局的容器
代码如下:
listVideoUtil = new ListVideoUtil(this);
listVideoUtil.setFullViewContainer(videoFullContainer);
public ListVideoUtil(Context context) {
gsyVideoPlayer = new StandardGSYVideoPlayer(context);
this.context = context;
}
StandardGSYVideoPlayer的初始化化主要是加载R.layout.video_layout_standard这个布局,并且实例化一些组件,为组件设置监听事件;
初始化组件包括:播放/暂停按钮,播放时长,总时长,播放进度条,返回按钮,全屏按钮和视频标题,
初始化组件不包括:用于显示视频的TextureView,这个TextureView是动态添加的,放在下片文章中讲述;
初始化内容比较简单;
思考以下几个问题:
1.视频窗口的大小和位置是如何匹配ListView的item大小和位置的?
2.视频播放画面是如何显示出来的?
3.视频播放的声音如何显示出来的?
通过代码分析;
@Override
public View getView(final int position, View convertView, ViewGroup parent) {
....
....
listVideoUtil.addVideoPlayer(position, holder.imageView, TAG, holder.videoContainer, holder.playerBtn);
holder.playerBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
notifyDataSetChanged();
//listVideoUtil.setLoop(true);
listVideoUtil.setPlayPositionAndTag(position, TAG);
// final String url = "https://tv.miguvideo.com/?from=singlemessage&isappinstalled=0#video/live/761358370/room201706301600304971_R1";
//listVideoUtil.setCachePath(new File(FileUtils.getPath()));
final String url = "http://baobab.wdjcdn.com/14564977406580.mp4";
listVideoUtil.startPlay(url);
}
});
........
.......
}
listview的adapter的getview方法每次都会走一次listVideoUtil.addVideoPlayer(position, holder.imageView, TAG, holder.videoContainer, holder.playerBtn);
/**
* 动态添加视频播放
*
* @param position 位置
* @param imgView 封面
* @param tag TAG类型
* @param container player的容器
* @param playBtn 播放按键
*/
public void addVideoPlayer(final int position, View imgView, String tag,
ViewGroup container, View playBtn) {
container.removeAllViews();
if (isCurrentViewPlaying(position, tag)) {
if (!isFull) {
ViewGroup viewGroup = (ViewGroup) gsyVideoPlayer.getParent();
if (viewGroup != null)
viewGroup.removeAllViews();
container.addView(gsyVideoPlayer);
playBtn.setVisibility(View.INVISIBLE);
}
} else {
playBtn.setVisibility(View.VISIBLE);
container.removeAllViews(); //增加封面
container.addView(imgView);
}
}
代码可以看出addVideoPlayer方法的意思是,如果当前的position等于listVideoUtil中获取到的position,videoContainer中就显示gsyVideoPlayer,否则显示视频封面;
点击播放按钮的作用:
1.向listVideoUtil中设置position,并通过notifyDataSetChanged方法使重新走getview(),重新走getview方法时,会重新进入addVideoPlayer方法判断;
至此第一个疑问已经解决:视频显示在哪个item就是这个position决定的;视频窗口的大小由videoContainer的大小决定;
2.执行代码listVideoUtil.startPlay(url);
下面开始分析startPlay(url)中做了哪些操作?
/**
* 开始播放
*
* @param url 播放的URL
*/
public void startPlay(String url) {
if (isSmall()) {
smallVideoToNormal();//如果是小窗口,就转为正常窗口播放,这个问题后面再说
}
this.url = url;
gsyVideoPlayer.release();
gsyVideoPlayer.setLooping(isLoop);//视频是否循环播放
gsyVideoPlayer.setSpeed(speed);//视频播放速度
gsyVideoPlayer.setNeedShowWifiTip(needShowWifiTip);//非wifi环境下,显示流量提醒
gsyVideoPlayer.setNeedLockFull(needLockFull);//是否需要全屏锁屏
gsyVideoPlayer.setUp(url, true, cachePath, mapHeadData, objects);//设置边缓存边播放
//增加title
gsyVideoPlayer.getTitleTextView().setVisibility(View.GONE);
//设置返回键
gsyVideoPlayer.getBackButton().setVisibility(View.GONE);
//设置全屏按键功能
gsyVideoPlayer.getFullscreenButton().setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
resolveFullBtn();
}
});
gsyVideoPlayer.startPlayLogic();
}
就是一些简单设置项,其中gsyVideoPlayer.setUp(url, true, cachePath, mapHeadData, objects);会把url改为全局的mUrl;
@Override
public void startPlayLogic() {
if (mStandardVideoAllCallBack != null) {
Debuger.printfLog("onClickStartThumb");
mStandardVideoAllCallBack.onClickStartThumb(mUrl, mObjects);
}
prepareVideo();
startDismissControlViewTimer();
}
/**
* 开始状态视频播放
*/
protected void prepareVideo() {
if (GSYVideoManager.instance().listener() != null) {
GSYVideoManager.instance().listener().onCompletion();
}
GSYVideoManager.instance().setListener(this);
GSYVideoManager.instance().setPlayTag(mPlayTag);
GSYVideoManager.instance().setPlayPosition(mPlayPosition);
addTextureView();//动态添加TextureView
mAudioManager.requestAudioFocus(onAudioFocusChangeListener, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);//暂时获取音频焦点
((Activity) getContext()).getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);//保持屏幕常亮不灭屏
GSYVideoManager.instance().prepare(mUrl, mMapHeadData, mLooping, mSpeed);
setStateAndUi(CURRENT_STATE_PREPAREING);
}
科普1,音频焦点分类:
AudioManager.AUDIOFOCUS_REQUEST_GRANTED------------永久获取音频焦点
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT-------------暂时获取音频焦点,比如音乐后台播放,当前视频播放会抢夺音频焦点,视频播放完成,音乐自动播放;
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK--------提示类型获取音频焦点,比如听音乐的时候来短信,音乐声音降低
/**
* 添加播放的view
*/
protected void addTextureView() {
if (mTextureViewContainer.getChildCount() > 0) {
mTextureViewContainer.removeAllViews();
}
mTextureView = null;
mTextureView = new GSYTextureView(getContext());
mTextureView.setSurfaceTextureListener(this);
mTextureView.setRotation(mRotate);
RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
layoutParams.addRule(RelativeLayout.CENTER_IN_PARENT);
mTextureViewContainer.addView(mTextureView, layoutParams);
}
构造了一个TextureView,并添加到gsyVideoPlayer布局中。
TextureView设置了SurfaceTextureListener;
科普2:SurfaceTextureListener回调方法:
1.onSurfaceTextureAvailable(SurfaceTexture arg0, int arg1, int arg2)-------------TextureView可用时调用
2.onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height)
3.onSurfaceTextureDestroyed(SurfaceTexture surface)------------TextureView销毁时调用
4.onSurfaceTextureUpdated(SurfaceTexture surface)
回到代码:
@Override
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
mSurface = new Surface(surface);
GSYVideoManager.instance().setDisplay(mSurface);
//显示暂停切换显示的图片
showPauseCover();
}
意思是:在TextureView可以使用时,将surface交给GSYVideoManager,用于显示视频的画面,至此第二个问题已经解决,当然此时还没有开始播放视频, 真正播放视频是在
GSYVideoManager.instance().prepare(mUrl, mMapHeadData, mLooping, mSpeed)中,此处的mUrl就是上面保存的url;
public void prepare(final String url, final Map, String> mapHeadData, boolean loop, float speed) { if (TextUtils.isEmpty(url)) return; Message msg = new Message(); msg.what = HANDLER_PREPARE; GSYModel fb = new GSYModel(url, mapHeadData, loop, speed); msg.obj = fb; mMediaHandler.sendMessage(msg); }
public class MediaHandler extends Handler { public MediaHandler(Looper looper) { super(looper); } @Override public void handleMessage(Message msg) { super.handleMessage(msg); switch (msg.what) { case HANDLER_PREPARE: initVideo(msg); break;
private void initVideo(Message msg) { try { currentVideoWidth = 0; currentVideoHeight = 0; mediaPlayer.release(); if (videoType == GSYVideoType.IJKPLAYER) { initIJKPlayer(msg); } else if (videoType == GSYVideoType.IJKEXOPLAYER) { initEXOPlayer(msg); } setNeedMute(needMute); mediaPlayer.setOnCompletionListener(GSYVideoManager.this); mediaPlayer.setOnBufferingUpdateListener(GSYVideoManager.this); mediaPlayer.setScreenOnWhilePlaying(true); mediaPlayer.setOnPreparedListener(GSYVideoManager.this); mediaPlayer.setOnSeekCompleteListener(GSYVideoManager.this); mediaPlayer.setOnErrorListener(GSYVideoManager.this); mediaPlayer.setOnInfoListener(GSYVideoManager.this); mediaPlayer.setOnVideoSizeChangedListener(GSYVideoManager.this); mediaPlayer.prepareAsync(); } catch (Exception e) { e.printStackTrace(); } }根据videoType的类型,创建不同的播放器,initIJKPlayer(msg)或者initEXOPlayer(msg);
不管哪种播放器,msg中有视频播放的url,用于播放器播放视频使用;
最后调用mediaPlayer.prepareAsync()播放视频;
首先思考以下几个问题:
1.点击全屏按钮,视频是怎么填充整个屏幕的?(以及视频放大时的动画效果实现)
2.视频填充屏幕后,又是如何实现横屏的?
3.视频是如何做到,列表中的视频和放大后的视频,无缝衔接的?
依旧通过代码分析:
//设置全屏按键功能 gsyVideoPlayer.getFullscreenButton().setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { resolveFullBtn(); } });全屏按钮由一个点击事件触发
public void resolveFullBtn() { if (fullViewContainer == null) { return; } if (!isFull) { resolveToFull(); } else { resolveMaterialToNormal(gsyVideoPlayer); } }fullViewContain是在ListVideoUtil初始化的时候设置的,具体分析可以查看 http://blog.csdn.net/qq_15631341/article/details/74332254
代码的意思是:
如果当前不是全屏,走resolveToFull;
如果当前是全屏,走resolveMaterialToNormal;
我们先分析展示全屏的过程,
/** * 处理全屏逻辑 */ private void resolveToFull() { systemUiVisibility = ((Activity) context).getWindow().getDecorView().getSystemUiVisibility(); CommonUtil.hideSupportActionBar(context, hideActionBar, hideStatusBar);//根据需求隐藏actionbar和statusbar if (hideKey) { hideNavKey(context); } isFull = true; //将gsyVideoplayer从原来的容器中剥离,即gsyVideoPlayer不在显示在ListView的摸一个item上了 ViewGroup viewGroup = (ViewGroup) gsyVideoPlayer.getParent();
//此处需要注意,gsyvideo还没有从listview的item上移除时,保存了当前的布局信息,用于后面动画的展示
listParams
=
gsyVideoPlayer
.getLayoutParams()
;
if
(viewGroup !=
null
) {
listParent
= viewGroup
;
viewGroup.removeView(
gsyVideoPlayer
)
;
}
否则执行resolveFullAdd()方法;即这两种方法的区别是一个是有动画效果的,一个是无动画效果的;
先来看一下没有动画效果的是如何实现的?
/** * 添加到全屏父布局里 */ private void resolveFullAdd() { fullViewContainer.setBackgroundColor(Color.BLACK); fullViewContainer.addView(gsyVideoPlayer); resolveChangeFirstLogic(50); }就是把一开始设置的fullViewContain的背景改为黑色,再把gsyVideoPlayer的布局添加进来,最后执行resolveChangeFirstLogic(50)方法;
再来看一下有动画效果的是 如何实现的?
/** * 如果是5.0的动画开始位置 */ private void resolveMaterialAnimation() { listItemRect = new int[2]; listItemSize = new int[2]; saveLocationStatus(context, hideStatusBar, hideActionBar);//保存gsyvideoplayer在listview的item中的位置和大小 FrameLayout.LayoutParams lpParent = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); FrameLayout frameLayout = new FrameLayout(context); frameLayout.setBackgroundColor(Color.BLACK); FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(listItemSize[0], listItemSize[1]); lp.setMargins(listItemRect[0], listItemRect[1], 0, 0); frameLayout.addView(gsyVideoPlayer, lp); fullViewContainer.addView(frameLayout, lpParent); handler.postDelayed(new Runnable() { @Override public void run() { //开始动画 TransitionManager.beginDelayedTransition(fullViewContainer); resolveMaterialFullVideoShow(gsyVideoPlayer); resolveChangeFirstLogic(600); } }, 300); }
/** * 保存大小和状态 */ private void saveLocationStatus(Context context, boolean statusBar, boolean actionBar) { listParent.getLocationOnScreen(listItemRect);//获取listView中的gsyvideoplayer相对于屏幕的左边距和上边距并存入listItemRect中 int statusBarH = getStatusBarHeight(context); int actionBerH = getActionBarHeight((Activity) context); if (statusBar) { listItemRect[1] = listItemRect[1] - statusBarH; } if (actionBar) { listItemRect[1] = listItemRect[1] - actionBerH; } listItemSize[0] = listParent.getWidth(); listItemSize[1] = listParent.getHeight(); }listParent是在resolveToFull中保存的gsyvideoplayer在listView的item中的布局参数;
上面的全屏动画在handler的post方法之前,先构造了左边这个布局,listItemRect标注反了,需要互换一下,不好意思,不会画图,将就一下,并把它加入到fullViewContain中;只不过因为demo中的左右边距都为0,所以listItemRect[0]就是0,listItemRect[1]就是上边距再加上statusbar和actionbar的高度
/** * 如果是5.0的,要从原位置过度到全屏位置 */ private void resolveMaterialFullVideoShow(GSYBaseVideoPlayer gsyVideoPlayer) { FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) gsyVideoPlayer.getLayoutParams(); lp.setMargins(0, 0, 0, 0); lp.height = ViewGroup.LayoutParams.MATCH_PARENT; lp.width = ViewGroup.LayoutParams.MATCH_PARENT; lp.gravity = Gravity.CENTER; gsyVideoPlayer.setLayoutParams(lp); gsyVideoPlayer.setIfCurrentIsFullscreen(true); }在resolveMaterialFullVideoShow方法执行后,变为右边那张图;
至于中间的动画效果:只要
TransitionManager.beginDelayedTransition(fullViewContainer);
科普1:
要改变某个控件的位置,可以用修改布局参数后setLayoutParams,如果想给这个过程加上动画效果,则可以在父节点上调用TransitionManager.beginDelayedTransition
无论是resolveToFull还是resolveMaterialToNormal至此已经完成了全屏变化;两个方法最后都调用了resolveChangeFirstLogic
/** * 是否全屏一开始马上自动横屏 */ private void resolveChangeFirstLogic(int time) { if (isFullLandFrist()) { handler.postDelayed(new Runnable() { @Override public void run() { if (orientationUtils.getIsLand() != 1) { orientationUtils.resolveByClick(); } } }, time); } gsyVideoPlayer.setIfCurrentIsFullscreen(true); if (videoAllCallBack != null) { Debuger.printfLog("onEnterFullscreen"); videoAllCallBack.onEnterFullscreen(this.url); } }该方法是用来实现全屏后的屏幕旋转;
/** * 点击切换的逻辑,比如竖屏的时候点击了就是切换到横屏不会受屏幕的影响 */ public void resolveByClick() { mClick = true; if (mIsLand == 0) { screenType = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE; activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); gsyVideoPlayer.getFullscreenButton().setImageResource(gsyVideoPlayer.getShrinkImageRes()); mIsLand = 1; mClickLand = false; } else { screenType = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT; activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); if (gsyVideoPlayer.isIfCurrentIsFullscreen()) { gsyVideoPlayer.getFullscreenButton().setImageResource(gsyVideoPlayer.getShrinkImageRes()); } else { gsyVideoPlayer.getFullscreenButton().setImageResource(gsyVideoPlayer.getEnlargeImageRes()); } mIsLand = 0; mClickPort = false; } }这一块代码病史很难,就是横屏设置Activity为竖屏,竖屏就设置Activity为横屏;
最后解释一下,从列表视频到全屏视频的过程中,无缝衔接了,原因就是,只是GsyVideoPlayer这个布局换了一个父控件,GsyVideoManager中的medieplayer并没有停止播放,所以视频无缝衔接了。
最后留一个问题,大家自己思考吧,如何从全屏再回到列表视频????
从resolveMaterialToNormal(gsyVideoPlayer)分析开始。