使用 ExoPlayer 實現(xiàn) 列表視頻自動播放

官網(wǎng)地址及github地址

https://github.com/google/ExoPlayer
https://exoplayer.dev/

源碼地址

視頻播放代碼在 .exoplayer 包
https://github.com/wuchao226/Jetpackppjoke

先看效果圖

preview.gif

ExoPlayer 的特點及簡單介紹

ExoPlayer 是 Google 官方推出的一款開源的應(yīng)用級別的音視頻播放框架嫡丙,它是一個獨立的庫,所以我們可以在我們的項目中進行相應(yīng)的庫引用,非常的方便蹋半。也可以自己通過開源代碼進行定制雪侥、修改肿仑、擴展湿硝。

簡單使用

1犀斋、項目根目錄的build.gradle里添加倉庫地址

repositories {
    google()
    jcenter()
}

項目app目錄的下build.gradle里添加ExoPlayer庫地址律胀。

implementation 'com.google.android.exoplayer:exoplayer:2.X.X'
// 例如我這里使用2.11.4版本:
implementation 'com.google.android.exoplayer:exoplayer:2.11.4'

具體的版本號信息和更新的概要可以在這里查看:https://github.com/google/ExoPlayer/blob/release-v2/RELEASENOTES.md

如果只需要引入其中的幾個功能模塊的話专钉,我們也可以分拆開進行引用:

implementation 'com.google.android.exoplayer:exoplayer-core:2.X.X'
implementation 'com.google.android.exoplayer:exoplayer-dash:2.X.X'
implementation 'com.google.android.exoplayer:exoplayer-ui:2.X.X'

implementation 'com.google.android.exoplayer:exoplayer-hls:2.X.X'
implementation 'com.google.android.exoplayer:exoplayer-smoothstreaming:2.X.X'

整個ExoPlayer庫包括5個子庫,依賴了整個ExoPlayer庫和依賴5個子庫是等效的累铅。

  • exoplayer-core:核心功能 (必要)
  • exoplayer-dash:支持DASH內(nèi)容
  • exoplayer-hls:支持HLS內(nèi)容
  • exoplayer-smoothstreaming:支持SmoothStreaming內(nèi)容
  • exoplayer-ui:用于ExoPlayer的UI組件和相關(guān)的資源跃须。

根據(jù)自己的需要進行引用,core核心包必須引用娃兽,ui包也建議引用菇民。
開啟Java8語法支持:

compileOptions {
  targetCompatibility JavaVersion.VERSION_1_8
}

ExoPlayer 的 FFmpeg 擴展提供 FfmpegAudioRenderer,使用 FFmpeg 進行解碼投储,并可以呈現(xiàn)各種格式編碼的音頻第练。

ExoPlayer庫的核心是ExoPlayer接口,ExoPlayer的API暴露了基本上大部分的媒體播放操作功能玛荞,比如緩沖媒體娇掏、播放、暫停和快進勋眯、媒體監(jiān)聽等功能婴梧。

基本功能使用的話我們只需要關(guān)心這幾個類:

  • PlayerView:播放器的渲染界面UI;
  • SimpleExoPlayer/ExoPlayer:播放器核心API類客蹋;
  • MediaSource:媒體資源塞蹭,用于定義要播放的媒體,加載媒體讶坯,加載音視頻的播放源地址番电,以及從哪里加載媒體,簡單的說辆琅,MediaSource就是代表我們要播放的媒體文件漱办,可以是本地資源,可以是網(wǎng)絡(luò)資源婉烟。MediaSource在播放開始的時候娩井,通過ExoPlayer.prepare方法注入。隅很,MediaSource 有很多擴展類撞牢,如 ConcatenatingMediaSource率碾、ClippingMediaSource、LoopingMediaSource屋彪、MergingMediaSource所宰、DashMediaSource、SsMediaSource畜挥、HlsMediaSource仔粥、ProgressiveMediaSource等,都有不同的功能蟹但。
  • TrackSelector:軌道選擇器(音軌設(shè)置)躯泰,用于選擇 MediaSource 提供的軌道(tracks),供每個可用的渲染器使用华糖,一般使用 DefaultTrackSelector 即可麦向。
  • Renderer:渲染器,用于渲染媒體文件客叉。當創(chuàng)建播放器的時候诵竭,Renderers被注入。
  • LoadControl:用于控制 MediaSource 何時緩沖更多的媒體資源以及緩沖多少媒體資源兼搏。LoadControl 在創(chuàng)建播放器的時候被注入卵慰。一般使用 DefaultLoadControl 即可。

接下來我們看下具體使用步驟:
布局中引入PlayerView:
如 layout_exo_player_view.xml

<?xml version="1.0" encoding="utf-8"?>
<com.google.android.exoplayer2.ui.PlayerView 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/player_view"
    android:keepScreenOn="true"
    app:use_controller="false"
    app:show_timeout="1000"
    app:keep_content_on_player_reset="false"
    app:surface_type="texture_view"
    app:resize_mode="zoom"
    app:show_buffering="never"
    app:player_layout_id="@layout/layout_simple_exo_player_view">
