Hello,愛貓的老司機(jī)來埋坑啦<( ̄︶ ̄)>毅贮,鑒于之前的《Android 實(shí)現(xiàn)視屏播放器办悟、邊播邊緩存功能、外加鏟屎(IJKPlayer)》好像還挺多人關(guān)注的滩褥,文中一些地方因?yàn)槠ň褪菓校﹩栴}一筆帶過病蛉,這篇就拓?fù)淞囊涣钠渲辛斜砣粒€有播放中的視頻滑出屏幕用小窗口播放的實(shí)現(xiàn)瑰煎,剛好最近有做了一些調(diào)整铺然。
上例牌 github>>>>>>>> https://github.com/CarGuo 對,就是這個郭老司機(jī)酒甸。
本期就不話嘮了探熔,周一誰有精力說話呢┑( ̄Д  ̄)┍,程序猿的周末是什么烘挫?
列表中播放視頻全屏展示
看過小喵上一篇視頻相關(guān)文章的應(yīng)該知道小喵手賤的用了兩種實(shí)現(xiàn)方式诀艰,一種是基于懶人的系統(tǒng)層模式柬甥;一種是基于單例的UI邏輯播放器的模式的ListVideoUtil。至于為什么是兩種呢其垄?因?yàn)槭仲v啊苛蒲。(ノ?益?)ノ彡┻━┻,本文如有不明之處可結(jié)合前文一起食用:《Android 實(shí)現(xiàn)視屏播放器绿满、邊播邊緩存功能臂外、外加鏟屎(IJKPlayer)》。
1喇颁、系統(tǒng)層實(shí)現(xiàn)全屏播放
偉人曾經(jīng)說過漏健,每一個Activity都有一個自己的默認(rèn)布局,這里面又包含有了一個com.android.internal.R.id.content橘霎,而且是一個FrameLayout(請無視上面的廢話)蔫浆,如此看來用來作為我們?nèi)溜@示的父布局妥妥的。此處手賤的加入了動畫效果的支持姐叁,一直覺得5.0的過渡動畫挺高大上的瓦盛,作為一個material design的應(yīng)用必須有這樣的逼格(什么?你說兼容外潜?這里美女太多我聽不到····)原环。
作為一只內(nèi)向的程序猿,語言組織能力有限处窥,我們還是從代碼上來嘱吗,從代碼上去吧,注釋滿滿的滔驾,順序看下去不難理解(前提是你看的下柜与,確實(shí)長♂了點(diǎn))。
1.1 進(jìn)入全屏
- 獲取到了com.android.internal.R.id.content這個ViewGroup嵌灰。
- 清除當(dāng)前列表播放器L上的TextureView渲染控件,等待全屏播放器F的渲染控件颅悉。
- 新創(chuàng)建一個視頻邏輯播放器F沽瞭,為它設(shè)置一個固定id,這樣干掉它的時候通過這個id也能快速找到剩瓶。
- 保存當(dāng)前的狀態(tài)欄驹溃、標(biāo)題欄信息和列表中在屏幕位置的信息,用于恢復(fù)到原本的狀態(tài)延曙。
- 創(chuàng)建一個黑色背景的FrameLayout豌鹤,充滿屏幕用來承載全屏播放器F,這樣全屏播放器F可以在其中執(zhí)行動畫效果枝缔。
- 5.0以下直接加全屏播放器F到ViewGroup居中充滿全屏布疙,5.0以上則執(zhí)行動畫蚊惯。
- 5.0以上先通過margin讓全屏播放器加入到ViewGroup同列表的位置一致,之后通過過渡動畫平移到屏幕中間灵临,居中充滿全屏截型。
怎么樣,看起來是不是有些混亂儒溉?(ノ?益?)ノ彡┻━┻宦焦,我就說程序猿還是看代碼好溝通是吧,雖然很長就是顿涣。
//獲得com.android.internal.R.id.content
private ViewGroup getViewGroup() {
return (ViewGroup) (CommonUtil.scanForActivity(getContext())).findViewById(Window.ID_ANDROID_CONTENT);
}
···此處省略無數(shù)只草泥馬
//這兩個是TextureView的回調(diào)波闹,在這remove和onPause還有add的時候基本會進(jìn)入
@Override
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
//更新數(shù)據(jù)到這個surface上渲染
mSurface = new Surface(surface);
GSYVideoManager.instance().setDisplay(mSurface);
}
@Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
//告訴數(shù)據(jù)管理器這個渲染控件放棄了
GSYVideoManager.instance().setDisplay(null);
surface.release();
return true;
}
···此處省略無數(shù)只草泥馬
這個開始全屏的頁面邏輯
//將播放的視頻渲染控件移除,進(jìn)入上面的回調(diào)涛碑,讓新的邏輯播放器可以接入
if (mTextureViewContainer.getChildCount() > 0) {
mTextureViewContainer.removeAllViews();
}
//保存全屏之前的狀態(tài)欄和
saveLocationStatus(context, statusBar, actionBar);
try {
//生成一個播放器精堕,因?yàn)槔^承關(guān)系,會創(chuàng)建一個當(dāng)前列表item一樣的UI邏輯播放器
//這些邏輯都是寫在GSYBaseVideoPlayer這個抽象類下
Constructor<GSYBaseVideoPlayer> constructor = (Constructor<GSYBaseVideoPlayer>) GSYBaseVideoPlayer.this.getClass().getConstructor(Context.class);
final GSYBaseVideoPlayer gsyVideoPlayer = constructor.newInstance(getContext());
//給它一個固定的id锌唾,在這樣移除的時候就知道在哪里
gsyVideoPlayer.setId(FULLSCREEN_ID);
//獲取屏幕的高度
WindowManager wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
final int w = wm.getDefaultDisplay().getWidth();
final int h = wm.getDefaultDisplay().getHeight();
//創(chuàng)建一個層用于加入都window層中锄码,設(shè)置為黑色,用于包含著播放器
FrameLayout.LayoutParams lpParent = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
FrameLayout frameLayout = new FrameLayout(context);
frameLayout.setBackgroundColor(Color.BLACK);
//如果5.0的機(jī)器就執(zhí)行動畫晌涕,這里其實(shí)可以用VauleAnimaton兼容5.0以下的
if (mShowFullAnimation && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
//先把播放器的位置設(shè)置為在列表中一樣位置
FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(getWidth(), getHeight());
lp.setMargins(mListItemRect[0], mListItemRect[1], 0, 0);
frameLayout.addView(gsyVideoPlayer, lp);
vp.addView(frameLayout, lpParent);
//稍微延時執(zhí)行動畫
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
//開啟5.0動畫
TransitionManager.beginDelayedTransition(vp);
//將播放器跳轉(zhuǎn)為充滿居中滋捶,系統(tǒng)自動過渡
resolveFullVideoShow(context, gsyVideoPlayer, h, w);
}
}, 300);
} else {
//非5.0的直接將播放器的布局加入到布局下
FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(getWidth(), getHeight());
frameLayout.addView(gsyVideoPlayer, lp);
vp.addView(frameLayout, lpParent);
//將播放器跳轉(zhuǎn)為充滿居中
resolveFullVideoShow(context, gsyVideoPlayer, h, w);
}
//設(shè)置全屏邏輯播放器和當(dāng)前列表的邏輯狀態(tài)一致
gsyVideoPlayer.setUp(mUrl, mCache, mObjects);
gsyVideoPlayer.setStateAndUi(mCurrentState);
//添加上渲染控件,通知數(shù)據(jù)加載管理器是用這個渲染
gsyVideoPlayer.addTextureView();
//配置對應(yīng)UI
gsyVideoPlayer.getFullscreenButton().setImageResource(R.drawable.video_shrink);
gsyVideoPlayer.getFullscreenButton().setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
clearFullscreenLayout();
}
});
gsyVideoPlayer.getBackButton().setVisibility(VISIBLE);
gsyVideoPlayer.getBackButton().setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
clearFullscreenLayout();
}
});
//將數(shù)據(jù)加載管理器的接口回到配置到全屏播放器里面
GSYVideoManager.instance().setLastListener(this);
GSYVideoManager.instance().setListener(gsyVideoPlayer);
} catch (Exception e) {
e.printStackTrace();
}
···此處省略無數(shù)只草泥馬
/**
* 全屏
*/
private void resolveFullVideoShow(Context context, GSYBaseVideoPlayer gsyVideoPlayer) {
//清除動畫的margin
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);
//加入旋轉(zhuǎn)工具類
mOrientationUtils = new OrientationUtils((Activity) context, gsyVideoPlayer);
mOrientationUtils.setEnable(mRotateViewAuto);
}
1.2 退出全屏
既然都進(jìn)去了♂余黎,出來還難嗎重窟?所以我們只需要反著來就行了,下面直接長代碼惧财,有注釋巡扇。(男人長一點(diǎn)有什么錯┑( ̄Д  ̄)┍)
- 是否橫屏,是的話先轉(zhuǎn)為豎屏
- 恢復(fù)狀態(tài)欄和標(biāo)題欄
- 5.0以下直接清除當(dāng)前列全屏播放器F垮衷,恢復(fù)視頻狀態(tài)
- 5.0以上顯示讓全屏播放器F過渡到原本的位置厅翔,再清除恢復(fù)視頻狀態(tài)
/**
* 退出系統(tǒng)層播放全屏效果
*/
public void clearFullscreenLayout() {
//需要判斷當(dāng)前是否橫屏,是的話要轉(zhuǎn)為界面之后稍等一會在退回搀突,這樣才不會界面抖動
int delay = mOrientationUtils.backToProtVideo();
//關(guān)閉旋轉(zhuǎn)
mOrientationUtils.setEnable(false);
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
backToNormal();
}
}, delay);
}
/**
* 回到正常效果
*/
private void backToNormal() {
//恢復(fù)狀態(tài)
showSupportActionBar(mContext, mActionBar, mStatusBar);
final ViewGroup vp = getViewGroup();
//拿到content和播放器
final View oldF = vp.findViewById(FULLSCREEN_ID);
final GSYVideoPlayer gsyVideoPlayer;
if (oldF != null) {
gsyVideoPlayer = (GSYVideoPlayer) oldF;
if (mShowFullAnimation && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
TransitionManager.beginDelayedTransition(vp);
//執(zhí)行動畫回到原本的列表中的位置
FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) gsyVideoPlayer.getLayoutParams();
lp.setMargins(mListItemRect[0], mListItemRect[1], 0, 0);
lp.width = mListItemSize[0];
lp.height = mListItemSize[1];
//注意配置回來刀闷,不然動畫效果會不對
lp.gravity = Gravity.NO_GRAVITY;
gsyVideoPlayer.setLayoutParams(lp);
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
resolveNormalVideoShow(oldF, vp, gsyVideoPlayer);
}
}, 400);
} else {
//直接移除
resolveNormalVideoShow(oldF, vp, gsyVideoPlayer);
}
} else {
//直接移除
resolveNormalVideoShow(null, vp, null);
}
}
/**
* 恢復(fù)
*/
private void resolveNormalVideoShow(View oldF, ViewGroup vp, GSYVideoPlayer gsyVideoPlayer) {
//移除全屏播放器
if (oldF.getParent() != null) {
ViewGroup viewGroup = (ViewGroup) oldF.getParent();
vp.removeView(viewGroup);
}
//拿回狀態(tài)
mCurrentState = GSYVideoManager.instance().getLastState();
if (gsyVideoPlayer != null) {
mCurrentState = gsyVideoPlayer.getCurrentState();
}
//重新設(shè)置回調(diào)
GSYVideoManager.instance().setListener(GSYVideoManager.instance().lastListener());
GSYVideoManager.instance().setLastListener(null);
//播放器恢復(fù)
setStateAndUi(mCurrentState);
//通知數(shù)據(jù)加載播放器用回列表的渲染
addTextureView();
CLICK_QUIT_FULLSCREEN_TIME = System.currentTimeMillis();
}
2、ListVideoUtil實(shí)現(xiàn)全屏播放
總體上邏輯和上文是一致的仰迁,只是這種實(shí)現(xiàn)在列表中是不包含邏輯播放器甸昏,邏輯播放器和全屏邏輯播放器都是一個單例,需要你手動在list列表的最外層加多一個布局做全屏播放徐许,在每個item那里預(yù)留一個位置用于包容列表的播放器施蜜,還有一個播放按鈕用于播放。
感覺很麻煩是吧雌隅,耦合度又高翻默,但是它可以在視頻滑出界面的時候不被釋放缸沃,一直保持在原來的位置。
2.1 全屏
和上面的邏輯基本一致冰蘑,就不廢話了(可以偷懶了)和泌,只需要注意用的時候操作方式不一樣,總結(jié)起來就是有些麻煩祠肥。
//配置好全屏布局
listVideoUtil.setFullViewContainer(videoFullContainer);
listVideoUtil.setHideStatusBar(true);
···此處省略無數(shù)只草泥馬
//增加封面
ImageView imageView = new ImageView(context);
imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
imageView.setImageResource(R.mipmap.xxx1);
//將列表的位置武氓,封面,列表的TAG仇箱,列表是的父布局县恕,播放按鍵傳入進(jìn)去
listVideoUtil.addVideoPlayer(position, imageView, TAG, holder.videoContainer, holder.playerBtn);
holder.playerBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//更新其他item
notifyDataSetChanged();
//設(shè)置播放器的標(biāo)志位,防止錯位
listVideoUtil.setPlayPositionAndTag(position, TAG);
//url開始播放
final String url = "http://baobab.wdjcdn.com/14564977406580.mp4";
listVideoUtil.startPlay(url);
}
});
列表中播放視頻小窗口播放
有時候我們會想要視頻滑出屏幕的時候有個小窗口在右下角剂桥,最好還是可以關(guān)閉和拖動的(看視頻的時候可以快速最小化收起來忠烛,不停止,避免尷尬對吧)权逗。邏輯和實(shí)現(xiàn)全屏一樣美尸,用系統(tǒng)的content層來承載,不同的是利用margin讓視頻出現(xiàn)在右下角斟薇,這樣我們拖動的時候只要改變視頻的margin师坎,就可以讓視頻小窗體在它的父布局內(nèi)移動啦。
/**
* 顯示小窗口
*/
public void showSmallVideo(Point size, final boolean actionBar, final boolean statusBar) {
//利用content實(shí)現(xiàn)堪滨,和全屏一樣胯陋,只是大小和背景色不一樣
final ViewGroup vp = getViewGroup();
removeVideo(vp, SMALL_ID);
if (mTextureViewContainer.getChildCount() > 0) {
mTextureViewContainer.removeAllViews();
}
try {
Constructor<GSYBaseVideoPlayer> constructor = (Constructor<GSYBaseVideoPlayer>) GSYBaseVideoPlayer.this.getClass().getConstructor(Context.class);
GSYBaseVideoPlayer gsyVideoPlayer = constructor.newInstance(getContext());
gsyVideoPlayer.setId(SMALL_ID);
FrameLayout.LayoutParams lpParent = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
FrameLayout frameLayout = new FrameLayout(mContext);
FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(size.x, size.y);
int marginLeft = CommonUtil.getScreenWidth(mContext) - size.x;
int marginTop = CommonUtil.getScreenHeight(mContext) - size.y;
if (actionBar) {
marginTop = marginTop - getActionBarHeight((Activity) mContext);
}
if (statusBar) {
marginTop = marginTop - getStatusBarHeight(mContext);
}
//利用margin讓視頻出現(xiàn)在右下角,這樣我們拖動的時候只要改變margin就好啦
lp.setMargins(marginLeft, marginTop, 0, 0);
frameLayout.addView(gsyVideoPlayer, lp);
vp.addView(frameLayout, lpParent);
//繼續(xù)播放
gsyVideoPlayer.setUp(mUrl, mCache, mObjects);
gsyVideoPlayer.setStateAndUi(mCurrentState);
gsyVideoPlayer.addTextureView();
gsyVideoPlayer.onClickUiToggle();
gsyVideoPlayer.setSmallVideoTextureView(new SmallVideoTouch(gsyVideoPlayer, marginLeft, marginTop));
GSYVideoManager.instance().setLastListener(this);
GSYVideoManager.instance().setListener(gsyVideoPlayer);
} catch (InstantiationException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 隱藏小窗口
*/
public void hideSmallVideo() {
final ViewGroup vp = getViewGroup();
GSYVideoPlayer gsyVideoPlayer = (GSYVideoPlayer) vp.findViewById(SMALL_ID);
removeVideo(vp, SMALL_ID);
mCurrentState = GSYVideoManager.instance().getLastState();
if (gsyVideoPlayer != null) {
mCurrentState = gsyVideoPlayer.getCurrentState();
}
GSYVideoManager.instance().setListener(GSYVideoManager.instance().lastListener());
GSYVideoManager.instance().setLastListener(null);
setStateAndUi(mCurrentState);
addTextureView();
CLICK_QUIT_FULLSCREEN_TIME = System.currentTimeMillis();
}
這是觸摸邏輯袱箱,這拖動的視頻窗體的時候遏乔,通過改變margin來實(shí)現(xiàn)窗體的移動,注意不要跑飛了就要发笔,加個閾值盟萨。多說無益,看代碼(又省下了好多字):
public class SmallVideoTouch implements View.OnTouchListener {
private int mDownX, mDownY;
private int mMarginLeft, mMarginTop;
private int _xDelta, _yDelta;
private GSYBaseVideoPlayer mGsyBaseVideoPlayer;
public SmallVideoTouch(GSYBaseVideoPlayer gsyBaseVideoPlayer, int marginLeft, int marginTop) {
super();
mMarginLeft = marginLeft;
mMarginTop = marginTop;
mGsyBaseVideoPlayer = gsyBaseVideoPlayer;
}
@Override
public boolean onTouch(View view, MotionEvent event) {
final int X = (int) event.getRawX();
final int Y = (int) event.getRawY();
switch (event.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
mDownX = X;
mDownY = Y;
FrameLayout.LayoutParams lParams = (FrameLayout.LayoutParams) mGsyBaseVideoPlayer
.getLayoutParams();
_xDelta = X - lParams.leftMargin;
_yDelta = Y - lParams.topMargin;
break;
case MotionEvent.ACTION_UP:
if (Math.abs(mDownY - Y) < 5 && Math.abs(mDownX - X) < 5) {
return false;
} else {
return true;
}
case MotionEvent.ACTION_MOVE:
FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) mGsyBaseVideoPlayer
.getLayoutParams();
layoutParams.leftMargin = X - _xDelta;
layoutParams.topMargin = Y - _yDelta;
//不能超過屏幕上下左右的位置
if (layoutParams.leftMargin >= mMarginLeft) {
layoutParams.leftMargin = mMarginLeft;
}
if (layoutParams.topMargin >= mMarginTop) {
layoutParams.topMargin = mMarginTop;
}
if (layoutParams.leftMargin <= 0) {
layoutParams.leftMargin = 0;
}
if (layoutParams.topMargin <= 0) {
layoutParams.topMargin = 0;
}
mGsyBaseVideoPlayer.setLayoutParams(layoutParams);
}
return false;
}
}
最后
如果你看到這里了讨,恭喜你看完了<( ̄︶ ̄)>捻激!那么,下面還有沙發(fā)量蕊,請問您要坐一坐嗎?d=====( ̄▽ ̄*)b不坐也沒關(guān)系艇挨,還有g(shù)ithub可以去呢:https://github.com/CarGuo 残炮。