这片文章基于开源项目: 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要做的几个步骤:

停止前一个播放。调用MediaPlayer.stop() 方法来完成。
调用MediaPlayer.reset()方法来重设MediaPlayer 。这么做的原因是在滚动列表中,view可能会被重用,我们希望所有的资源都能被释放。
调用MediaPlayer.release() 方法来释放MediaPlayer
清除MediaPlayer的实例。当应该播放新的视频的时候,新的MediaPlayer实例将被创建。
为可视范围最大的view创建MediaPlayer实例。
调用MediaPlayer.setDataSource(String url)来为新的MediaPlayer 设置数据源。
调用MediaPlayer.prepare(),这里没有必要调用异步的MediaPlayer.prepareAsync()。
调用MediaPlayer.start()
等待实际的视频开始。

所有的这些操作都被封装在了在一个独立线程中处理的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中有一个默认的实现:

/**
* 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 = ; currentView.getLocalVisibleRect(mCurrentViewRect); int height = currentView.getHeight(); if(viewIsPartiallyHiddenTop()){
// view is partially hidden behind the top edge
percents = (height - mCurrentViewRect.top) * / height;
} else if(viewIsPartiallyHiddenBottom(height)){
percents = mCurrentViewRect.bottom * / 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() + ,
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

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

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

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

    英文原文:Implementing video playback in a scrolled list (ListView & RecyclerView) 本文将讲解如何在列表中实现视频播放. ...

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

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

  3. android 之 ExpandableListView列表中的列表

    有时候,我们需要设计这样一个界面,外面有一个列表,当我们点击其中列表中的某个条目时,就会展开这个条目,出现一个新的列表.比如下图:(程序运行的效果图,在这里贴出来) 当我们点击第一项时,视图变为: - ...

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

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

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

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

  6. Android 获取对象列表中的某一列 / 所有对象的某一字段,Realm数据库可获取某一字段所有值

    现在项目用的数据库是Realm,所以想要获取数据库中某一字段的数据没有一句直接的语句进行获取,就像MySQL一样的select name from User,从User表里获取所有的name. 所以只 ...

  7. wzplayer for android V1.6.1 (支持音视频加密播放)

    1.更新 2013-11-25: 1.6.1 修复1.6.0版本对rk版本的支持. 以往版本: 1.6.0 1)1.6.0修改了所有默认音频渲染使用AudioTrack输出,这样只要不播放视频,能支持 ...

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

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

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

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

随机推荐

  1. 伸缩--也可用于tabs

    var $ranklist_li = $("div.ranklist_model ul li"); $ranklist_li.hover(function () { $(this) ...

  2. 如何组织CSS?

    前端工程师在开发一个单页面或者小网站的时候有可能不会在意CSS的组织问题,但如果要开发一个中大型的网站,就要好好的组织CSS文件,不然会增加维护成本,整个网站的结构也没条理性. 如何组织CSS?一般常 ...

  3. XML结构,写到TreeView树上

    http://blog.csdn.net/ztzi321/article/details/44077563

  4. CentOS 源设置

    安装完CentOS后,系统默认的源可能有限满,这时我们需要添加国内比较好的源. 一.国内比较好的源https://opsx.alibaba.com/mirror                  #阿 ...

  5. @MapperScan 和 @ComponentScan 区别

    1.首先@MapperScan和@ComponentScan都是扫描包 2.@ComponentScan是组件扫描注解,用来扫描@Controller  @Service  @Repository这类 ...

  6. oracle 正序 逆序 排序查询

    正序:从小到大 order by t.id asc 逆序:从大到小 order by t.id desc

  7. JAVA学习第五十四课 — IO流(八)打印流 &amp; 序列流

    一.综合练习-文件清单列表 获取指定文件夹下,指定扩展名的文件(含子文件夹),并将这些文件的绝对路径写到一个文本文件里.也就是建立一个指定扩展名的文件列表 1.深度遍历 2.过滤器->容器 3. ...

  8. [Android实例] 高速静默更新,低流量耗费,让APP活跃起来!

    大家好.我是csdn的新人,给大家带来一个做了一个星期的SDK,能够实现将Android APP碎片化管理.自由更新,实时更新,低流量耗费的更新~~ Zag Whim Renewal A system ...

  9. 记真实自己,炫精彩人生---《爱记》app使用体验

    真的有款神器吗,能找到合适的Ta,能秀出自己的新生活,能让自己的心情舒爽,有,体验了下.就是爱记.果粉的福利. [爱记]是集心情记录.分享.评价与交流于一体的工具,TA是你心灵休憩的港湾,也是你记忆放 ...

  10. qmake生成Visual Studio工程

    整个Qt在Windows中都可以通过批处理来编译,当编译好Qt后,会生成qmake.exe. 在没有安装Qt-VS-Addin的情况下,如何使用别人提供的Qt *.pro项目文件呢? 使用qmake可 ...