</com.google.android.exoplayer2.ui.PlayerView>

屬性介紹:

  • android:keepScreenOn="true" :true:屏幕常亮
  • app:use_controller="false" :是否使用 PlayerView 提供的默認控制器(視頻加載佛呻、播放) false:不使用默認使用的播放控制界面裳朋;默認的 PlayerControlView 的控制界面是 R.layout.exo_playback_control_view.xml∠胖可以直接從ExoPlayer庫中復(fù)制到app的res目錄下面鲤嫡,然后做相應(yīng)的更改即可。
  • app:show_timeout="1000":控制界面自動消失時間是10秒夜矗。自定義的播放控制器和 PlayerView 綁定時泛范, 這個播放控制器顯示多久之后自動隱藏掉
  • app:keep_content_on_player_reset="false":player 重置時是否需要保留最后一幀, true:列表上下滑動時可能出現(xiàn)上個視頻的最后一幀
  • app:fastforward_increment="30000":快進30秒
  • app:rewind_increment="30000":快退30秒
  • app:surface_type="texture_view" :指定顯示視頻畫面 View 的類型
  • app:resize_mode="zoom":視頻畫面幀 的縮放形式
  • app:show_buffering="never":當視頻緩沖加載時是否需要顯示默認的loading加載框
    -app:player_layout_id="@layout/layout_simple_exo_player_view":指定 PlayerView 的布局樣式

自定義的 layout_simple_exo_player_view.xml:用于指定給 app:player_layout_id紊撕, 指定 PlayerView 的布局樣,默認的布局樣式是R.layout.exo_player_view赡突,可以復(fù)制后做相應(yīng)的更改

<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android">

    <com.google.android.exoplayer2.ui.AspectRatioFrameLayout
        android:id="@id/exo_content_frame"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_gravity="center">

        <!-- Video surface will be inserted as the first child of the content frame. -->
    </com.google.android.exoplayer2.ui.AspectRatioFrameLayout>
</merge>

自定義界面

ExoPlayer 默認使用的播放控制界面是PlayerControlView如果完全不想使用這個控制界面对扶,可以在布局文件里面修改

<com.google.android.exoplayer2.ui.PlayerView
   [...]
   app:use_controller="false"/>

這樣控制界面就不顯示了。
布局中引入PlayerControlView:
如 layout_exo_player_contorller_view.xml(布局層級優(yōu)化之后的視頻播放控制器)

<?xml version="1.0" encoding="utf-8"?>
<com.google.android.exoplayer2.ui.PlayerControlView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/control_view"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_gravity="bottom"
    android:orientation="vertical"
    app:controller_layout_id="@layout/layout_simple_exo_player_controller_view">
</com.google.android.exoplayer2.ui.PlayerControlView>

layout_simple_exo_player_controller_view.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_gravity="bottom"
    android:background="#0D000000"
    android:gravity="center_vertical"
    android:layoutDirection="ltr"
    android:orientation="horizontal"
    tools:targetApi="28">


    <TextView
        android:id="@id/exo_position"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:includeFontPadding="false"
        android:paddingLeft="4dp"
        android:paddingRight="4dp"
        android:textColor="#ffffff"
        android:textSize="14sp"
        android:textStyle="bold" />

    <com.google.android.exoplayer2.ui.DefaultTimeBar
        android:id="@id/exo_progress"
        android:layout_width="0dp"
        android:layout_height="26dp"
        android:layout_weight="1"
        android:visibility="visible" />

    <TextView
        android:id="@id/exo_duration"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:includeFontPadding="false"
        android:paddingLeft="4dp"
        android:paddingRight="4dp"
        android:textColor="#ffffff"
        android:textSize="14sp"
        android:textStyle="bold" />
</LinearLayout>

主要功能代碼

1惭缰、首先需要一個關(guān)聯(lián)類浪南,用來管理頁面上的視頻播放器和顯示視頻畫面的view以及視頻播放控制器的管理類

/**
 * @desciption 管理頁面上的視頻播放器
 */
public class PageListPlay {
    /**
     * 播放器核心API類
     */
    public SimpleExoPlayer exoPlayer;
    public PlayerView playerView;
    public PlayerControlView controlView;
    /**
     * 代表正在播放的視頻 url,并用來判斷 exoPlayer 之前播放的url和即將要播放的url是否是同一個媒體資源
     * 如果是同一個只需要恢復(fù)繼續(xù)播放即可漱受,反正創(chuàng)建新的 MediaSource 給 exoPlayer 去播放
     */
    public String playUrl;

