在可滑动列表(ListView & RecyclerView)中实现视频播放

Facebook:

Instagram:

Magisto

这篇文章里的技术是基于VideoPlayerManager这个开源类库的。

类库包含所有的代码以及一个示例。我在这篇文章里会跳过一些东西,所以如果要搞清楚某个细节,那最好还是把源码导入IDE然后再看这篇文章吧。就算你不看源码,这篇文章还是可以让你理解我以何种方式解决了什么问题。

两个问题

要实现目标我们要解决两个问题:

  1. 我们要控制视频播放。在Android系统中我们可以使用MediaPlayer.class操作SurfaceView而且播放视频。但这有很多缺陷。我们不能再list中使用VideoView,因为VideoView继承SurfaceView,而SurfaceView不支持UI同步缓冲(UI synchronization buffer),这导致当滑动list时视频会跟进度。TextureView支持同步缓冲,但没有基于TextureView的VideoView,所以我们需要一个继承TextureView而可以操作Android
    MediaPlayer的View。MediaPlayer中几乎所有的操作(prepare, start, stop等等)基本上都直接调用了操作硬件的底层方法。硬件操作起来比较棘手,尤其是当某个操作耗时16毫秒以上时会进入lagging list(龟速操作表),所以我们需要在后台线程中调用这些方法。
  2. 我们还要知道当用户滑动的时候哪个View处在活动状态。所以我们需要追踪滑动操作并随时操作可见度最高的View。

控制视频播放

现在要提供下述功能:

假设视频正在播放,用户滑动了列表,导致另一个View可见度更高,此时要终止现有的视频播放并播放新的View中的视频。

所以主要的功能就是:停止现有播放,并在停止完成后开始新的播放。

下面是一个样例:当你点击一个视频时,正在播放的视频停止,新的视频开始播放。

VideoPlayerView

首先我们要在TextureView的基础上实现VideoView。我们不能在滑动菜单中直接使用VideoView,因为当用户在视频播放时滑动列表会让视频加载出错。

我将任务分成了几个部分:

  1. 创建了一个ScalableTextureView类,这是TextureView的子类,而且它可以修改它处于的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和Messages Handler Thread

Video Playback Manager和负责调用MediaPlayer的方法的MessagesHandlerThread一起工作。我们需要在子线程中调用诸如prepare(),start()等方法因为它们直接操作底层硬件。有一次我在UI线程中调用的MediaPlayer.reset()方法,但player除了问题,导致主线程阻塞了4分钟。所以我们不用异步的MediaPlayer.prepareAsync,而是使用同步的MediaPlayer.prepare。我们把所有的操作放在子线程中以同步的方式进行。

以下是开启播放新视频的操作流程,需要分几步操作MediaPlayer:

  1. 停止现有播放。这可以通过调用MediaPlayer.stop()方法完成。
  2. 调用MediaPlayer.reset()方法重置MediaPlayer。这是因为在滑动列表中View可能会被复用,所以要尽可能的释放资源。
  3. 调用MediaPlayer.release()方法来释放MediaPlayer。
  4. 清除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()。我们定制Message的原因是我们可以靠这个设置状态(State),因为我们可以知道操作处在哪个阶段(比如Stop阶段)。这可以帮助我们管理哪个Message正在运行以及我们可以做些什么相对应的操作。

/**
* 这个PlayerMessage 调用内部的{@link VideoPlayerView}实例的{@link MediaPlayer#stop()}方法
*/
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的方法就可以。这会将下列Message对象添加至MessagesHandlerThread。

// 暂停队列,并检查现在的状态
// 如果状态是”已开始”就停止现有视频
mPlayerHandler.addMessage(new Stop(mCurrentPlayer, this));
mPlayerHandler.addMessage(new Reset(mCurrentPlayer, this));
mPlayerHandler.addMessage(new Release(mCurrentPlayer, this));
mPlayerHandler.addMessage(new ClearPlayerInstance(mCurrentPlayer, this));

// 设置新的播放视频的View

mPlayerHandler.addMessage(new SetNewViewForPlayback(newVideoPlayerView, this));

