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

英文原文: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

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

时间: 2024-10-07 06:32:40

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

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

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

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

WPF 显示文件列表中使用 ListBox 变到ListView 最后使用DataGrid 故事背景: 需要检索某目录下文件,并列出来,提供选择和其他功能. 第一版需求: 列出文件供选择即可,代码如下: <ListBox Name="multiSelectFileLst" ItemsSource="{Binding FileList}" Grid.Row="1" Grid.Column="1" > <List

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

在H5页面中嵌入视频的情况是比较多件的,有时候会碰到需要自动播放的情况,之前根本觉得这不是问题,但是自己的项目中需要视频的时候就有点sb了,达不到老板的要求,那个急呀~~~ 好在之前看过公司前辈的项目,正好用到了视频,并且可以自动播放,爽!!! 赶紧copy过来,记录一下.这里主要监听了canplaythrough事件,然后自己去让视频play(),在这个过程中还发现,ios和安卓不一样,安卓上需要设置muted才能自动播放,ios没这个限制,还有就是有时候视频也可能有问题,导致不能自动播放,之

(更新版)Android VideoPlayer 在滚动列表实现item视频播放(ListView控件和RecyclerView)

由于写这篇文章时挂着梯子 ,回来发现没有图片 对不起各位了-.. 现在我改好了! 2016.5.27 15时 阴 at BJ 转载请标明出处:粪乧 http://blog.csdn.net/wooder111/article/details/51513582 原文翻译: 点击跳转 在这篇文章中,我将介绍如何实现列表中的视频播放.在流行的应用,如Facebook,Instagram的或Magisto的工作原理相同: Facebook的: Magisto的: Instagram的: 这篇文章是基于开

行车记录仪视频无法播放和数据丢失的解决办法

在使用行车记录仪时一些不可控因素,造成视频无法播放或是发生数据丢失的情况该如何处理,本篇文章介绍了对于这两个问题的解决方案! 行车记录仪的数据一般都是存储在SD卡或硬盘中,而一般常见的行车记录仪内存卡的容量都比较小,盖周期也比较短,所以突发事故发生后一定要及时取下内存卡,以免被新的数据覆盖掉. 行车记录仪采用的都是循环覆盖方式来记录文件的,被覆盖的记录本来不易恢复,长时间的录制会使视频循环覆盖多次,底层数据会被重写多次,因此视频被多次覆盖后恢复的几率是非常小的. 行车记录仪中的视频无法播放怎么办

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

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

Android中三种视频的播放

在Android中,我们有三种方式来实现视频的播放: 1.使用其自带的播放器.指定Action为ACTION_VIEW,Data为Uri,Type为其MIME类型. 2.使用VideoView来播放.在布局文件中使用VideoView结合MediaController来实现对其控制. 3.使用MediaPlayer类和SurfaceView来实现,这种方式很灵活. 1.调用其自带的播放器: Uri uri = Uri.parse(Environment.getExternalStorageDir

在wpf中如何让MediaElement的视频循环播放

原文:在wpf中如何让MediaElement的视频循环播放 MediaElement原始的播放是只播放一遍:如何设置让MediaElement播放 的视频或者音频循环播放,解决如下: 修改MediaElement模版 <MediaElement  Name="myMediaElement" Margin="13,35,14,0"  Height="100" VerticalAlignment="Top">    

怎样调整EDIUS中视频的播放速度

今天小编来为大家讲解一下在EDIUS中如何缩短一段视频的时间.说到缩短视频的时间呢,其实上就是让视频快速地播放,这样就会涉及到EDIUS持续时间.时间重映射与冻结帧的问题.下面小编就带领你们一起学习EDIUS 8教程吧! 查看更多内容请直接前往:http://www.ediuschina.com/xinshou/shijian-chixu.html 首先我们把素材拖到时间轨上,然后右击选择“持续时间”,我们在这里修改即可.持续时间分为从左到右分别是:时.分.秒.帧.具体操作见下图: 我们右击素材