    public PageListPlay() {
        Application application = AppGlobals.getApplication();
        //創(chuàng)建exoplayer播放器實例
        exoPlayer = new SimpleExoPlayer.Builder(application,
                //視頻每一這的畫面如何渲染,實現(xiàn)默認的實現(xiàn)類
                new DefaultRenderersFactory(application))
                //測量播放過程中的帶寬络凿,如果不需要,可以為null
                .setBandwidthMeter(new DefaultBandwidthMeter.Builder(application).build())
                //視頻的音視頻軌道如何加載,使用默認的軌道選擇器
                .setTrackSelector(new DefaultTrackSelector(application))
                //視頻緩存控制邏輯,使用默認的即可
                .setLoadControl(new DefaultLoadControl())
                .build();

        //加載咱們布局層級優(yōu)化之后的能夠展示視頻畫面的View
        playerView = (PlayerView) LayoutInflater.from(application).inflate(R.layout.layout_exo_player_view,
                null, false);

        //加載咱們布局層級優(yōu)化之后的視頻播放控制器
        controlView = (PlayerControlView) LayoutInflater.from(application).inflate(R.layout.layout_exo_player_contorller_view,
                null, false);

        //把播放器實例 和 playerView,controlView相關(guān)聯(lián)
        //如此視頻畫面才能正常顯示,播放進度條才能自動更新
        playerView.setPlayer(exoPlayer);
        controlView.setPlayer(exoPlayer);
    }

    public void release() {
        if (exoPlayer != null) {
            exoPlayer.setPlayWhenReady(false);
            exoPlayer.stop(true);
            exoPlayer.release();
            exoPlayer = null;
        }

        if (playerView != null) {
            playerView.setPlayer(null);
            playerView = null;
        }

        if (controlView != null) {
            controlView.setPlayer(null);
            controlView = null;
        }
    }

    /**
     * 切換與播放器 exoplayer 綁定的 exoplayerView絮记。用于頁面切換視頻無縫續(xù)播的場景
     *
     * @param newPlayerView
     * @param attach
     */
    public void switchPlayerView(PlayerView newPlayerView, boolean attach) {
        playerView.setPlayer(attach ? null : exoPlayer);
        newPlayerView.setPlayer(attach ? exoPlayer : null);
    }
}

2摔踱、還需要一個管理類,用來管理每一個頁面的 PageListPlay 對象怨愤。

/**
 * @desciption 能適應(yīng)多個頁面視頻播放的 播放器管理者
 * 每個頁面一個播放器
 * 方便管理每個頁面的暫停/恢復(fù)操作
 */
public class PageListPlayManager {
    /**
     * 播放媒體的MediaSource
     */
    private static final ProgressiveMediaSource.Factory mediaSourceFactory;
    /**
     * 存儲每一個頁面對應(yīng)的 PageListPlay 對象
     * key:String類型的派敷,代表每一個頁面的生成標志
     */
    private static HashMap<String, PageListPlay> sPageListPlayHashMap = new HashMap<>();

    static {
        Application application = AppGlobals.getApplication();
        //創(chuàng)建http視頻資源如何加載的工廠對象
        DefaultHttpDataSourceFactory dataSourceFactory = new DefaultHttpDataSourceFactory(
                Util.getUserAgent(application, application.getPackageName()));
        //創(chuàng)建緩存,指定緩存位置撰洗,和緩存策略,為最近最少使用原則,最大為200m
        Cache cache = new SimpleCache(application.getCacheDir(),
                new LeastRecentlyUsedCacheEvictor(1024 * 1024 * 200), new ExoDatabaseProvider(application));
        //把緩存對象cache和負責緩存數(shù)據(jù)讀取篮愉、寫入的工廠類CacheDataSinkFactory 相關(guān)聯(lián)
        CacheDataSinkFactory cacheDataSinkFactory = new CacheDataSinkFactory(cache, Long.MAX_VALUE);

        /* 創(chuàng)建能夠 邊播放邊緩存的 本地資源加載和http網(wǎng)絡(luò)數(shù)據(jù)寫入的工廠類
         * public CacheDataSourceFactory(
         *       Cache cache, 緩存寫入策略和緩存寫入位置的對象
         *       DataSource.Factory upstreamFactory,http視頻資源如何加載的工廠對象
         *       DataSource.Factory cacheReadDataSourceFactory,本地緩存數(shù)據(jù)如何讀取的工廠對象
         *       @Nullable DataSink.Factory cacheWriteDataSinkFactory,http網(wǎng)絡(luò)數(shù)據(jù)如何寫入本地緩存的工廠對象
         *       @CacheDataSource.Flags int flags,加載本地緩存數(shù)據(jù)進行播放時的策略,如果遇到該文件正在被寫入數(shù)據(jù),或讀取緩存數(shù)據(jù)發(fā)生錯誤時的策略
         *       @Nullable CacheDataSource.EventListener eventListener  緩存數(shù)據(jù)讀取的回調(diào)
         */
        CacheDataSourceFactory cacheDataSourceFactory = new CacheDataSourceFactory(
                cache,
                dataSourceFactory,
                new FileDataSource.Factory(),
                cacheDataSinkFactory,
                CacheDataSource.FLAG_BLOCK_ON_CACHE,
                null);

        //最后 還需要創(chuàng)建一個 MediaSource 媒體資源 加載的工廠類
        //因為由它創(chuàng)建的MediaSource 能夠?qū)崿F(xiàn)邊緩沖邊播放的效果,
        //如果需要播放hls,m3u8 則需要創(chuàng)建DashMediaSource.Factory()
        mediaSourceFactory = new ProgressiveMediaSource.Factory(cacheDataSourceFactory);
    }

