Android在滚动列表中实现视频的播放 ListViewRecyclerView

出门莫恨无人随,书中车马多如簇。这篇文章主要讲述Android在滚动列表中实现视频的播放 ListViewRecyclerView相关的知识,希望能为你提供帮助。
英文原文:Implementing video playback in a scrolled list (ListView & RecyclerView) 
本文将讲解如何在列表中实现视频播放。类似于诸如 Facebook, Instagram 或者 Magisto这些热门应用的效果:
Facebook:

Android在滚动列表中实现视频的播放 ListViewRecyclerView

文章图片

Magisto:
Android在滚动列表中实现视频的播放 ListViewRecyclerView

文章图片

 
Instagram:
Android在滚动列表中实现视频的播放 ListViewRecyclerView

文章图片

这片文章基于开源项目:  VideoPlayerManager。
所有的代码和示例都在那里。本文将跳过许多东西。因此如果你要真正理解它是如何工作的,最好下载源码,并结合源代码一起阅读本文。但是即便是没有看源代码,本文也能帮助你理解我们在干什么。
两个问题要实现我们需要的功能,我们必须解决两个问题:
  1. 我们需要管理视频的播放。在安卓中,我们有一个和SurfaceView 一起工作的MediaPlayer.class 类可以播放视频。但是它有许多缺陷。我们不能在列表中使用普通的VideoView 。VideoView 继承自SurfaceView,而SurfaceView并没有UI同步缓冲区。这就导致了在列表滚动的时候,正在播放的视频需要跟上滚动的步伐。TextureView 中有同步缓冲区,但是在android SDK version 15 中没有基于TextureView 的VideoView。因此我们需要一个继承自TextureView 并和Android MediaPlayer一起工作的View。几乎所有MediaPlayer中的方法(prepare, start, stop 等等… )都调用和硬件相关的本地方法。当做了长于16ms的工作时(必然会),硬件会非常棘手然后我们就会看到一个卡顿的列表。这就是为什么我们需要从后台线程调用它们。
  2. 我们还需要知道滚动列表中的哪个View当前处于活动状态以切换播放的视频。所以我们需要跟踪滚动并定义可视范围最大的view。
管理视频播放我们的目标是提供以下功能:
假设视频正在播放。用户滚动列表,一个新的item替代正在播放的item成为可视范围最大的view。那么现在我们需要停止当前视频的播放并开始新的视频。
主要功能就是:停止前一个播放,并仅在旧的播放停止之后才开始新的播放。
【Android在滚动列表中实现视频的播放 ListViewRecyclerView】以下是一个例子:当你按下视频的缩略图-当前播放的视频停止播放,另一个视频开始播放。
 
Android在滚动列表中实现视频的播放 ListViewRecyclerView

文章图片

VideoPlayerView我们要做的第一件事就是实现基于TextureView的VideoView 。我们不能在滚动列表中使用VideoView 。这是因为如果在播放的过程中用户滚动了列表,视频的渲染会混乱。
我将把这个任务分为几部分:
1.创建一个ScalableTextureView,它是TextureView 的子类,同时它还知道如何调整SurfaceTexture (视频的播放就是运行在SurfaceTexture 上),并提供几个类似于ImageView scaleType的选项。
public  enum  ScaleType  {         CENTER_CROP,  TOP,  BOTTOM,  FILL }

2.创建一个VideoPlayerView,它是ScalableTextureView 的子类,含有跟MediaPlayer.class相关的所有功能。这个自定义view封装了MediaPlayer.class并提供了和VideoView十分类似的API。它具有MediaPlayer的所有方法:setDataSource, prepare, start, stop, pause, reset, release。
Video Player Manager and Messages Handler ThreadVideo Playback Manager和 MessagesHandlerThread 一起工作,负责调用MediaPlayer的方法。我们需要在单独的线程中调用例如prepare(), start()等这样的方法是因为它们直接和设备的硬件关联。我们也做过在UI线程中调用MediaPlayer.reset(),但是player出了问题,而且这个方法对UI线程的阻塞几乎有4分钟!这就是为什么我们不必使用异步的MediaPlayer.prepareAsync,而使用同步的MediaPlayer.prepare。我们让每件事情都在一个单独的线程里做。
至于开始一个新的播放的流程,这里是MediaPlayer要做的几个步骤:
  1. 停止前一个播放。调用MediaPlayer.stop() 方法来完成。
  2. 调用MediaPlayer.reset()方法来重设MediaPlayer 。这么做的原因是在滚动列表中,view可能会被重用,我们希望所有的资源都能被释放。
  3. 调用MediaPlayer.release() 方法来释放MediaPlayer
  4. 清除MediaPlayer的实例。当应该播放新的视频的时候,新的MediaPlayer实例将被创建。
  5. 为可视范围最大的view创建MediaPlayer实例。
  6. 调用MediaPlayer.setDataSource(String url)来为新的MediaPlayer 设置数据源。
  7. 调用MediaPlayer.prepare(),这里没有必要调用异步的MediaPlayer.prepareAsync()。
  8. 调用MediaPlayer.start()
  9. 等待实际的视频开始。
