使用 ExoPlayer 实现 列表视频自动播放
官网地址及github地址
https://github.com/google/ExoPlayer
https://exoplayer.dev/
源码地址
视频播放代码在 .exoplayer 包
https://github.com/wuchao226/Jetpackppjoke
先看效果图
preview.gifExoPlayer 的特点及简单介绍
ExoPlayer 是 Google 官方推出的一款开源的应用级别的音视频播放框架,它是一个独立的库,所以我们可以在我们的项目中进行相应的库引用,非常的方便。也可以自己通过开源代码进行定制、修改、扩展。
简单使用
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内容 -
exoplayer-hls
:支持HLS内容 -
exoplayer-smoothstreaming
:支持SmoothStreaming内容 -
exoplayer-ui
:用于ExoPlayer的UI组件和相关的资源。
根据自己的需要进行引用,core核心包必须引用,ui包也建议引用。
开启Java8语法支持:
compileOptions {
targetCompatibility JavaVersion.VERSION_1_8
}
ExoPlayer 的 FFmpeg 扩展提供 FfmpegAudioRenderer,使用 FFmpeg 进行解码,并可以呈现各种格式编码的音频。
ExoPlayer库的核心是ExoPlayer接口,ExoPlayer的API暴露了基本上大部分的媒体播放操作功能,比如缓冲媒体、播放、暂停和快进、媒体监听等功能。
基本功能使用的话我们只需要关心这几个类:
-
PlayerView
:播放器的渲染界面UI; -
SimpleExoPlayer/ExoPlayer
:播放器核心API类; -
MediaSource
:媒体资源,用于定义要播放的媒体,加载媒体,加载音视频的播放源地址,以及从哪里加载媒体,简单的说,MediaSource就是代表我们要播放的媒体文件,可以是本地资源,可以是网络资源。MediaSource在播放开始的时候,通过ExoPlayer.prepare方法注入。,MediaSource 有很多扩展类,如 ConcatenatingMediaSource、ClippingMediaSource、LoopingMediaSource、MergingMediaSource、DashMediaSource、SsMediaSource、HlsMediaSource、ProgressiveMediaSource等,都有不同的功能。 -
TrackSelector
:轨道选择器(音轨设置),用于选择 MediaSource 提供的轨道(tracks),供每个可用的渲染器使用,一般使用 DefaultTrackSelector 即可。 -
Renderer
:渲染器,用于渲染媒体文件。当创建播放器的时候,Renderers被注入。 -
LoadControl
:用于控制 MediaSource 何时缓冲更多的媒体资源以及缓冲多少媒体资源。LoadControl 在创建播放器的时候被注入。一般使用 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库中复制到app的res目录下面,然后做相应的更改即可。 -
app:show_timeout="1000"
:控制界面自动消失时间是10秒。自定义的播放控制器和 PlayerView 绑定时, 这个播放控制器显示多久之后自动隐藏掉 -
app:keep_content_on_player_reset="false"
:player 重置时是否需要保留最后一帧, true:列表上下滑动时可能出现上个视频的最后一帧 -
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,可以复制后做相应的更改
<?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(布局层级优化之后的视频播放控制器)
<?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、首先需要一个关联类,用来管理页面上的视频播放器和显示视频画面的view以及视频播放控制器的管理类
/**
* @desciption 管理页面上的视频播放器
*/
public class PageListPlay {
/**
* 播放器核心API类
*/
public SimpleExoPlayer exoPlayer;
public PlayerView playerView;
public PlayerControlView controlView;
/**
* 代表正在播放的视频 url,并用来判断 exoPlayer 之前播放的url和即将要播放的url是否是同一个媒体资源
* 如果是同一个只需要恢复继续播放即可,反正创建新的 MediaSource 给 exoPlayer 去播放
*/
public String playUrl;
public PageListPlay() {
Application application = AppGlobals.getApplication();
//创建exoplayer播放器实例
exoPlayer = new SimpleExoPlayer.Builder(application,
//视频每一这的画面如何渲染,实现默认的实现类
new DefaultRenderersFactory(application))
//测量播放过程中的带宽,如果不需要,可以为null
.setBandwidthMeter(new DefaultBandwidthMeter.Builder(application).build())
//视频的音视频轨道如何加载,使用默认的轨道选择器
.setTrackSelector(new DefaultTrackSelector(application))
//视频缓存控制逻辑,使用默认的即可
.setLoadControl(new DefaultLoadControl())
.build();
//加载咱们布局层级优化之后的能够展示视频画面的View
playerView = (PlayerView) LayoutInflater.from(application).inflate(R.layout.layout_exo_player_view,
null, false);
//加载咱们布局层级优化之后的视频播放控制器
controlView = (PlayerControlView) LayoutInflater.from(application).inflate(R.layout.layout_exo_player_contorller_view,
null, false);
//把播放器实例 和 playerView,controlView相关联
//如此视频画面才能正常显示,播放进度条才能自动更新
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。用于页面切换视频无缝续播的场景
*
* @param newPlayerView
* @param attach
*/
public void switchPlayerView(PlayerView newPlayerView, boolean attach) {
playerView.setPlayer(attach ? null : exoPlayer);
newPlayerView.setPlayer(attach ? exoPlayer : null);
}
}
2、还需要一个管理类,用来管理每一个页面的 PageListPlay 对象。
/**
* @desciption 能适应多个页面视频播放的 播放器管理者
* 每个页面一个播放器
* 方便管理每个页面的暂停/恢复操作
*/
public class PageListPlayManager {
/**
* 播放媒体的MediaSource
*/
private static final ProgressiveMediaSource.Factory mediaSourceFactory;
/**
* 存储每一个页面对应的 PageListPlay 对象
* key:String类型的,代表每一个页面的生成标志
*/
private static HashMap<String, PageListPlay> sPageListPlayHashMap = new HashMap<>();
static {
Application application = AppGlobals.getApplication();
//创建http视频资源如何加载的工厂对象
DefaultHttpDataSourceFactory dataSourceFactory = new DefaultHttpDataSourceFactory(
Util.getUserAgent(application, application.getPackageName()));
//创建缓存,指定缓存位置,和缓存策略,为最近最少使用原则,最大为200m
Cache cache = new SimpleCache(application.getCacheDir(),
new LeastRecentlyUsedCacheEvictor(1024 * 1024 * 200), new ExoDatabaseProvider(application));
//把缓存对象cache和负责缓存数据读取、写入的工厂类CacheDataSinkFactory 相关联
CacheDataSinkFactory cacheDataSinkFactory = new CacheDataSinkFactory(cache, Long.MAX_VALUE);
/* 创建能够 边播放边缓存的 本地资源加载和http网络数据写入的工厂类
* public CacheDataSourceFactory(
* Cache cache, 缓存写入策略和缓存写入位置的对象
* DataSource.Factory upstreamFactory,http视频资源如何加载的工厂对象
* DataSource.Factory cacheReadDataSourceFactory,本地缓存数据如何读取的工厂对象
* @Nullable DataSink.Factory cacheWriteDataSinkFactory,http网络数据如何写入本地缓存的工厂对象
* @CacheDataSource.Flags int flags,加载本地缓存数据进行播放时的策略,如果遇到该文件正在被写入数据,或读取缓存数据发生错误时的策略
* @Nullable CacheDataSource.EventListener eventListener 缓存数据读取的回调
*/
CacheDataSourceFactory cacheDataSourceFactory = new CacheDataSourceFactory(
cache,
dataSourceFactory,
new FileDataSource.Factory(),
cacheDataSinkFactory,
CacheDataSource.FLAG_BLOCK_ON_CACHE,
null);
//最后 还需要创建一个 MediaSource 媒体资源 加载的工厂类
//因为由它创建的MediaSource 能够实现边缓冲边播放的效果,
//如果需要播放hls,m3u8 则需要创建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();
/**
* 活跃状态 视频可播放(满足自动播放时回调)
*/
void onActive();
/**
* 非活跃状态,暂停它(列表滚出屏幕时回调,恢复状态停止播放)
*/
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() {
/**
* 数据添加到 RecyclerView 后 回调该方法
*/
@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现在不是滚动状态
//SCROLL_STATE_DRAGGING 代表RecyclerView处于被外力引导的滚动状态,比如手指正在拖着进行滚动。
//SCROLL_STATE_SETTLING 代表RecyclerView处于自动滚动的状态,此时手指已经离开屏幕,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) {
//时序问题。当执行了AdapterDataObserver#onItemRangeInserted 可能还没有被布局到RecyclerView上。
//所以此时 recyclerView.getChildCount()还是等于0的。
//等childView 被布局到RecyclerView上之后,会执行onScrolled()方法
//并且此时 dx,dy都等于0
postAutoPlay();
} else {
//如果有正在播放的,且滑动时被划出了屏幕 则 停止他
if (mPlayingTarget != null && mPlayingTarget.isPlaying() && !isTargetInBounds(mPlayingTarget)) {
mPlayingTarget.inActive();
}
}
}
};
public PageListPlayDetector(LifecycleOwner owner, RecyclerView recyclerView) {
mRecyclerView = recyclerView;
//监听生命周期
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);
}
}
});
//监听有新的数据添加到 RecyclerView
recyclerView.getAdapter().registerAdapterDataObserver(mDataObserver);
recyclerView.addOnScrollListener(mScrollListener);
}
private void postAutoPlay() {
mRecyclerView.post(delayAutoPlay);
}
/**
* 自动播放 检测
*/
private void autoPlay() {
//判断屏幕上是否已经有视屏类型的 item
if (mTargets.size() <= 0 || mRecyclerView.getChildCount() <= 0) {
return;
}
//上一个 target 正在播放并且处于屏幕内,不需要检测新的 target
if (mPlayingTarget != null && mPlayingTarget.isPlaying() && isTargetInBounds(mPlayingTarget)) {
return;
}
IPlayTarget activeTarget = null;
for (IPlayTarget target : mTargets) {
//判断 PlayTarget 是否有一半以上的 View 处在屏幕内
boolean inBounds = isTargetInBounds(target);
if (inBounds) {
//找到满足自动播放条件的 target
activeTarget = target;
break;
}
}
if (activeTarget != null) {
//把上一个满足自动播放条件的 target 关闭
if (mPlayingTarget != null && mPlayingTarget.isPlaying()) {
//停止播放
mPlayingTarget.inActive();
}
//找到满足自动播放条件的 target,进行全局保存
mPlayingTarget = activeTarget;
//播放
activeTarget.onActive();
}
}
/**
* 检测 IPlayTarget 所在的 viewGroup 是否至少还有一半的大小在屏幕内
*
* @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上下范围内
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);
//缓冲转圈圈的view
bufferView = findViewById(R.id.buffer_view);
//封面view
cover = findViewById(R.id.cover);
//高斯模糊背景图,防止出现两边留嘿
blur = findViewById(R.id.blur_background);
//播放盒暂停的按钮
playBtn = findViewById(R.id.play_btn);
playBtn.setOnClickListener(v -> {
if (isPlaying()) {
inActive();
} else {
onActive();
}
});
this.setTransitionName("listPlayerView");
}
/**
* 视频播放状态
*
* @param playWhenReady 播放是否继续
* @param playbackState 播放状态
*/
@Override
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
//监听视频播放的状态
PageListPlay pageListPlay = PageListPlayManager.get(mCategory);
SimpleExoPlayer exoPlayer = pageListPlay.exoPlayer;
//视频已经开始播放,exoPlayer 缓存区不等于0
if (playbackState == Player.STATE_READY && exoPlayer.getBufferedPosition() != 0 && playWhenReady) {
//隐藏封面图
cover.setVisibility(GONE);
//隐藏缓冲的转圈图
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;
}
/**
* 活跃状态 视频可播放(满足自动播放时回调)
*/
@Override
public void onActive() {
//视频播放,或恢复播放
//通过该View所在页面的mCategory(比如首页列表tab_all,沙发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;
}
//此处我们需要主动调用一次 switchPlayerView,把播放器Exoplayer和展示视频画面的View ExoplayerView相关联
//为什么呢?因为在列表页点击视频Item跳转到视频详情页的时候,详情页会复用列表页的播放器Exoplayer,然后和新创建的展示视频画面的View ExoplayerView相关联,达到视频无缝续播的效果
//如果 我们再次返回列表页,则需要再次把播放器和ExoplayerView相关联
pageListPlay.switchPlayerView(playerView, true);
ViewParent parent = playerView.getParent();
if (parent != this) {
//把展示视频画面的View添加到ItemView的容器上
if (parent != null) {
((ViewGroup) parent).removeView(playerView);
//还应该暂停掉列表上正在播放的那个
((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);
}
//如果是同一个视频资源,则不需要从重新创建mediaSource。
//但需要onPlayerStateChanged 否则不会触发onPlayerStateChanged()
if (TextUtils.equals(pageListPlay.playUrl, mVideoUrl)) {
onPlayerStateChanged(true, Player.STATE_READY);
} else {
MediaSource mediaSource = PageListPlayManager.createMediaSource(mVideoUrl);
exoPlayer.prepare(mediaSource);
//循环播放模式
exoPlayer.setRepeatMode(Player.REPEAT_MODE_ONE);
//开启新的视频播放后把视频的url保存到 PageListPlay 对象里;
//用来判断 exoPlayer 之前播放的url和即将要播放的url是否是同一个媒体资源,
//如果是同一个只需要恢复继续播放即可,反正创建新的 MediaSource 给 exoPlayer 去播放
pageListPlay.playUrl = mVideoUrl;
}
controlView.show();
controlView.addVisibilityListener(this);
exoPlayer.addListener(this);
//视频缓冲好后,立马播放
exoPlayer.setPlayWhenReady(true);
}
/**
* 非活跃状态,暂停它(列表滚出屏幕时回调,恢复状态停止播放)
*/
@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) {
//点击该区域时 我们诸主动让视频控制器显示出来
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}" />
<!-- 视频区域-->
<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 适配器中的使用
关键代码
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;
//由于沙发Tab的几个子页面 复用了HomeFragment。
//我们需要判断下 当前页面 它是否有ParentFragment.
//当且仅当 它和它的ParentFragment均可见的时候,才能恢复视频播放
if (getParentFragment() != null) {
if (getParentFragment().isVisible() && isVisible()) {
playDetector.onResume();
}
} else {
if (isVisible()) {
playDetector.onResume();
}
}*/
}
@Override
public void onPause() {
//如果是跳转到详情页,咱们就不需要 暂停视频播放了
//如果是前后台切换 或者去别的页面了 都是需要暂停视频播放的
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();
}