    public static MediaSource createMediaSource(String url) {
        return mediaSourceFactory.createMediaSource(Uri.parse(url));
    }
     /**
     * 獲取每一個頁面的 PageListPlay 對象
     */
    public static PageListPlay get(String pageName) {
        PageListPlay pageListPlay = sPageListPlayHashMap.get(pageName);
        if (pageListPlay == null) {
            pageListPlay = new PageListPlay();
            sPageListPlayHashMap.put(pageName, pageListPlay);
        }
        return pageListPlay;
    }
    /**
     * 銷毀
     */
    public static void release(String pageName) {
        PageListPlay pageListPlay = sPageListPlayHashMap.get(pageName);
        if (pageListPlay != null) {
            pageListPlay.release();
        }
    }
}

3、列表滾動時自動播放的檢測邏輯差导,并寫個接口试躏,面向接口來編程

/**
 * @desciption 視頻播放的 接口
 */
public interface IPlayTarget {
    /**
     * 得到 PlayerView 所在的容器,得到 View 后才能在列表滾動的時候去檢測它的位置是否滿足自動播放
     *
     * @return ViewGroup
     */
    ViewGroup getOwner();

    /**
     * 活躍狀態(tài) 視頻可播放(滿足自動播放時回調(diào))
     */
    void onActive();

    /**
     * 非活躍狀態(tài)设褐,暫停它(列表滾出屏幕時回調(diào)颠蕴,恢復(fù)狀態(tài)停止播放)
     */
    void inActive();

    /**
     * 當前 PlayTarget 是否在播放,幫助我們完成自動播放檢測邏輯
     *
     * @return boolean
     */
    boolean isPlaying();
}

/**
 * @desciption 列表視頻自動播放 檢測邏輯
 */
public class PageListPlayDetector {

    /**
     * 收集一個個的能夠進行視頻播放的 對象络断,面向接口
     */
    private List<IPlayTarget> mTargets = new ArrayList<>();
    private RecyclerView mRecyclerView;
    /**
     * 正在播放的那個
     */
    private IPlayTarget mPlayingTarget;
    /**
     * RecyclerView 在屏幕上的位置
     */
    private Pair<Integer, Integer> rvLocation = null;
    private Runnable delayAutoPlay = new Runnable() {
        @Override
        public void run() {
            autoPlay();
        }
    };
    private RecyclerView.AdapterDataObserver mDataObserver = new RecyclerView.AdapterDataObserver() {
        /**
         * 數(shù)據(jù)添加到 RecyclerView 后 回調(diào)該方法
         */
        @Override
        public void onItemRangeInserted(int positionStart, int itemCount) {
            super.onItemRangeInserted(positionStart, itemCount);
            autoPlay();
        }
    };
    private RecyclerView.OnScrollListener mScrollListener = new RecyclerView.OnScrollListener() {
        @Override
        public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
            super.onScrollStateChanged(recyclerView, newState);
            //SCROLL_STATE_IDLE 代表RecyclerView現(xiàn)在不是滾動狀態(tài)
            //SCROLL_STATE_DRAGGING 代表RecyclerView處于被外力引導的滾動狀態(tài)裁替,比如手指正在拖著進行滾動。
            //SCROLL_STATE_SETTLING 代表RecyclerView處于自動滾動的狀態(tài)貌笨,此時手指已經(jīng)離開屏幕弱判,RecyclerView的滾動是自身的慣性在維持
            if (newState == RecyclerView.SCROLL_STATE_IDLE) {
                autoPlay();
            }
        }

        /**
         * 獲取RecyclerView的滾動距離
         */
        @Override
        public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
            super.onScrolled(recyclerView, dx, dy);
            //當dx > 0 時,代表手指向左拖動锥惋,RecyclerView則從右向左滾動昌腰。
            //當dx < 0時,代表手指向右拖動膀跌,RecyclerView則從左向右滾動遭商。
            //當dy > 0時,代表手指向上拖動捅伤,RecyclerView則從上向下滾動(就是我們最常見的劫流,從頂部開始往下滾動)。
            //當dy < 0時丛忆,代表手指向下拖動祠汇,RecyclerView則從下向上滾動(就是從列表底部往回揮動)。