所有的这些操作都被封装在了在一个独立线程中处理的Message里面,假如这是Stop message,将调用VideoPlayerView.stop(),而它最终调用的是MediaPlayer.stop()。我们需要自定义的messages是因为这样我们就能设置当前状态。我们可以知道它是正在停止还是已经停止或者其它状态。它帮助我们控制当前处理的是什么message,如果需要,我们可以对它做点什么,比如,开始新的播放。
/**   *  This  PlayerMessage  calls  {@link  MediaPlayer#stop()}  on  the  instance  that  is  used  inside  {@link  VideoPlayerView}   */ public  class  Stop  extends  PlayerMessage  {         public  Stop(VideoPlayerView  videoView,  VideoPlayerManagerCallback  callback)  {                 super(videoView,  callback);         }        @Override         protected  void  performAction(VideoPlayerView  currentPlayer)  {                 currentPlayer.stop();         }        @Override         protected  PlayerMessageState  stateBefore()  {                 return  PlayerMessageState.STOPPING;         }        @Override         protected  PlayerMessageState  stateAfter()  {                 return  PlayerMessageState.STOPPED;         } }

如果我们需要开始一个新的播放,我们只需调用VideoPlayerManager中的一个方法。它向MessagesHandlerThread中添加了如下消息组合。
//  pause  the  queue  processing  and  check  current  state //  if  current  state  is  "started"  then  stop  old  playback mPlayerHandler.addMessage(new  Stop(mCurrentPlayer,  this)); mPlayerHandler.addMessage(new  Reset(mCurrentPlayer,  this)); mPlayerHandler.addMessage(new  Release(mCurrentPlayer,  this)); mPlayerHandler.addMessage(new  ClearPlayerInstance(mCurrentPlayer,  this)); //  set  new  video  player  view mPlayerHandler.addMessage(new  SetNewViewForPlayback(newVideoPlayerView,  this)); //  start  new  playback mPlayerHandler.addMessages(Arrays.asList(                 new  CreateNewPlayerInstance(videoPlayerView,  this),                 new  SetAssetsDataSourceMessage(videoPlayerView,  assetFileDescriptor,  this),  //  I  use  local  file  for  demo                 new  Prepare(videoPlayerView,  this),                 new  Start(videoPlayerView,  this) )); //  resume  queue  processing

消息的运行是同步的,因此我们可以在任意时刻暂停队列的处理,比如:
当前的视频处于准备状态(MedaiPlayer.prepare()被调用, MediaPlayer.start() 在队列中等待) ,用户滚动别表因此我们需要在一个新的view上开始播放视频。在这种情况下,我们:
  1. 暂停队列的处理
  2. 移除所有挂起的消息
  3. 把“ Stop” , “ Reset” , “ Release” , “ Clear Player instance” 发送到队列。它们将在我们从“ Prepare” 返回的时候立即被调用。
  4. 发送 “ Create new Media Player instance” , “ Set Current Media Player” (这个消息改变执行messages的MediaPlayer对象), “ Set data source” , “ Prepare” , “ Start” 消息。这些消息将在新的view上开始视频的播放。
好了,这样我们就有了按照我们需求运行视频播放的工具:停止前一个播放然后显示下一个。
这里是library的gradle 依赖:
dependencies  {         compile  ‘com.github.danylovolokh:video-player-manager:0.2.0‘ }

