本篇文章已受權微信公衆號 guolin_blog (郭霖)獨家發佈android
上一篇文章簡單講解了騰訊新聞的視頻無縫切換效果的實現(視頻在播放中進行頁面切換),若是你沒有看過上篇,能夠先去看看Android 高仿騰訊新聞視頻切換效果。 上一篇寫得比較隨意,只是講解了兩個頁面間如何實現視頻在播放中的切換(切換播放器的container)及滾動中止播放等,部分效果沒有實現,有一些細節不是處理得很好,因此從新補上一篇更加詳細的教程。相同的內容此次就不在贅述了。 一樣,仍是先上效果圖 git
此次播放器換成了JZVideoPlayer,若是項目中尚未接入播放器或者剛接入的,仍是建議換成PlayerBase,高度解耦,可擴展性高,提供無縫續播助手。github
JZVideoPlayer版本是以前的,而且改動有點大。這裏主要介紹思路,跟注意點,用PlayerBase一樣也能夠實現的。 JZVideoPlayer實現無縫切換其實就是更改player的ViewParentbash
public void attachToContainer(ViewGroup container) {
detachSuperContainer();
if (container != null) {
container.addView(JZVideoPlayerManager.getCurrentJzvd(), new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
playerContainer = container;
}
}
public void detachSuperContainer() {
JZVideoPlayer player = JZVideoPlayerManager.getCurrentJzvd();
ViewParent parent = player.getParent();
if (parent != null && parent instanceof ViewGroup) {
((ViewGroup) parent).removeView(player);
}
}
複製代碼
4G跟wifi切換出現提示:註冊一個廣播進行監聽微信
@Override
protected void onResume() {
super.onResume();
JZVideoPlayer.goOnPlayOnResume();
IntentFilter filter = new IntentFilter();
filter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION);
filter.addAction(WifiManager.WIFI_STATE_CHANGED_ACTION);
registerReceiver(wifiReceiver, filter);
}
@Override
protected void onStop() {
super.onStop();
try {
//weChat moment share will execute twice so try catch
unregisterReceiver(wifiReceiver);
} catch (Exception e) {
e.printStackTrace();
}
}
private BroadcastReceiver wifiReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (intent != null && WifiManager.NETWORK_STATE_CHANGED_ACTION.equals(intent.getAction())) {
NetworkInfo info = intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO);
if (info != null) {
if (info.getState().equals(NetworkInfo.State.DISCONNECTED)) {
if (JZMediaManager.isWiFi) {
JZMediaManager.isWiFi = false;
JZVideoPlayer.WIFI_TIP_DIALOG_SHOWED = false;
if(播放中或者加載中){
JZMediaManager.instance().jzMediaInterface.pause();
JZVideoPlayerManager.getCurrentJzvd().onStatePause();
}
}
} else if (info.getState().equals(NetworkInfo.State.CONNECTED)) {
if (!JZMediaManager.isWiFi) {
JZMediaManager.isWiFi = true;
JZVideoPlayer.WIFI_TIP_DIALOG_SHOWED = true;
if (JZVideoPlayerManager.getCurrentJzvd() != null &&
JZVideoPlayerManager.getCurrentJzvd().currentState == JZVideoPlayer.CURRENT_STATE_PAUSE) {
JZVideoPlayer.goOnPlayOnResume();
}
}
}
}
}
}
};
複製代碼
這裏放在onResume裏面註冊是由於個人項目不止一個頁面有視頻,因此須要在這裏監聽。這裏注意一下,微信分享的時候onStop會調用2次,因此要try catch。4G切wifi的時候,要注意若是是用戶手動暫停,是不須要自動播放的。app
public static void onScrollPlayVideo(RecyclerView recyclerView, int firstVisiblePosition, int lastVisiblePosition) {
if (JZMediaManager.isWiFi) {
for (int i = 0; i <= lastVisiblePosition - firstVisiblePosition; i++) {
View child = recyclerView.getChildAt(i);
View view = child.findViewById(R.id.player);
if (view != null && view instanceof JZVideoPlayerStandard) {
JZVideoPlayerStandard player = (JZVideoPlayerStandard) view;
if (getViewVisiblePercent(player) == 1f) {
if (JZMediaManager.instance().positionInList != i + firstVisiblePosition) {
player.startButton.performClick();
}
break;
}
}
}
}
}
複製代碼
這裏使用的是播放中item的position去判斷是不是第一個徹底可見的視頻,若是你的item的position會變(別問我爲何,真的會有這種狀況,手動狗頭),就要用ide
JZVideoPlayerManager.getCurrentJzvd() != player
複製代碼
去判斷。 計算view的可見百分比,範圍是0-1佈局
public static float getViewVisiblePercent(View view) {
if (view == null) {
return 0f;
}
float height = view.getHeight();
Rect rect = new Rect();
if (!view.getLocalVisibleRect(rect)) {
return 0f;
}
float visibleHeight = rect.bottom - rect.top;
Log.d(TAG, "getViewVisiblePercent: emm " + visibleHeight);
return visibleHeight / height;
}
複製代碼
public static void onScrollReleaseAllVideos(int firstVisiblePosition, int lastVisiblePosition,float percent) {
int currentPlayPosition = JZMediaManager.instance().positionInList;
if (currentPlayPosition >= 0) {
if ((currentPlayPosition <= firstVisiblePosition || currentPlayPosition >= lastVisiblePosition - 1)) {
if (getViewVisiblePercent(JZVideoPlayerManager.getCurrentJzvd()) < percent) {
JZVideoPlayer.releaseAllVideos();
}
}
}
}
複製代碼
//初版
holder.itemView.setTranslationY(attr.getY() - l[1]);
holder.container.setScaleX(attr.getWidth() / (float) holder.container.getMeasuredWidth());
holder.container.setScaleY(attr.getHeight() / (float) holder.container.getMeasuredHeight());
//修改版
holder.itemView.setTranslationY(attr.getY() - l[1] - (holder.container.getMeasuredHeight() - attr.getHeight()) / 2);
holder.container.setScaleX(attr.getWidth() / (float) holder.container.getMeasuredWidth());
holder.container.setScaleY(attr.getHeight() / (float) holder.container.getMeasuredHeight());
複製代碼
若是容器大小相同(視頻列表頁進入評論頁),那直接用座標相減就行,但這裏對播放器的大小進行了改變,就須要減去高度差的一半,這裏還要除以2是由於縮放的中心是view的中點。post
這裏因爲用的JZVideoPlayer,須要固定播放容器的寬高,否則會觸發view的onMeasure致使閃爍動畫
進入這個頁面的時候須要分直接進入和視頻播放進入兩種狀況。直接進入,就是直接添加fragment,再播放第一個視頻,視頻播放進入就是無縫切換效果。退出頁面同理
PS:無縫切換的時候要留意一下,在新聞頁,點擊是直接進入視頻列表,而在視頻列表這裏,點擊是出現控制器的。在新聞頁有個倒計時動畫,而在視頻列表頁是沒有的。這些在頁面切換的時候,都須要進行對應的顯示隱藏和點擊事件的設置等等。
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
if (dy != 0) {
JZUtils.onScrollReleaseAllVideos(mLayoutManager.findFirstVisibleItemPosition(), mLayoutManager.findLastVisibleItemPosition(), 0.2f);
}
}
複製代碼
這裏的onScrolled()
方法有個小小的坑(我的感受)。
* Callback method to be invoked when the RecyclerView has been scrolled. This will be
* called after the scroll has completed.
* <p>
* This callback will also be called if visible item range changes after a layout
* calculation. In that case, dx and dy will be 0.
*
* @param recyclerView The RecyclerView which scrolled.
* @param dx The amount of horizontal scroll.
* @param dy The amount of vertical scroll.
*/
public void onScrolled(RecyclerView recyclerView, int dx, int dy)
複製代碼
當recyclerView滑動後,這個方法就會被回調。很正常對吧。但是下面還有兩行呢。當可見item從新測量,佈局後,也會觸發這個方法,此時dx,dy都是0。這裏要注意的就是咱們這裏是有全屏功能的,並且還會切換橫豎屏,那就會觸發這個方法。致使功能不正常了。因此上面加了個不爲0的判斷。
播放完畢自動播放下一個視頻 這裏須要留意一下,播放下一個視頻我是經過滑動下一個視頻到頂部從而觸發播放的。但是也會有這種狀況,就是你的視頻特別少(咱們的app就是),那就沒法播放最後一個視頻了。騰訊的數據量夠大,通常不會有這個問題。因此當沒有更多的時候,須要在recyclerView的底部插入一條數據,顯示沒有更多數據,就能夠播放這個視頻了,騰訊也是這麼處理的,看得出來設計得很周全,一個頁面只能徹底顯示一個視頻,考慮得十分全面啊。
遮罩
遮罩這裏用的是自定義view,畫一個半通明的背景。
當列表播放時,顯示遮罩,而且須要一個過渡的效果(透明度動畫)。
當滑動界面,顯示評論頁,切換全屏,退出視頻列表時,隱藏遮罩,不須要過渡效果。
onVideoSizeChanged()
方法進行監聽。if (JZMediaManager.instance().currentVideoWidth > JZMediaManager.instance().currentVideoHeight) {
JZVideoPlayer.FULLSCREEN_ORIENTATION = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
if(JZVideoPlayerManager.getCurrentJzvd().currentScreen == SCREEN_WINDOW_FULLSCREEN){
JZUtils.setRequestedOrientation(JZVideoPlayerManager.getCurrentJzvd().getContext(), ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
}
} else {
JZVideoPlayer.FULLSCREEN_ORIENTATION = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
if(JZVideoPlayerManager.getCurrentJzvd().currentScreen == SCREEN_WINDOW_FULLSCREEN){
JZUtils.setRequestedOrientation(JZVideoPlayerManager.getCurrentJzvd().getContext(), ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
}
}
複製代碼
根據寬高設置進入全屏是豎屏仍是橫屏(若是大家公司非主流,不能根據視頻寬高判斷,那就後臺加個字段設置吧)。
android P這裏有個bug,切換屏幕方向的時候會黑屏,暫時未發現解決辦法,知道怎麼解決的大佬歡迎下方留言啊!!!另外,部分國產手機seekbar點擊以後不是直接跳到對應的進度,而是快進一點點,也是服了呀...
public void changeUrl(String url, Object... objects) {
this.currentUrlMapIndex = 0;
this.seekToInAdvance = 0;
LinkedHashMap map = new LinkedHashMap();
map.put(URL_KEY_DEFAULT, url);
Object[] dataSourceObjects = new Object[1];
dataSourceObjects[0] = map;
this.dataSourceObjects = dataSourceObjects;
this.objects = objects;
setState(CURRENT_STATE_PREPARING_CHANGING_URL);
resetProgressAndTime();
}
複製代碼
主要就是重置一些狀態,改變變量的值。 判斷方向 一樣也會觸發onVideoSizeChanged()
方法,在裏面進行判斷就行了,其實就是上面那段代碼啦。
PS:切換url的時候最好把畫面渲染層隱藏起來,播放的時候再顯示。否則的話部分機器可能會出現最後一幀的畫面被拉伸的狀況。
列表滑動
這裏須要注意一下,咱們上面對滑動進行了監聽,不能調用smoothScrollTo()或者smoothScrollBy()方法。這裏能夠直接調用scrollToPositionWithOffset(),直接滑動到對應位置(若是你不是LinearLayoutManager,那就本身想辦法吧。)
若是你跟我同樣,都是用的JZVideoPlayer,那下面就要留意一下啦
if (JZVideoPlayerManager.getCurrentJzvd().currentScreen == SCREEN_WINDOW_FULLSCREEN) {
JZMediaManager.instance().positionInList++;
JZVideoPlayerManager.getCurrentJzvd().changeUrl(mList.get(JZMediaManager.instance().positionInList).getVideoUrl());
mLayoutManager.scrollToPositionWithOffset(JZMediaManager.instance().positionInList, 0);
mRecycler.postDelayed(new Runnable() {
@Override
public void run() {
JZVideoPlayerManager.setFirstFloor((JZVideoPlayer) mRecycler.getChildAt(0).findViewById(R.id.player));
}
}, 500);
}
複製代碼
進入和退出視頻列表頁進行無縫播放時,對播放器的父view進行了更改,也就會須要進行addView或者removeView,而且修改相關接口等等操做。
if (還在播放第一個視頻) {
videoListFragment.removeVideoList();
recycler.postDelayed(new Runnable() {
@Override
public void run() {
JZMediaManager.instance().positionInList = clickPosition;
int first = mLayoutManager.findFirstVisibleItemPosition();
View v = recycler.getChildAt(clickPosition - first);
if (v != null) {
final PlayerContainer container = v.findViewById(R.id.adapter_video_container);
if (不是無縫播放進入視頻列表頁) {
container.removeAllViews();
}
//播放器接口,狀態設置
}
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
transaction.remove(videoListFragment);
transaction.commitAllowingStateLoss();
}
}, 800);
} else {
JZVideoPlayer.releaseAllVideos();
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
transaction.remove(videoListFragment);
transaction.commitAllowingStateLoss();
if (是無縫播放進入視頻列表頁) {
int first = mLayoutManager.findFirstVisibleItemPosition();
View v = recycler.getChildAt(clickPosition - first);
if (v != null) {
final PlayerContainer container = v.findViewById(R.id.adapter_video_container);
container.removeAllViews();
//從新添加播放器
}
}
}
複製代碼
仍是解釋一下吧,這裏分4種狀況:
邏輯確實複雜,須要多看幾遍
若是還沒接入播放器,仍是用PlayerBase吧。
評論頁跟上一次沒有大的區別,作了一點小小的改動:視頻播放完畢後,會重置爲普通狀態,退出評論頁返回視頻列表頁,會自動播放下一條。代碼就不貼了,詳見demo。 大功告成,喝杯82年雪碧慶祝一下吧。
下面是關於動態加載ijkplayer so文件的,不須要的能夠跳過 動態加載so目前只見到這2種方案:
File dir = getDir("libs", Context.MODE_PRIVATE);
File soFile = new File(dir, "ijkffmpeg.so");
複製代碼
是soFile的路徑。 而後就是加載so庫,剛開始我覺得直接把IjkMediaPlayer.Java拷出來修改加載路徑就大功告成,但是卻仍是報錯,找不到方法。查了下,發現JNI的方法名是須要包名+類名+方法名,而我這裏直接拷過來,包名變了,也就找不到方法了。因此 須要把ijk整個庫拷下來,引入到項目裏再進行修改(也能夠修改so庫中的包名)。 PS:若是你擔憂仍是找不到so,能夠這樣作
try {
jzMediaInterface.prepare();
} catch (Throwable e) {
e.printStackTrace();
Object dataSource = JZMediaManager.getCurrentDataSource();
Log.e(TAG, "handleMessage: " + e.getMessage());
Toast.makeText(MyApplication.getInstance(), "so error", Toast.LENGTH_SHORT).show();
JZVideoPlayer.setMediaInterface(new JZExoPlayer());
jzMediaInterface.currentDataSource = dataSource;
jzMediaInterface.prepare();
}
複製代碼
捕獲初始化錯誤,再切換回備用內核。
拖了很久終於把這個東西寫完了,高難度的東西沒多少,全都是細節的處理。雖然效果還能夠,但仍是逃不了上次說的問題,不能在activity間切換,邏輯複雜,耦合度過高。
最後,附上源碼,有問題或者有更好的實現方式,歡迎下方留言,有空看到會回覆的。