            if (dx == 0 && dy == 0) {
                //時序問題熄诡。當執(zhí)行了AdapterDataObserver#onItemRangeInserted  可能還沒有被布局到RecyclerView上可很。
                //所以此時 recyclerView.getChildCount()還是等于0的。
                //等childView 被布局到RecyclerView上之后凰浮,會執(zhí)行onScrolled()方法
                //并且此時 dx,dy都等于0
                postAutoPlay();
            } else {
                //如果有正在播放的,且滑動時被劃出了屏幕 則 停止他
                if (mPlayingTarget != null && mPlayingTarget.isPlaying() && !isTargetInBounds(mPlayingTarget)) {
                    mPlayingTarget.inActive();
                }
            }
        }
    };

    public PageListPlayDetector(LifecycleOwner owner, RecyclerView recyclerView) {
        mRecyclerView = recyclerView;
        //監(jiān)聽生命周期
        owner.getLifecycle().addObserver(new LifecycleEventObserver() {
            @Override
            public void onStateChanged(@NonNull LifecycleOwner source, @NonNull Lifecycle.Event event) {
                if (event == Lifecycle.Event.ON_DESTROY) {
                    mPlayingTarget = null;
                    mTargets.clear();
                    recyclerView.removeOnScrollListener(mScrollListener);
                    owner.getLifecycle().removeObserver(this);
                }
            }
        });
        //監(jiān)聽有新的數(shù)據(jù)添加到 RecyclerView
        recyclerView.getAdapter().registerAdapterDataObserver(mDataObserver);
        recyclerView.addOnScrollListener(mScrollListener);
    }

    private void postAutoPlay() {
        mRecyclerView.post(delayAutoPlay);
    }

    /**
     * 自動播放 檢測
     */
    private void autoPlay() {
        //判斷屏幕上是否已經(jīng)有視屏類型的 item
        if (mTargets.size() <= 0 || mRecyclerView.getChildCount() <= 0) {
            return;
        }
        //上一個 target 正在播放并且處于屏幕內(nèi)我抠,不需要檢測新的 target
        if (mPlayingTarget != null && mPlayingTarget.isPlaying() && isTargetInBounds(mPlayingTarget)) {
            return;
        }

        IPlayTarget activeTarget = null;
        for (IPlayTarget target : mTargets) {
            //判斷 PlayTarget 是否有一半以上的 View 處在屏幕內(nèi)
            boolean inBounds = isTargetInBounds(target);
            if (inBounds) {
                //找到滿足自動播放條件的 target
                activeTarget = target;
                break;
            }
        }
        if (activeTarget != null) {
            //把上一個滿足自動播放條件的 target 關(guān)閉
            if (mPlayingTarget != null && mPlayingTarget.isPlaying()) {
                //停止播放
                mPlayingTarget.inActive();
            }
            //找到滿足自動播放條件的 target苇本,進行全局保存
            mPlayingTarget = activeTarget;
            //播放
            activeTarget.onActive();
        }
    }

    /**
     * 檢測 IPlayTarget 所在的 viewGroup 是否至少還有一半的大小在屏幕內(nèi)
     *
     * @param target IPlayTarget
     * @return boolean
     */
    private boolean isTargetInBounds(IPlayTarget target) {
        //得到 PlayerView 所在的容器
        ViewGroup owner = target.getOwner();
        //RecyclerView 在屏幕上的位置
        ensureRecyclerViewLocation();
        //如果 owner 沒有被展示出來或者沒有 Attached 到 Window 上面
        if (!owner.isShown() || !owner.isAttachedToWindow()) {
            return false;
        }
        //計算 owner 在屏幕上的位置
        int[] location = new int[2];
        owner.getLocationOnScreen(location);
        //計算 owner 的中心在屏幕上的位置
        int center = location[1] + owner.getHeight() / 2;

        //承載視頻播放畫面的ViewGroup它需要至少一半的大小 在RecyclerView上下范圍內(nèi)
        return center >= rvLocation.first && center <= rvLocation.second;
    }

    private Pair<Integer, Integer> ensureRecyclerViewLocation() {
        if (rvLocation == null) {
            int[] location = new int[2];
            mRecyclerView.getLocationOnScreen(location);
            int top = location[1];
            int bottom = top + mRecyclerView.getHeight();
            rvLocation = new Pair(top, bottom);
        }
        return rvLocation;
    }

    public void addTarget(IPlayTarget target) {
        mTargets.add(target);
    }

    public void removeTarget(IPlayTarget target) {
        mTargets.remove(target);
    }

    public void onPause() {
        if (mPlayingTarget != null) {
            mPlayingTarget.inActive();
        }
    }

    public void onResume() {
        if (mPlayingTarget != null) {
            mPlayingTarget.onActive();
        }
    }
}

用于列表視頻播放 ListPlayerView

/**
 * @desciption: 列表視頻播放專用
 */