识别list中可见范围最大的view.List Visibility Utils第一个问题是管理视频的播放问题。第二个问题则是跟踪哪个item的可见范围最大并把播放切换到那个view。
这里有一个名叫ListItemsVisibilityCalculator 的接口和它的实现SingleListViewItemActiveCalculator 就是做这个工作的。
为了计算列表中item的可见度,adapter中使用的model class必须实现ListItem interface 。
/**   *  A  general  interface  for  list  items.   *  This  interface  is  used  by  {@link  ListItemsVisibilityCalculator}   *   *  @author  danylo.volokh   */ public  interface  ListItem  {         /**           *  When  this  method  is  called,  the  implementation  should  provide  a           *  visibility  percents  in  range  0  -  100  %           *  @param  view  the  view  which  visibility  percent  should  be           *  calculated.           *  Note:  visibility  doesn‘t  have  to  depend  on  the  visibility  of  a           *  full  view.            *  It  might  be  calculated  by  calculating  the  visibility  of  any           *  inner  View           *           *  @return  percents  of  visibility           */         int  getVisibilityPercents(View  view);         /**           *  When  view  visibility  become  bigger  than  "current  active"  view           *  visibility  then  the  new  view  becomes  active.           *  This  method  is  called           */         void  setActive(View  newActiveView,  int  newActiveViewPosition);         /**           *  There  might  be  a  case  when  not  only  new  view  becomes  active,           *  but  also  when  no  view  is  active.           *  When  view  should  stop  being  active  this  method  is  called           */         void  deactivate(View  currentView,  int  position); }

