秀品中视频播放模块的解析(上)
视频的列表播放介绍
在社交类App中,对视频的播放日渐流行,Facebook, Instagram,微博等都已支持在显示列表中直接播放视频。本文将以秀品项目源码来介绍列表播放视频的实现,希望通过该文章可以理解其实现原理以及如何使用该功能模块。
实现列表中播放视频,主要需要解决一下问题:
- 用户的手势处理,滑动列表时根据计算更新视频的状态。
- 视频的状态管理和控制。
滑动手势处理
列表的显示实现,使用RecyclerView来完成的,主要的类有:
- 自定义的RecyclerView,ViewPostRecyclerView
- 自定义Adapter,AdapterPlayer
- 自定义ViewHolder,VHolderPlayer
- 视频和RecyclerView的连接控制类,VideoRecyclerViewAutoControlAttacher
以上类中,AdapterPlayer只是简单的添加了ViewHolder以及在第一次加载itemView的相应操作,VHolderPlayer对视频相关的View做了封装。滑动手势的响应主要交由VideoRecyclerViewAutoControlAttacher类(以下简称Attacher类)来处理,Attacher类作为RecyclerView和视频类的连接类,控制的视频播放,暂停以及回收的时机,以及对RecyclerView滑动手势的监听和处理来进行最终的决策,由于第一次RecyclerView在加载ItemViews时并不伴随着手势,不能触发视频的播放,所以Adapter提供了一个接口OnFirstHolderBindListener,由Attacher实现:
@Override
public void onFirstHolderBind() {
if (mTimer != null) {
mTimer.cancel();
mTimer.purge();
}
if (mIsAutoPlayEnabled) {
mTimer = new Timer();
mTimer.schedule(new TimerTask() {
@Override
public void run() {
//涉及到界面相关更新,通过handler指定延时时间触发播放
mViewHandler.obtainMessage().sendToTarget();
}
}, FIRST_CHECK_DELAY);
}
}
在视频相关的操作中需要Wifi网络条件下才被允许,网络状态变化后需要更新视频的播放状态,Attraher通过事件总线的方式来进行操作。
接下来重点看一下手势的监听处理。
Attracher类通过一个attch方法来持有RecyclerView对象,在该方法中,对成员变量进行了一些初始化,比如确定ItemView的header高,回收操作的大小边界,以及最重要的滑动手势监听的实现:
public void attach(final RecyclerView loadMoreRecyclerView) {
if (loadMoreRecyclerView != null && loadMoreRecyclerView.getLayoutManager() != null && loadMoreRecyclerView.getAdapter() != null) {
……
mVideoHeight = ScreenUtil.getScreenWidth(context);
//ItemView中不只是视频,视频上的宽度即为headerHeight,此处为52dp
mHeaderHeight = resources.getDimensionPixelSize(R.dimen.post_holder_header_height);
//上滑回收的最大值,当显示的部分小于该大小时回收,此处为43dp
mReleaseUpLimit = resources.getDimensionPixelSize(R.dimen.post_holder_action_container_height);
//下滑回收的最大值
mReleaseDownLimit = (int) (resources.getDimensionPixelSize(R.dimen.post_holder_header_height) / 3f * 2);
//该类用与滑动方向的判定,在Sroll事件中被调用
final ScrollDirectionDetector scrollDirectionDetector = new ScrollDirectionDetector(new ScrollDirectionDetector.OnDetectScrollListener() {
@Override
public void onScrollDirectionChanged(int scrollDirection) {
mScrollDirection = scrollDirection;
}
});
//RecyclerView添加滑动事件监听
loadMoreRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
// 滑动停止时根据当前位置激活视频播放
……
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
// 滑动过程中一旦触发停止播放的条件则立即执行
……
}
});
return;
}
throw new IllegalArgumentException("必须先设置 Adapter 与 LayoutManager 后才能调用 attach()");
}
scrollDirectionDetector对象会在onScrolled中被调用,即在滑动过程中来更新动作状态,实现方法可以看一下该类下的onDetectedListScroll方法,其原理是根据当前ItemView位置和上一次保存的位置比较,如果相同就获取View的top值进行比较,不同则直接与上一次位置比较确定是上滑还是下,最后保存这次位置和top值:
public void onDetectedListScroll(RecyclerView recyclerView, int firstVisibleItem) {
//移到判断内?
View view = recyclerView.getChildAt(0);
int top = (view == null) ? 0 : view.getTop();
//如果第一个可见的Item与上一次一致,就判断其View的Top值,最后确定是上滑还是下滑
if (firstVisibleItem == mOldFirstVisibleItem) {
if (top > mOldTop) {
onScrollDown(); //更新状态,回调OnDetectScrollListener接口的方法
} else if (top < mOldTop) {
onScrollUp();
}
} else {
//不同的item直接判断上下滑
if (firstVisibleItem < mOldFirstVisibleItem) {
onScrollDown();
} else {
onScrollUp();
}
}
mOldTop = top;
mOldFirstVisibleItem = firstVisibleItem;
}
在onScrolled滑动事件中,还做了以下两件事:
- 通过计算获取需要停止播放的videoToStop(ViewHolder)
- 确定需要播放的ViewHolder所在位置值
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
// 滑动过程中一旦触发停止播放的条件则立即执行
// mRecyclerViewHeaderCount = loadMoreRecyclerView.getHeaderCount();
mRecyclerViewHeaderCount = 0;
//获取第一个和最后一个可见Item的位置值,不包含头部
int firstVisiblePos = mLayoutManager.findFirstVisibleItemPosition() - mRecyclerViewHeaderCount;
int lastVisiblePos = mLayoutManager.findLastVisibleItemPosition() - mRecyclerViewHeaderCount;
//检测滑动方向,更新mScrollDirection
scrollDirectionDetector.onDetectedListScroll(recyclerView, firstVisiblePos);
if (mScrollDirection == ScrollDirectionDetector.UP) {
//根据位置获取要停止的ViewHolder
VHolderPlayer videoToStop = findDeactivateHolderWhenUp(firstVisiblePos);
if (videoToStop != null) {
//停止
videoToStop.deactivate();
PlayerApp.getAppInstance().getPlayerPool().removeCompleteNoAutoPlay(videoToStop.getPlayerKey());
//如果ViewHolder位置满足回收的大小,直接释放资源
if (videoToStop.itemView.getHeight() - mRect4UpDeactivate.top < mReleaseUpLimit) {
//释放资源
videoToStop.release();
}
}
//获取需要播放的ViewHolder位置
int posToActivate = findActivateHolderWhenUp(lastVisiblePos);
if (posToActivate != -1) {
//更新成员变量,在停止滑动时根据该值播放视频
mPositionToActivate = posToActivate;
}
} else if (mScrollDirection == ScrollDirectionDetector.DOWN) {
VHolderPlayer videoToStop = findDeactivateHolderWhenDown(lastVisiblePos);
if (videoToStop != null) {
videoToStop.deactivate();
PlayerApp.getAppInstance().getPlayerPool().removeCompleteNoAutoPlay(videoToStop.getPlayerKey());
if (mRect4DownDeactivate.bottom < mReleaseDownLimit) {
videoToStop.release();
}
}
int posToActivate = findActivateHolderWhenDown(firstVisiblePos);
if (posToActivate != -1) {
mPositionToActivate = posToActivate;
}
}
}
那么如何获取需要停止的ViewHolder以及要播放的位置呢?其两者的思路是一致的,大致的原理是通过判断ItemView中的视频区域(TextureView)的可见范围高度大小和其滑动的分享来做处理的。最大的问题是如何获取可见区域,幸运的是Android中View类提供了一个getLocalVisibleRect(Rect r)方法,此方法可以通过传入一个Rect对象,来获取当前显示区域的宽高大小,得到的四个点lift,top,right,bottom的值是以该view的左上角为参考点的值,可以通过下图进行理解。
关于实际的操作,可以通过下图来理解整个RecyclerView在滑动过程中的计算操作:
recyclerview-video
以上滑为例,在onScrolled中会执行获取停止ViewHodler的findDeactivateHolderWhenUp方法和获取需要播放位置的findActivateHolderWhenUp方法,其两者的方法步骤类似,通过getLocalVisiable方法获取显示的区域,然后计算得到TextureView的区域与一个设定好的基准值做比较(图中的x和y值),在该代码中,基准值为屏幕宽度的三分之一,当被隐藏的TextureView超过该值,进行播放或暂停。代码如下:
/**
* 列表向上移动时 - 尝试找出需要停止播放的 ViewHolder
*
* @param firstVisiblePos 列表首个可见的 item 的位置
* @return 需要停止播放的 ViewHolder,如果没有找到返回 null
*/
private VHolderPlayer findDeactivateHolderWhenUp(int firstVisiblePos) {
//获取当前ViewHolder的位置
int currentCalcPos = firstVisiblePos + mRecyclerViewHeaderCount;
if (currentCalcPos > mRecyclerViewHeaderCount - 1) {
//在RecyclerView中获取ViewHolder
RecyclerView.ViewHolder viewHolder = mRecyclerView.findViewHolderForAdapterPosition(currentCalcPos);
if (viewHolder instanceof VHolderPlayer) {
VHolderPlayer currentVideoHolder = (VHolderPlayer) viewHolder;
//获取显示View的Rect,即该View显示相对于自己完整的Rect的位置区域
currentVideoHolder.itemView.getLocalVisibleRect(mRect4UpDeactivate);
//TextureView区域是否大于基准值
if (mRect4UpDeactivate.top - mHeaderHeight > mVideoHeight / 3f) {
return currentVideoHolder;
}
}
}
return null;
}
private int findActivateHolderWhenUp(int lastVisiblePos) {
int currentCalcPos = lastVisiblePos + mRecyclerViewHeaderCount;
RecyclerView.ViewHolder holder = mRecyclerView.findViewHolderForAdapterPosition(currentCalcPos);
if (holder != null && holder instanceof VHolderPlayer) {
VHolderPlayer currentVideoHolder = (VHolderPlayer) holder;
currentVideoHolder.itemView.getLocalVisibleRect(mRect4UpActivate);
if (mRect4UpActivate.bottom - mHeaderHeight > mVideoHeight / 3f) {
return currentCalcPos;
}
}
return -1;
}
而onScrollStateChanged就简单的多了,该方法会在滑动结束的时候触发,通过获取在onScrolled里得到位置mPositionToActivate,从RecyclerView中读取需要更新的ViewHolder,并开始播放,更新当前活动状态的key。代码如下:
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
// 滑动停止时根据当前位置激活视频播放
if (newState != mScrollState && newState == RecyclerView.SCROLL_STATE_IDLE && mPositionToActivate >= 0) {
//获取要播放的ViewHolder
RecyclerView.ViewHolder holder = mRecyclerView.findViewHolderForAdapterPosition(mPositionToActivate);
if (holder != null && holder instanceof VHolderPlayer) {
if (mIsAutoPlayEnabled) {
VHolderPlayer video = (VHolderPlayer) holder;
if (video.isNeedPlayNow()) {
//如果需要自动播放,开始播放
video.activate();
//更新key
mCurrentActiveKey = video.getPlayerKey();
}
}
//重置位置
mPositionToActivate = -1;
}
}
//保存状态
mScrollState = newState;
}
以上为整个滑动事件的解析。