// 开始新视频的播放
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)
));
// 队列运行继续

因为Message是同步运行的,所以我们可以随时暂停队列(queue)并运行新的Message。比如:

现在的电影正处在准备阶段(MediaPlayer.prepare()被调用了,MediaPlayer.start()正在队列中等待),而用户滑动了列表,从而需要在新的View中播放新的视频。这样我们只需要:

  1. 暂停现有的队列
  2. 移除所有等待的Message
  3. 添加”停止”,“重置”,“实例”,”清空实例”这四个Message到队列中,当准备操作一返回就执行。
  4. 添加”创建新的实例”,“设置Media Player(这会改变message所操作的Media Player)”,“设置数据源”,“准备”,“开始”到队列中。这些操作会在新的View中开始播放视频。

OK!现在我们有了播放视频的工具,而且它可以:停止现有视频,播放新视频。

下面是在gradle中添加依赖的代码,详见上面的github链接

dependencies {
    compile ‘com.github.danylovolokh:video-player-manager:0.2.0‘
}

识别列表中可见度最高的View

第一个问题是控制视频播放,第二个问题就是追踪可见度最高的View并播放相应视频。

有一个已经实现的实体叫ListItemsVisibilityCalculator而它的实现类SingleListViewItemActiveCalculator可以解决我们的问题。

要使用这个类需要Adapter实现ListItem接口,这样才能计算每个Item的可见度。下面是ListItem的代码:

/**
* ListItem的接口
* 这个接口会被 {@link ListItemsVisibilityCalculator}调用
*
* @author danylo.volokh
*/
public interface ListItem {
    /**
    * 这个方法被调用时, 实现的方法应该返回0-100中的一个可见度百分比
    * @param view 用于计算可见度的View
    * 注意:可见度不一定要依靠一整个View来计算,也可以靠其内部的一个View来计算。
    *
    * @return 可见度百分比
    */
    int getVisibilityPercents(View view);

    /**
    * 当新View的可见度超过了现在的View,新的View会被激活(active),这个方法就被调用
    */
    void setActive(View newActiveView, int newActiveViewPosition);

    /**
    * 还有可能没有一个View处在激活状态,所有的view都要停止活动,这个方法就被调用           */
    void deactivate(View currentView, int position);
}

ListItemsVisibilityCalculator可以追踪滑动的方向并在过程中计算每个Item的可见度。这个可见度可以依靠Item内的任何View进行计算,所以你可以按自己的方式实现getVisibilityPercents()方法。

当然在代码示例中有一个默认的实现。代码如下:

/**
* 这个方法计算现在View的可见度百分比。这个方法仅在View所在屏幕大小小于本来的大小时正常工作。
* @param currentView - 要被计算可见度的View * @return currentView的可见度百分比
*/
@Override
public int getVisibilityPercents(View currentView) {

    int percents = 100;

    currentView.getLocalVisibleRect(mCurrentViewRect);

    int height = currentView.getHeight();

    if(viewIsPartiallyHiddenTop()){
    // View的上部分被掩盖
        percents = (height - mCurrentViewRect.top) * 100 / height;
    } else if(viewIsPartiallyHiddenBottom(height)){
    percents = mCurrentViewRect.bottom * 100 / height;
    }

    return percents;
}

所以,每一个View都应该可以计算它自己的可见度百分比(Visibility Percents)。SingleListViewItemActiveCalculator会在滑动时获取每个View的可见度百分比,所以重写的方法要尽量轻量。

当可见度最高的View旁边的View的可见度超过了这个View,setActive()方法就会被调动,此时要进行视频播放的转换操作。

还有一个类ItemsPositionGetter,它充当ListItemsVisibilityCalculator和列表(ListView, RecyclerView)之间的适配器(Adapter)。这样ListItemsVisibilityCalculator不知道列表到底是什么列表,而只负责本职工作。但它还是需要从ItemsPositionGetter中获取一些信息。以下是ItemsPositionGetter的代码:

/**
* 这个类是提供给{@link ListItemsVisibilityCalculator}的API
* 这个类可以访问RecyclerView和ListView的数据 *
* 对于RecyclerView和ListView有两个不同的实现
* RecyclerView引入了LayoutManager
*
* 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();
}

设计这种结构是为了解耦。下面是演示视频:

使用这个类库只需要如下编写gradle

dependencies {
    compile ‘com.github.danylovolokh:list-visibility-utils:0.2.0‘
}

将VideoPlayerManager和ListVisibilityUtils结合起来实现滑动列表的视频播放

现在我们有两个类库,可以解决一开始提出的两个问题。把这两个结合起来就可以实现我们想要的功能了。

下面是使用RecyclerView的Fragment的代码

  1. 初始化ListItemsVisibilityCalculator,并传入List作为参数。

     /**
     * 只有可见度最高的View处于激活状态(正在播放)
     * 使用{@link SingleListViewItemActiveCalculator}计算View的可见度
     */
     private final ListItemsVisibilityCalculator mVideoVisibilityCalculator = new SingleListViewItemActiveCalculator(new DefaultSingleItemCalculatorCallback(), mList);
    

    DefaultSingleItemCalculatorCallback类仅仅会在可见度最高的View变化时调用ListItem.setActive()方法,你也可以重写这个方法:

     /**
     * 这个回调接口的方法会在发现新的可见度最高的View时,或是没有激活View时调用
     * 后者可能会在用户快速滑动列表时发生,会调用{@link Callback#deactivateCurrentItem(ListItem, View, int)}方法
     */
     public interface Callback<T extends ListItem>{
         void activateNewCurrentItem(T item, View view, int position);
         void deactivateCurrentItem(T item, View view, int position);
     }
    
  2. 初始化VideoPlayerManager
     /**
     * 我们在这里使用{@link SingleVideoPlayerManager},同一时间只有一个视频可以被播放     */
     private final VideoPlayerManager<MetaData> mVideoPlayerManager = new SingleVideoPlayerManager(new PlayerItemChangeListener() {
         @Override
         public void onPlayerItemChanged(MetaData metaData) {
    
         }
     });
    
  3. 设置RecyclerView的onScrollListener并将scroll事件传递给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 RecyclerViewItemsPositionGetter(mLayoutManager, mRecyclerView);
    
  5. 在onResume()中调用方法,使屏幕亮起时启动对View的可见度的计算。
     @Override
     public void onResume() {
         super.onResume();
         if(!mList.isEmpty()){
             // 要在List View Handler中调用这个方法以使list有内容
    
             mRecyclerView.post(new Runnable() {
                 @Override
                 public void run() {
    
                     mVideoVisibilityCalculator.onScrollStateIdle(
                         mItemsPositionGetter,
                         mLayoutManager.findFirstVisibleItemPosition(),
                         mLayoutManager.findLastVisibleItemPosition());
                 }
             });
         }
     }
    

完成了。现在我们就可以在滑动列表中播放视频了:

时间: 2024-11-09 02:44:06

在可滑动列表(ListView & RecyclerView)中实现视频播放的相关文章

在滚动列表中实现视频的播放(ListView &amp; RecyclerView)

英文原文:Implementing video playback in a scrolled list (ListView & RecyclerView) 本文将讲解如何在列表中实现视频播放.类似于诸如 Facebook, Instagram 或者 Magisto这些热门应用的效果: Facebook: Magisto: Instagram: 这片文章基于开源项目: VideoPlayerManager. 所有的代码和示例都在那里.本文将跳过许多东西.因此如果你要真正理解它是如何工作的,最好下载

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

英文原文:Implementing video playback in a scrolled list (ListView & RecyclerView) 本文将讲解如何在列表中实现视频播放.类似于诸如 Facebook, Instagram 或者 Magisto这些热门应用的效果: Facebook: Magisto: Instagram: 这片文章基于开源项目: VideoPlayerManager. 所有的代码和示例都在那里.本文将跳过许多东西.因此如果你要真正理解它是如何工作的,最好下载

视频在滑动列表中的异步缓存和播放