ListItemsVisibilityCalculator 跟踪滚动的方向并在运行时计算item的可视度。item的可见度可能取决于列表中单个item里面的任意view。由你来实现getVisibilityPercents() 方法。
在sample demo app中有一个默认的实现:
/**   *  This  method  calculates  visibility  percentage  of  currentView.   *  This  method  works  correctly  when  currentView  is  smaller  then  it‘s  enclosure.   *  @param  currentView  -  view  which  visibility  should  be  calculated   *  @return  currentView  visibility  percents   */ @Override public  int  getVisibilityPercents(View  currentView)  {        int  percents  =  100;         currentView.getLocalVisibleRect(mCurrentViewRect);         int  height  =  currentView.getHeight();         if(viewIsPartiallyHiddenTop()){                 //  view  is  partially  hidden  behind  the  top  edge         percents  =  (height  -  mCurrentViewRect.top)  *  100  /  height;         }  else  if(viewIsPartiallyHiddenBottom(height)){                 percents  =  mCurrentViewRect.bottom  *  100  /  height;         }        return  percents; }

每个 view都需要知道如何计算它的可见百分比。滚动发生的时候,SingleListViewItemActiveCalculator将从每个view 索取这个值,所有这里的实现不能太复杂。
当某个邻居的可见度超过了当前活动item,setActive 方法将被调用。就在这时应该切换播放。
还有一个作为ListItemsVisibilityCalculator 和 ListView 或者 RecyclerView之间适配器的ItemsPositionGetter。这样ListItemsVisibilityCalculator 就不需要知道这到底是一个ListView 还是RecyclerView。它只是做自己的工作。但是它需要知道一些ItemsPositionGetter提供的信息:
/**   *  This  class  is  an  API  for  {@link  ListItemsVisibilityCalculator}   *  Using  this  class  is  can  access  all  the  data  from  RecyclerView  /    *  ListView   *   *  There  is  two  different  implementations  for  ListView  and  for    *  RecyclerView.   *  RecyclerView  introduced  LayoutManager  that‘s  why  some  of  data  moved   *  there   *   *  Created  by  danylo.volokh  on  9/20/2015.   */ public  interface  ItemsPositionGetter  {         View  getChildAt(int  position);         int  indexOfChild(View  view);         int  getChildCount();         int  getLastVisiblePosition();         int  getFirstVisiblePosition(); }

考虑到业务逻辑和model分离的原则,把那样的逻辑放在model 中是有点乱。但是做一些修改的也许能做到分离。不过虽然现在不怎么好看,但是运行起来还是没有问题。
下面是效果图:
Android在滚动列表中实现视频的播放 ListViewRecyclerView

文章图片

下面是这个library的 gradle dependency:
dependencies  {         compile  ‘com.github.danylovolokh:list-visibility-utils:0.2.0‘ }

Combination of Video Player Manager and List Visibility Utils to implement video playback in the scrolling list.现在我们已经有了两个能解决我们所有问题的library。让我们把它们结合起来实现我们需要的功能。
这里是取自使用了RecyclerView的fragment 中的代码:
1.初始化ListItemsVisibilityCalculator,并传递一个list的引用给它。
/**   *  Only  the  one  (most  visible)  view  should  be  active  (and  playing).   *  To  calculate  visibility  of  views  we  use  {@link  SingleListViewItemActiveCalculator}   */ private  final  ListItemsVisibilityCalculator  mVideoVisibilityCalculator  =  new  SingleListViewItemActiveCalculator( new  DefaultSingleItemCalculatorCallback(),  mList);

DefaultSingleItemCalculatorCallback 只是在活动view改变的时候调用了 ListItem.setActive 方法,但是你可以自己重写它,做自己想做的事情:
/**   *  Methods  of  this  callback  will  be  called  when  new  active  item  is  found  {@link  Callback#activateNewCurrentItem(ListItem,  View,  int)}   *  or  when  there  is  no  active  item  {@link  Callback#deactivateCurrentItem(ListItem,  View,  int)}  -  this  might  happen  when  user  scrolls  really  fast   */ public  interface  Callback< T  extends  ListItem> {         void  activateNewCurrentItem(T  item,  View  view,  int  position);         void  deactivateCurrentItem(T  item,  View  view,  int  position); }

2. 初始化VideoPlayerManager。
/**   *  Here  we  use  {@link  SingleVideoPlayerManager},  which  means  that  only  one  video  playback  is  possible.   */ private  final  VideoPlayerManager< MetaData>   mVideoPlayerManager  =  new  SingleVideoPlayerManager(new  PlayerItemChangeListener()  {         @Override         public  void  onPlayerItemChanged(MetaData  metaData)  {        } });

3. 为RecyclerView设置on scroll listener 并传递scroll events 到 list visibility utils。
@Override public  void  onScrollStateChanged(RecyclerView  view,  int  scrollState)  {   mScrollState  =  scrollState;   if(scrollState  ==  RecyclerView.SCROLL_STATE_IDLE  & &   mList.isEmpty()){  mVideoVisibilityCalculator.onScrollStateIdle(                     mItemsPositionGetter,                     mLayoutManager.findFirstVisibleItemPosition(),                     mLayoutManager.findLastVisibleItemPosition());   }   }@Override public  void  onScrolled(RecyclerView  recyclerView,  int  dx,  int  dy)  {   if(!mList.isEmpty()){       mVideoVisibilityCalculator.onScroll(                   mItemsPositionGetter,                   mLayoutManager.findFirstVisibleItemPosition(),                   mLayoutManager.findLastVisibleItemPosition()  -                   mLayoutManager.findFirstVisibleItemPosition()  +  1,                   mScrollState);   } } });

4. 创建ItemsPositionGetter。
ItemsPositionGetter  mItemsPositionGetter  =  new  RecyclerViewItemPositionGetter(mLayoutManager,  mRecyclerView);

5.同时我们在onResume 中调用一个方法以便在我们打开屏幕的时候马上开始计算可见范围最大的item。
@Override public  void  onResume()  {         super.onResume();         if(!mList.isEmpty()){                 //  need  to  call  this  method  from  list  view  handler  in  order  to  have  filled  list                mRecyclerView.post(new  Runnable()  {                         @Override                         public  void  run()  {                                mVideoVisibilityCalculator.onScrollStateIdle(                                                 mItemsPositionGetter,                                                 mLayoutManager.findFirstVisibleItemPosition(),                                                 mLayoutManager.findLastVisibleItemPosition());                         }                 });         } }

这样我们就得到了一组在列表中播放的视频。
 
Android在滚动列表中实现视频的播放 ListViewRecyclerView

文章图片

总的来说,这只是对最重要部分的解释。在sample   app中有更多的代码:
https://github.com/danylovolokh/VideoPlayerManager
要了解更多细节请查看源代码。
Cheers  ; )
  再分享一下我老师大神的人工智能教程吧。零基础!通俗易懂!风趣幽默!还带黄段子!希望你也加入到我们人工智能的队伍中来!http://www.captainbed.net

    推荐阅读