public class ListPlayerView extends FrameLayout implements IPlayTarget, PlayerControlView.VisibilityListener,
        Player.EventListener {

    public View bufferView;
    public PPImageView cover, blur;
    protected AppCompatImageView playBtn;
    protected String mCategory;
    protected String mVideoUrl;
    protected boolean isPlaying;
    protected int mWidthPx;
    protected int mHeightPx;

    public ListPlayerView(@NonNull Context context) {
        this(context, null);
    }

    public ListPlayerView(@NonNull Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public ListPlayerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

    public ListPlayerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);

        LayoutInflater.from(context).inflate(R.layout.layout_player_view, this, true);

        //緩沖轉(zhuǎn)圈圈的view
        bufferView = findViewById(R.id.buffer_view);
        //封面view
        cover = findViewById(R.id.cover);
        //高斯模糊背景圖,防止出現(xiàn)兩邊留嘿
        blur = findViewById(R.id.blur_background);
        //播放盒暫停的按鈕
        playBtn = findViewById(R.id.play_btn);

        playBtn.setOnClickListener(v -> {
            if (isPlaying()) {
                inActive();
            } else {
                onActive();
            }
        });

        this.setTransitionName("listPlayerView");
    }

    /**
     * 視頻播放狀態(tài)
     *
     * @param playWhenReady 播放是否繼續(xù)
     * @param playbackState 播放狀態(tài)
     */
    @Override
    public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
        //監(jiān)聽視頻播放的狀態(tài)
        PageListPlay pageListPlay = PageListPlayManager.get(mCategory);
        SimpleExoPlayer exoPlayer = pageListPlay.exoPlayer;
        //視頻已經(jīng)開始播放,exoPlayer 緩存區(qū)不等于0
        if (playbackState == Player.STATE_READY && exoPlayer.getBufferedPosition() != 0 && playWhenReady) {
            //隱藏封面圖
            cover.setVisibility(GONE);
            //隱藏緩沖的轉(zhuǎn)圈圖
            bufferView.setVisibility(GONE);
        } else if (playbackState == Player.STATE_BUFFERING) {
            //視頻在緩沖

            //顯示緩沖圖
            bufferView.setVisibility(VISIBLE);
        }
        isPlaying = playbackState == Player.STATE_READY && exoPlayer.getBufferedPosition() != 0 && playWhenReady;
        playBtn.setImageResource(isPlaying ? R.drawable.icon_video_pause : R.drawable.icon_video_play);
    }

    public void bindData(String category, int widthPx, int heightPx, String coverUrl, String videoUrl) {
        mCategory = category;
        mVideoUrl = videoUrl;
        mWidthPx = widthPx;
        mHeightPx = heightPx;
        cover.setImageUrl(coverUrl);

        //如果該視頻的寬度小于高度,則高斯模糊背景圖顯示出來
        if (widthPx < heightPx) {
            PPImageView.setBlurImageUrl(blur, coverUrl, 10);
            blur.setVisibility(VISIBLE);
        } else {
            blur.setVisibility(INVISIBLE);
        }
        setSize(widthPx, heightPx);
    }

    protected void setSize(int widthPx, int heightPx) {
        //這里主要是做視頻寬大與高,或者高大于寬時  視頻的等比縮放
        int maxWidth = PixUtils.getScreenWidth();
        int maxHeight = maxWidth;

        int layoutWidth = maxWidth;
        int layoutHeight = 0;

        int coverWidth;
        int coverHeight;
        if (widthPx >= heightPx) {
            coverWidth = maxWidth;
            layoutHeight = coverHeight = (int) (heightPx / (widthPx * 1.0f / maxWidth));
        } else {
            layoutHeight = coverHeight = maxHeight;
            coverWidth = (int) (widthPx / (heightPx * 1.0f / maxHeight));
        }

        ViewGroup.LayoutParams params = getLayoutParams();
        params.width = layoutWidth;
        params.height = layoutHeight;
        setLayoutParams(params);

        ViewGroup.LayoutParams blurParams = blur.getLayoutParams();
        blurParams.width = layoutWidth;
        blurParams.height = layoutHeight;
        blur.setLayoutParams(blurParams);

        FrameLayout.LayoutParams coverParams = (LayoutParams) cover.getLayoutParams();
        coverParams.width = coverWidth;
        coverParams.height = coverHeight;
        coverParams.gravity = Gravity.CENTER;
        cover.setLayoutParams(coverParams);

        FrameLayout.LayoutParams playBtnParams = (LayoutParams) playBtn.getLayoutParams();
        playBtnParams.gravity = Gravity.CENTER;
        playBtn.setLayoutParams(playBtnParams);
    }

    @Override
    public void onVisibilityChange(int visibility) {
        playBtn.setVisibility(visibility);
        playBtn.setImageResource(isPlaying() ? R.drawable.icon_video_pause : R.drawable.icon_video_play);
    }

    /**
     * 得到 PlayerView 所在的容器菜拓,得到 View 后才能在列表滾動的時候去檢測它的位置是否滿足自動播放
     */
    @Override
    public ViewGroup getOwner() {
        return this;
    }

    /**
     * 活躍狀態(tài) 視頻可播放(滿足自動播放時回調(diào))
     */
    @Override
    public void onActive() {
        //視頻播放,或恢復(fù)播放

        //通過該View所在頁面的mCategory(比如首頁列表tab_all,沙發(fā)tab的tab_video,標簽帖子聚合的tag_feed) 字段瓣窄,
        //取出管理該頁面的 Exoplayer 播放器,ExoplayerView 播放 View,控制器對象 PageListPlay
        PageListPlay pageListPlay = PageListPlayManager.get(mCategory);
        PlayerView playerView = pageListPlay.playerView;
        PlayerControlView controlView = pageListPlay.controlView;
        SimpleExoPlayer exoPlayer = pageListPlay.exoPlayer;
        if (playerView == null) {
            return;
        }
        //此處我們需要主動調(diào)用一次 switchPlayerView尘惧,把播放器Exoplayer和展示視頻畫面的View ExoplayerView相關(guān)聯(lián)
        //為什么呢康栈?因為在列表頁點擊視頻Item跳轉(zhuǎn)到視頻詳情頁的時候,詳情頁會復(fù)用列表頁的播放器Exoplayer喷橙,然后和新創(chuàng)建的展示視頻畫面的View ExoplayerView相關(guān)聯(lián)啥么,達到視頻無縫續(xù)播的效果
        //如果 我們再次返回列表頁,則需要再次把播放器和ExoplayerView相關(guān)聯(lián)
        pageListPlay.switchPlayerView(playerView, true);

        ViewParent parent = playerView.getParent();
        if (parent != this) {
            //把展示視頻畫面的View添加到ItemView的容器上
            if (parent != null) {
                ((ViewGroup) parent).removeView(playerView);
                //還應(yīng)該暫停掉列表上正在播放的那個
                ((ListPlayerView) parent).inActive();
            }
            ViewGroup.LayoutParams coverParams = cover.getLayoutParams();
            this.addView(playerView, 1, coverParams);
        }

        ViewParent ctrlParent = controlView.getParent();
        if (ctrlParent != this) {
            //把視頻控制器 添加到ItemView的容器上
            if (ctrlParent != null) {
                ((ViewGroup) ctrlParent).removeView(controlView);
            }
            FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
            params.gravity = Gravity.BOTTOM;
            this.addView(controlView, params);
        }

        //如果是同一個視頻資源,則不需要從重新創(chuàng)建mediaSource贰逾。
        //但需要onPlayerStateChanged 否則不會觸發(fā)onPlayerStateChanged()
        if (TextUtils.equals(pageListPlay.playUrl, mVideoUrl)) {
            onPlayerStateChanged(true, Player.STATE_READY);
        } else {
            MediaSource mediaSource = PageListPlayManager.createMediaSource(mVideoUrl);
            exoPlayer.prepare(mediaSource);
            //循環(huán)播放模式
            exoPlayer.setRepeatMode(Player.REPEAT_MODE_ONE);
             //開啟新的視頻播放后把視頻的url保存到 PageListPlay 對象里悬荣;
            //用來判斷 exoPlayer 之前播放的url和即將要播放的url是否是同一個媒體資源,
            //如果是同一個只需要恢復(fù)繼續(xù)播放即可疙剑,反正創(chuàng)建新的 MediaSource 給 exoPlayer 去播放
            pageListPlay.playUrl = mVideoUrl;
        }
        controlView.show();
        controlView.addVisibilityListener(this);
        exoPlayer.addListener(this);
        //視頻緩沖好后氯迂,立馬播放
        exoPlayer.setPlayWhenReady(true);
    }

    /**
     * 非活躍狀態(tài),暫停它(列表滾出屏幕時回調(diào)言缤,恢復(fù)狀態(tài)停止播放)
     */
    @Override
    public void inActive() {
        //暫停視頻的播放并讓封面圖和 開始播放按鈕 顯示出來
        PageListPlay pageListPlay = PageListPlayManager.get(mCategory);
        if (pageListPlay.controlView == null || pageListPlay.exoPlayer == null) {
            return;
        }
        //暫停視頻播放
        pageListPlay.exoPlayer.setPlayWhenReady(false);
        pageListPlay.controlView.removeVisibilityListener(this);
        pageListPlay.exoPlayer.removeListener(this);
        cover.setVisibility(VISIBLE);
        playBtn.setVisibility(VISIBLE);
        playBtn.setImageResource(R.drawable.icon_video_play);
    }

    /**
     * 當前 PlayTarget 是否在播放嚼蚀,幫助我們完成自動播放檢測邏輯
     */
    @Override
    public boolean isPlaying() {
        return isPlaying;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //點擊該區(qū)域時 我們諸主動讓視頻控制器顯示出來
        PageListPlay pageListPlay = PageListPlayManager.get(mCategory);
        pageListPlay.controlView.show();
        return true;
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        isPlaying = false;
        bufferView.setVisibility(GONE);
        cover.setVisibility(VISIBLE);
        playBtn.setVisibility(VISIBLE);
        playBtn.setImageResource(R.drawable.icon_video_play);
    }

    /**
     * 獲取視頻播放控制器
     */
    public View getPlayController() {
        PageListPlay listPlay = PageListPlayManager.get(mCategory);
        return listPlay.controlView;
    }
}

