解決android 滑動列表頁自動播視頻中的一些技術難點。助力更好的實現相似需求。不涉及到播放器的具體編解碼技術,由於各家用的播放器可能都不同(實際上是我不會~)android
建議在滑動中止的時候播視頻,還在滑動的時候建議不播。目前大廠的app基本上都是基於這個思路來作。 這麼作的主要緣由有兩點:算法
以RecyclerView 爲例:bash
因此咱們其實只要在這個回調中進行markdown
這個case 的條件 就表明列表頁滑動中止。 這就是咱們播視頻的時機了網絡
咱們以最流行的作法:符合人類視覺感知的,選擇 從上到下列表頁中第一個完整展現視頻區域的 item 進行播放 有人說咱們直接用findFirstCompletelyVisibleItemPosition方法不就好了?其實這樣是不完美的,由於咱們列表頁中的item 並不僅是一個單一的視頻播放區域,他還有標題,描述,閱讀數等等**。你的視頻播放區域其實只佔你整個item的一部分**app
因此咱們就要計算出來 視頻區域完整顯示 這個條件。 代碼以下:ide
case SCROLL_STATE_IDLE: //取第一個展現出來的item int firstPosition = ((LinearLayoutManager) recyclerView.getLayoutManager()).findFirstVisibleItemPosition(); //最後一個展現出來的item int lastPosition = ((LinearLayoutManager) recyclerView.getLayoutManager()).findLastVisibleItemPosition(); //開始遍歷 for (int i = firstPosition; i <= lastPosition; i++) { //取出這個列表頁中的item View itemView = recyclerView.getLayoutManager().findViewByPosition(i); //必定要先判空 由於有時上面這個函數真的會返回null //而後接着判斷 是否是有視頻播放組件 好比有的列表頁除了有視頻item還有純圖片的item //因此要斷定這個條件 if (itemView != null && itemView.findViewById(R.id.video_player) != null) { VideoPlayer videoPlayer = itemView.findViewById(R.id.video_player); if (isCanBePlayedByRect(videoPlayer)) { safePlayVideoMethod(videoPlayer); break; } } } 複製代碼
private boolean isCanBePlayedByRect(VideoPlayer videoPlayer) { Rect rect = new Rect(); //這裏就能夠取出來視頻播放區域的座標軸了 videoPlayer.getLocalVisibleRect(rect); int videoHeight = videoPlayer.getHeight(); //符合這個條件就意味着 整個視頻播放區域 都是完整的呈如今視頻中的 final boolean playFlag = (rect.top == 0 && rect.bottom == videoHeight) return playFlag; } 複製代碼
達成這個條件 咱們基本的算法就寫好了,可是這樣仍舊不完美。函數
考慮另一種複雜的狀況,好比說看下面這張圖:優化
咱們能夠看到列表頁中的最後2個item其實 是已經徹底露出屏幕的,他是符合咱們上一小節的斷定條件的。 可是視覺上咱們仍然是以爲 這2個視頻播放區域是不完整的,由於被底部的tab 遮蓋住了。spa
甚至有的用戶會把虛擬鍵給調出來。以下圖
這樣咱們的item展現的區域被遮蓋的就更多了。用前面一小節的算法已經不夠了。
此時咱們須要更進一步,針對前面的算法作必定的修改。有人會說,咱們直接用底部tab的高度減一下不就好了麼? 這樣其實也能夠,可是這樣作 不太優雅,緣由有2:
咱們這裏能夠用另一種比較好的方法規避上述的問題:
直接取底部tab的top座標(針對整個座標軸而言)。
咱們能夠在activity的第一幀繪製完畢之後 取一下這個座標:
if (mTabWidget != null && MAIN_PAGE_BOTTOM_TAB_RECT_TOP == 0) { Rect rect = new Rect(); //注意 咱們這裏 使用的是getGlobalVisibleRect 而不是getLocal了 //區別就是 global取得就是 針對全屏幕的座標軸 mTabWidget.getGlobalVisibleRect(rect); MAIN_PAGE_BOTTOM_TAB_RECT_TOP = rect.top; } 複製代碼
到這裏咱們就拿到了 底部tab 這個view的 top的座標值了。因此剩下的條件咱們只要增長一條
視頻播放組件的 全屏bottom的值 要大於 咱們這個tab的top的值。 只有這樣才能保證 咱們視頻的view 是沒有被遮蓋的
private boolean isCanBePlayedByRect(VideoPlayer videoPlayer) { Rect rect = new Rect(); videoPlayer.getLocalVisibleRect(rect); Rect gRect = new Rect(); //把視頻view的全局 bottom座標 也取出來 與tab的top座標作對比 videoPlayer.getGlobalVisibleRect(gRect); int videoHeight = videoPlayer.getHeight(); final boolean playFlag = (rect.top == 0 && rect.bottom == videoHeight && gRect.bottom < MAIN_PAGE_BOTTOM_TAB_RECT_TOP); return playFlag; } 複製代碼
有人說 咱們用
這個回調 會簡單一些?其實這樣也是不完美的,仍是前面那個問題,有些item 他就是有其餘要素要展現, 就會出現 視頻區域已經看不到了,被滑走了, 此時理應中止播放,可是由於還有好比視頻閱讀數等東西剛好在列表但是區域範圍以內 致使這個item沒法進入這個回調。
爲了解決這個問題,咱們使用以下方案: 在列表頁滑動的時候,斷定視頻區域是否所有在列表頁以外,人已經看不到了,符合這個條件就中止播放視頻
代碼以下:
int position = 0; if (dy > 0) { //dy>0 表明 item 從下往上 被劃出屏幕的 因此咱們只須要斷定最上面一個可視的item便可 position = ((LinearLayoutManager) mRecyclerView.getLayoutManager()).findFirstVisibleItemPosition(); } else if (dy < 0) { //dy>0 表明 item 從上往下 被劃出屏幕的 因此咱們只須要斷定最下面一個可視的item便可 position = ((LinearLayoutManager) mRecyclerView.getLayoutManager()).findLastVisibleItemPosition(); } View itemView = mRecyclerView.getLayoutManager().findViewByPosition(position); if (itemView != null) { VideoPlayer videoPlayer = itemView.findViewById(R.id.video_player); if (videoPlayer != null) { Rect rect = new Rect(); Rect gRect = new Rect(); vivoSpaceVideoPlayer.getLocalVisibleRect(rect); vivoSpaceVideoPlayer.getGlobalVisibleRect(gRect); //item 從下往上劃出屏幕 的斷定條件 final boolean stopFlagForDownToUp = (dy > 0 && rect.bottom - rect.top <= STOP_FLAG_PX_VALUE); //item 從上往下劃出屏幕的 斷定條件 ----有底部tab final boolean stopFlagForUptoDown1 = (dy < 0 && gRect.top > (MAIN_PAGE_BOTTOM_TAB_RECT_TOP - STOP_FLAG_PX_VALUE)); //item 從上往下劃出屏幕的 斷定條件 ----沒有底部tab final boolean stopFlagForUptoDown2 = (dy < 0 && rect.top != 0); if (stopFlagForDownToUp || stopFlagForUptoDownHasParent || stopFlagForUptoDownNoParent) { videoPlayer.release(); } } } 複製代碼
這裏的算法比較簡單,我就不一一解釋了,惟一要說一下的就是這個
//定義個閾值 中止播放斷定使用
private static final int STOP_FLAG_PX_VALUE = 5;
複製代碼
爲何要加一個這個?由於針對全屏座標而言,有時候滑動快了並不會通過某一個值,因此咱們要有必定的餘量。 例如 咱們條件是 當咱們的視頻區域的top座標(滑動的時候會從例如 1900開始遞增) 大於 tab的top座標(假設這個值是2100)的時候 就斷定被遮蓋了 中止滑動, 並不表明視頻區域的top座標是 1900 1901 1902 這樣1px 1px的遞增到2100的。 有時候他會忽略掉,好比某些手機常常出現1997 1998 1999 2102 等等 狀況,因此這裏咱們要必定的餘量。 確保 視頻必定會被中止播放。
當咱們網絡數據回來之後 ,咱們會把值set到RecyclerView 裏面,而後notify,此時界面就有展現了,可是這個時候 若是用戶沒有滑動,又剛好第一屏的item有一個是視頻,那咱們怎麼觸發這種條件的自動播放呢?
其實RecyclerView有個特性就是 第一次渲染完畢之後 onScrolled會被執行一次,因此咱們只要在這裏作個標誌位 而後去播放一次 就好了。
@Override public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { if (!mRvInitFlag) { playFirstVideoView(); mRvInitFlag = true; return; } 複製代碼
private void playFirstVideoView() { VLog.d(TAG, "method in playFirstVideoView"); if (mRecyclerView.getLayoutManager() != null) { //只有是 視頻 纔有意義,若是都不是視頻 那沒有任何意義 int firstPosition = ((LinearLayoutManager) mRecyclerView.getLayoutManager()).findFirstVisibleItemPosition(); int lastPosition = ((LinearLayoutManager) mRecyclerView.getLayoutManager()).findLastVisibleItemPosition(); for (int i = firstPosition; i <= lastPosition; i++) { // 若是 i<0 則直接跳過 由於毫無心義 if (i < 0) { continue; } View itemView = mRecyclerView.getLayoutManager().findViewByPosition(i); if (itemView != null && itemView.findViewById(R.id.video_player) != null) { VideoPlayer videoPlayer = itemView.findViewById(R.id.video_player); Rect rect = new Rect(); Rect gRect = new Rect(); videoPlayer.getLocalVisibleRect(rect); videoPlayer.getGlobalVisibleRect(gRect); int videoHeight = videoPlayer.getHeight(); //只要第一個元素 可視區域是完整的,那麼就直接給他播放 if ((rect.bottom - rect.top) == videoHeight && gRect.bottom < MAIN_PAGE_BOTTOM_TAB_RECT_TOP && gRect.bottom > 0) { safePlayVideoMethod(videoPlayer); //既然找到了 那麼就直接播放 不要再管其餘的了 break; } } } } 複製代碼