英文原文:Implementing video playback in a scrolled list (ListView & RecyclerView)

本文将讲解如何在列表中实现视频播放。类似于诸如 Facebook, Instagram 或者 Magisto这些热门应用的效果:

Facebook:

Magisto:

Instagram:

这片文章基于开源项目: 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。那么现在我们需要停止当前视频的播放并开始新的视频。

主要功能就是:停止前一个播放,并仅在旧的播放停止之后才开始新的播放

以下是一个例子:当你按下视频的缩略图-当前播放的视频停止播放,另一个视频开始播放。

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 Thread

Video 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 中是有点乱。但是做一些修改的也许能做到分离。不过虽然现在不怎么好看,但是运行起来还是没有问题。

下面是效果图:

下面是这个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());             }
        });
    }
}

这样我们就得到了一组在列表中播放的视频。

总的来说,这只是对最重要部分的解释。在sample  app中有更多的代码:

https://github.com/danylovolokh/VideoPlayerManager

要了解更多细节请查看源代码。

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

  1. Android 在滚动列表中实现视频的播放(ListView & RecyclerView)

    这片文章基于开源项目: VideoPlayerManager. 所有的代码和示例都在那里.本文将跳过许多东西.因此如果你要真正理解它是如何工作的,最好下载源码,并结合源代码一起阅读本文.但是即便是没有 ...

  2. WPF 显示文件列表中使用 ListBox 变到ListView 最后使用DataGrid

    WPF 显示文件列表中使用 ListBox 变到ListView 最后使用DataGrid 故事背景: 需要检索某目录下文件,并列出来,提供选择和其他功能. 第一版需求: 列出文件供选择即可,代码如下 ...

  3. h5中嵌入视频自动播放的问题

    在H5页面中嵌入视频的情况是比较多件的,有时候会碰到需要自动播放的情况,之前根本觉得这不是问题,但是自己的项目中需要视频的时候就有点sb了,达不到老板的要求,那个急呀~~~ 各种查资料,找到一个方法, ...

  4. Android 中WebView中video视频自动播放

    转载于https://juejin.im/post/5d5ac7eb51882562744fae37 如果有使用过Android的WebView 播放视频的伙伴们一定会发现, 在点开视频网页的时候并没 ...

  5. C#使用EmguCV实现视频读取和播放,及多个视频一起播放的问题

    大家知道WPF中多线程访问UI控件时会提示UI线程的数据不能直接被其他线程访问或者修改,该怎样来做呢? 分下面两种情况 1.WinForm程序 1)第一种方法,使用委托: private delega ...

  6. Android 高级UI设计笔记09:Android如何实现无限滚动列表

    ListView和GridView已经成为原生的Android应用实现中两个最流行的设计模式.目前,这些模式被大量的开发者使用,主要是因为他们是简单而直接的实现,同时他们提供了一个良好,整洁的用户体验 ...

  7. Android 高级UI设计笔记09:Android实现无限滚动列表

    1. 无限滚动列表应用场景: ListView和GridView已经成为原生的Android应用实现中两个最流行的设计模式.目前,这些模式被大量的开发者使用,主要是因为他们是简单而直接的实现,同时他们 ...

  8. web网页中使用vlc插件播放相机rtsp流视频

    可参考: 使用vlc播放器做rtsp服务器 使用vlc播放器播放rtsp视频 使用vlc进行二次开发做自己的播放器 vlc功能还是很强大的,有很多的现成的二次开发接口,不需配置太多即可轻松做客户端播放 ...

  9. 解决ppt中视频不能播放的问题

    小伙伴一直在纠结一个问题,有个ppt,在其他人的电脑上可以正常播放其中的视频,但是在某一个电脑上却总是不能播放,一直没找到原因,俺们今早捯饬了一下,貌似找到一丢丢原因和解决办法了. #1,疑似原因 为 ...

随机推荐

  1. ubuntu下安装花生壳

    下载地址:http://hsk.oray.com/download/#type=linux 官方的文档: 花生壳(公网版) for linux的安装以及使用 Linux花生壳(公网版)将大大简化大家的 ...

  2. WPFの布局中Panel的选用

    一.Canvas 这个容器能够对元素做准确的定位,但同时也是其创建的页面不够灵活. 二.StackPanel 最大的优点是:他会顺序的对他的子元素进行排列显示.(没有任何附加属性) 要注意的是:他有两 ...

  3. IOSanimationDidStop

    -animationDidStop:finished: 方法中的flag参数表明了动画是自然结束还是被打断,我们可以在控制台打印出来.如果你用停止按钮来终止动画,它会打印NO,如果允许它完成,它会打印 ...

  4. MVC2.0==>MVC3.0

    总结出如下4个MVC3.0和2.0的重要区别. 1. @ 符号在 View 页面中的用法: C#代码以 @符号开头,例如 1 <h2>Name: @Model.Name</h2> ...

  5. mysql计划任务

    这两天一直遇见mysql计划任务的案例,今天我就给大家分享一个真是的实例: 1.创建计划任务的语法: create event 任务名称 on schedule  at 时间周期 starts '年- ...

  6. HDU1757 A Simple Math Problem 矩阵快速幂

    A Simple Math Problem Time Limit: 3000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Ot ...

  7. BZOJ 1927 星际竞速(最小费用最大流)

    题目链接:http://61.187.179.132/JudgeOnline/problem.php?id=1927 题意:一个图,n个点.对于给出的每条边 u,v,w,表示u和v中编号小的那个到编号 ...

  8. [HDOJ4609]3-idiots(FFT,计数)

    题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=4609 题意:n个数,问取三个数可以构成三角形的组合数. FFT预处理出两个数的组合情况,然后枚举第三个 ...

  9. ARM处理器启动流程

    根据<<芯片手册>>查看相关内容: 1.启动方式 2.地址布局 3.启动流程

  10. 用@RequestMapping映射请求

    DispatcherServlet接受一个web请求之后,将请求发送给@Controller注解声明的不同控制器类. 这个调度过程依赖控制器类及其处理程序方法中声明的各种@RequestMapping ...