ListPlayerView 的具體使用

1、xml 布局

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>

        <variable
            name="feed"
            type="com.wuc.jetpackppjoke.model.Feed" />

    </data>

    <androidx.appcompat.widget.LinearLayoutCompat
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/color_white"
        android:orientation="vertical"
        android:paddingTop="@dimen/dp_10">

        <include
            layout="@layout/layout_feed_author"
            app:user="@{feed.author}" />

        <include
            layout="@layout/layout_feed_text"
            app:feedText="@{feed.feeds_text}"
            app:lines="@{3}" />

        <!--   視頻區(qū)域-->
        <com.wuc.jetpackppjoke.view.ListPlayerView
            android:id="@+id/list_player_view"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginBottom="@dimen/dp_10" />

        <include
            layout="@layout/layout_feed_tag"
            app:tagText="@{feed.activityText}" />

        <include
            layout="@layout/layout_feed_top_comment"
            app:comment="@{feed.topComment}" />

        <include
            android:id="@+id/interaction_binding"
            layout="@layout/layout_feed_interaction"
            app:feed="@{feed}" />
    </androidx.appcompat.widget.LinearLayoutCompat>
</layout>

2管挟、adapter 適配器中的使用
關(guān)鍵代碼

LayoutFeedTypeVideoBinding videoBinding = (LayoutFeedTypeVideoBinding) mBinding;
videoBinding.listPlayerView.bindData(mCategory, item.width, item.height, item.cover, item.url);
listPlayerView = videoBinding.listPlayerView;