视频在滑动列表中的异步缓存和播放,转自大量高质量游戏应用源码的众筹论坛 http://www.zccode.com/forum.php?mod=viewthread&tid=679&extra= 最近在Github上看到VideoPlayerManager这么一个项目,目的在是ListView和RecyclerView中播放小视频,模仿了Instagram中滑动到可见视频项时开始播放该视频,滑动至不可见时停止视频播放的功能 但是该项目存在几个问题: 快速上下滑动列表后,无法再播放视频,有时

横向滑动的listview效果的实现方法,scrollview嵌套水平滑动的listview卡顿的解决方法

很多时候,界面需要实现横向滑动的listview效果.网络上有一种方法,自定义了HorizontalListView,用法同正常的listview,可实现水平滑动效果. 但是如果一个界面 为垂直滑动的scrollview嵌套水平滑动的listview的时候,滑动水平listview的时候,会很卡.我最近就遇到了这样的问题,一直把思路放在监听水平和垂直滑动手势,想实现滑动角度小于45的时候 垂直的scrollview 滑动效果被禁止.但是一直没有研究出来. 于是一个偶然的机会,灵光一闪,想到用Ho

关于RecyclerView中Viewholder和View的缓存机制的探究

这个题目放在草稿箱里面许久了,一直没有动力提笔.趁现在公司人还没有来齐,工作量还不是很大,就挤出来时间来把它完善了. 我们知道,RecyclerView是经典的ListView的进化与升华,它比ListView更加灵活,但也因此引入了一定的复杂性.最新的v7支持包新添加了RecyclerView. 我们知道,ListView通过使用ViewHolder来提升性能.ViewHolder通过保存item中使用到的控件的引用来减少findViewById的调用,以此使ListView滑动得更加顺畅.但

universal image loader在listview/gridview中滚动时重复加载图片的问题及解决方法

在listview/gridview中使用UIL来display每个item的图片,当图片数量较多需要滑动滚动时会出现卡顿,而且加载过的图片再次上翻后依然会重复加载(显示设置好的加载中图片) 最近在使用UIL遇到了这个问题,相信这个问题许多使用UIL的人都碰到过 现在把解决方法贴出来给有同样问题的朋友做参考 先看下UIL的工作流程 在已经允许内存,存储卡缓存的前提下,当一个图片被请求display时,首先要判断图片是否缓存在内存中,如果false则尝试从存储卡读取,如果依然不存在最后才从网络地址

滑动更改ListView的标题

转载请注明出处:  http://blog.csdn.net/forwardyzk/article/details/42710837 我们平时看到当滑动ListView时,标题的内容会不断的更改,并且标题会有一个推动的效果,下面与大家共享一个示例. 思路: 1.自定义ListView,给ListView绘画一个子标题(childView),将其位置设置为(0,0,width,height) 2.给ListView添加滑动监听事件. 当向下滑动时,当前第一个完全显示的item的标题内容和标题内容进

RecyclerView 中嵌套ViewPager不显示

一个多种布局的RecyclerView中,有的item是图片,有的是文字,竟然还有的要是ViewPager! type可以通过getItemViewType来做,可ViewPager的setAdapter()没有显示 后来看了下文档,RecyclerView的item必须要个高度,如果ViewPager的高度也是通过适配器决定的,那么这个作为item的ViewPager就不能被RecyclerView获得高度 所以,给ViewPager一个固定高度就可以显示了,但是感觉这样很不灵活,其实我Vie

滑动删除ListView的Item的效果

本例子实现了滑动删除ListView的Itemdemo的效果.大家都知道.这种创意是来源于IOS的.左滑删除的功能.在Android上面实现比较麻烦.本例子中不仅实现了左滑删除功能.还实现了左滑赞.左滑分享.左滑收藏等功能.当然大家也可以根据自己项目的需求来修改功能.QQ和微信也实现了相同的功能.大家可以看看.先上程序运行的效果 采用的恶事一个开源库swipemenulistview.jar 代码如下 布局 <com.baoyz.swipemenulistview.SwipeMenuListVi