public boolean isVideoItem() {
   return mBinding instanceof LayoutFeedTypeVideoBinding;
}

public ListPlayerView getListPlayerView() {
   return listPlayerView;
}

3轿曙、在 Fragment 中使用

 private PageListPlayDetector playDetector;
 private boolean shouldPause = true;

 @Override
 public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
     super.onViewCreated(view, savedInstanceState);
     playDetector = new PageListPlayDetector(this, mRecyclerView);
 }

new FeedAdapter(getContext(), feedType){
    @Override
    public void onViewAttachedToWindow(@NonNull ViewHolder holder) {
       super.onViewAttachedToWindow(holder);
        if (holder.isVideoItem()){
            playDetector.addTarget(holder.getListPlayerView());
        }
    }

    @Override
    public void onViewDetachedFromWindow(@NonNull ViewHolder holder) {
        super.onViewDetachedFromWindow(holder);
        playDetector.removeTarget(holder.getListPlayerView());
    }
};

 @Override
    public void onResume() {
        super.onResume();
        playDetector.onResume();
        /*shouldPause = true;
        //由于沙發(fā)Tab的幾個子頁面 復(fù)用了HomeFragment。
        //我們需要判斷下 當前頁面 它是否有ParentFragment.
        //當且僅當 它和它的ParentFragment均可見的時候僻孝,才能恢復(fù)視頻播放
        if (getParentFragment() != null) {
            if (getParentFragment().isVisible() && isVisible()) {
                playDetector.onResume();
            }
        } else {
            if (isVisible()) {
                playDetector.onResume();
            }
        }*/
    }

    @Override
    public void onPause() {
        //如果是跳轉(zhuǎn)到詳情頁,咱們就不需要 暫停視頻播放了
        //如果是前后臺切換 或者去別的頁面了 都是需要暫停視頻播放的
        if (shouldPause) {
            playDetector.onPause();
        }
        super.onPause();
    }

    @Override
    public void onHiddenChanged(boolean hidden) {
        super.onHiddenChanged(hidden);
        if (hidden) {
            playDetector.onPause();
        } else {
            playDetector.onResume();
        }
    }

    @Override
    public void onDestroy() {
        //記得銷毀
        PageListPlayManager.release(feedType);
        super.onDestroy();
    }
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末导帝,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子穿铆,更是在濱河造成了極大的恐慌您单,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件荞雏,死亡現(xiàn)場離奇詭異虐秦,居然都是意外死亡,警方通過查閱死者的電腦和手機凤优,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進店門羡疗,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人别洪,你說我怎么就攤上這事×危” “怎么了挖垛?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵痒钝,是天一觀的道長。 經(jīng)常有香客問我痢毒,道長送矩,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任哪替,我火速辦了婚禮栋荸,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘凭舶。我一直安慰自己晌块,他們只是感情好,可當我...
    茶點故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布帅霜。 她就那樣靜靜地躺著匆背,像睡著了一般。 火紅的嫁衣襯著肌膚如雪身冀。 梳的紋絲不亂的頭發(fā)上钝尸,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天,我揣著相機與錄音搂根,去河邊找鬼珍促。 笑死,一個胖子當著我的面吹牛剩愧,可吹牛的內(nèi)容都是我干的猪叙。 我是一名探鬼主播,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼隙咸,長吁一口氣:“原來是場噩夢啊……” “哼沐悦!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起五督,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤藏否,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后充包,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體副签,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年基矮,在試婚紗的時候發(fā)現(xiàn)自己被綠了淆储。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡家浇,死狀恐怖本砰,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情钢悲,我是刑警寧澤点额,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布舔株,位于F島的核電站,受9級特大地震影響还棱,放射性物質(zhì)發(fā)生泄漏载慈。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一珍手、第九天 我趴在偏房一處隱蔽的房頂上張望办铡。 院中可真熱鬧,春花似錦琳要、人聲如沸寡具。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽晒杈。三九已至,卻和暖如春孔厉,著一層夾襖步出監(jiān)牢的瞬間拯钻,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工撰豺, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留粪般,地道東北人。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓污桦,卻偏偏與公主長得像亩歹,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子凡橱,可洞房花燭夜當晚...
    茶點故事閱讀 42,722評論 2 345