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

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

由于写这篇文章时挂着* ,回来发现没有图片 对不起各位了….. 现在我改好了!

2016.5.27 15时 阴 at BJ


转载请标明出处:粪乧 http://blog.csdn.net/wooder111/article/details/51513582

原文翻译: 点击跳转

在这篇文章中,我将介绍如何实现列表中的视频播放。在流行的应用,如Facebook,Instagram的或Magisto的工作原理相同:

Facebook的:
(更新版)Android VideoPlayer 在滚动列表实现item视频播放(ListView控件跟RecyclerView)
Magisto的:

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

Instagram的:

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

这篇文章是基于开源项目:VideoPlayerManager。

所有代码和工作示例是存在的。在这篇文章中的很多东西会被跳过,因此,如果有人真的需要理解它是如何工作最好是下载源代码和阅读在你的IDE的源代码的文章。但即使没有代码这篇文章将有利于与我们正在处理此理解。

两个问题

为了实现所需要的,我们必须解决两个问题:

  1. 我们要管理的视频播放。在Android中,我们有一个类MediaPlayer.class与SurfaceView协同工作,并且可以播放视频。但它也有很多缺点。我们不能在列表中使用通常的VideoView。VideoView延伸SurfaceView和SurfaceView不具有的UI同步缓冲器。所有这一切将导致我们到视频正在播放尝试,当你滚动它赶上名单的情况。同步缓存存在于TextureView但没有VideoView是基于TextureView在Android的SDK版本15. 因此,我们需要扩展TextureView并与Android的MediaPlayer工作的看法。另外,几乎所有的方法,从(准备,启动,停止等…) MediaPlayer的基本上都是调用与硬件配合本地方法。硬件可以是棘手的,如果将做任何工作时间超过16毫秒(它肯定会),那么我们将看到一个滞后的列表。这就是为什么需要从后台线程调用它们。

  2. 我们还需要知道哪些鉴于滚动名单上的当前活动,可以切换回放时,用户滚动。所以基本上我们必须跟踪滚动,并确定最引人注目的观点,如果它的变化。

管理视频播放

在这里,我们的目标是提供以下功能:

假设电影播放上。用户滚动列表中的列表和新产品变得比该视频播放所述一个更为明显。所以,现在我们必须停止现有的视频播放,并开始新的。
其主要功能是:停止播放以前,并开始播放新老一停之后。

这里是它如何工作的视频采样:当您按视频缩略图 - 当前视频播放停止,另一个开始。

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

VideoPlayerView

我们需要实现的第一件事情是一个基于TextureView的VideoView。我们不能在滚动列表中使用VideoView。因为视频渲染会搞砸了,如果用户在播放过程中滚动我们的名单

我分这个任务分为几个部分:

  1. 创建一个ScalableTextureView。它是TextureView的后代,并且知道如何调整表面纹理(在重放运行该表面纹理),并提供了类似的ImageView scaleType几个选项。

    public enum ScaleType {
    CENTER_CROP, TOP, BOTTOM, FILL
    }
  2. 创建VideoPlayerView。这是ScalableTextureView的后裔,它包含了所有与MediaPlayer.class的功能。这个自定义视图封装MediaPlayer.class,并提供了非常相似的VideoView的API。它具有所有被直接调用MediaPlayer的方法:的setDataSource,准备,启动,停止,暂停,恢复,释放。

视频播放器管理器和消息处理线程

视频播放经理与负责调用的MediaPlayer的方法MessagesHandlerThread一起工作。我们需要调用的方法类似制备(),启动()等在一个单独的线程,因为它们被直接连接到设备的硬件。并有情况下,当我们叫MediaPlayer.reset()在UI线程,但出现了一些问题的球员,这种方法是阻塞UI线程近4分钟!这就是为什么我们没有使用异步MediaPlayer.prepareAsync,我们可以使用同步MediaPlayer.prepare。我们是在一个单独的线程同步无所不为。

与启动新的播放流。下面是几个步骤做的MediaPlayer:

  1. 以前停止播放。它是通过调用MediaPlayer.stop()方法来实现。
  2. 通过调用MediaPlayer.reset()方法重置的MediaPlayer。我们需要做的是因为在滚动列表视图可能会被重用,我们希望有发布的所有资源。
  3. 通过调用MediaPlayer.release()方法释放的MediaPlayer。
  4. 清除的MediaPlayer的实例。当这种观点新的播放应该开始新的MediaPlayer实例将被创建。
  5. 新的最明显的视图创建实例上的MediaPlayer。
  6. 通过调用MediaPlayer.setDataSource(字符串URL)设置新的MediaPlayer的数据源。
  7. 呼叫MediaPlayer.prepare()。没有必要使用异步MediaPlayer.prepareAsync()
  8. 呼叫MediaPlayer.start()
  9. 等待实际回放启动。

所有这些行动被包装成在一个单独的线程处理的消息,例如这是停止的消息。它调用VideoPlayerView.stop(),最终调用MediaPlayer.stop()。我们需要自定义消息,因为我们可以设置当前状态。我们知道它是停止或已停止否则后果不堪设想。它可以帮助我们来控制哪些消息是现在进行时,我们可以做些什么,如果我们需要,例如,开始新的播放内容。

/**
 * 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()在队列中等待)和用户滚动列表,所以我们需要在一个新的视图开始播放。在这种情况下我们:

  1. 暂停队列处理
  2. 删除所有未决信息
  3. 邮报“停止”,“复位”,“释放”,“清除Player实例”队列。当我们从“准备”回报,他们将运行权
  4. 邮报“创建新的媒体播放器实例”,“设置当前媒体播放器”(这个变化在其上执行了邮件的MediaPlayer对象),“设置数据源”,“准备”,“开始”。而这一消息将开始新视图播放。

好了,我们必须运行在我们需要的方式播放公用事业:停止播放以前,只是再开始下一个。

下面是该库的依赖关系的gradle:

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

标识列表中的最明显的图。清单能见度utils的

第一个问题是管理视频播放。第二个问题是跟踪哪些观点是最明显和回放切换到这一观点。

有一个叫ListItemsVisibilityCalculator及其实施SingleListViewItemActiveCalculator做所有作业的实体。

即在适配器使用必须实现的ListItem接口,以便计算列表中的项目的知名度模型类:

/**
 * 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跟踪滚动的方向,并计算在运行项目的可见性。该项目的知名度可能取决于列表中的单个项目内部的任何视图。这是由你来实现getVisibilityPercents()方法。
有样品演示应用该方法的默认实现:

/**
 * 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;
}

因此,每个视图需要知道如何计算其知名度百分比。SingleListViewItemActiveCalculator将投票从滚动时发生这样的实现不应该是非常沉重的每个视图此值。
当任何邻居项目的知名度超过了当前活动项目的知名度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();
}

有那种逻辑的模型是搞乱了一下从模型中分离业务逻辑的想法。但有一些修改它可能被分离。顺便说一句,它工作正常,现在连怎么回事。
下面是简单的电影,显示它是如何工作:
(更新版)Android VideoPlayer 在滚动列表实现item视频播放(ListView控件跟RecyclerView)

下面是一个utils的Gradle依赖关系:

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

视频播放器管理器和目录能见度utils的组合来实现滚动列表的视频播放。

现在我们有解决我们所需要的一切两个库。让我们将它们结合起来,以获得我们所需要的功能。
在这里,从使用RecyclerView片段的代码:

  1. 初始化ListItemsVisibilityCalculator,并传递一个参考到一个列表吧。

    /**
    * 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只是调用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并通过滚动的事件列表中的知名度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的方法来启动,只要我们打开屏幕计算最明显的项目。

    @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 VideoPlayer 在滚动列表实现item视频播放(ListView控件跟RecyclerView)

基本上,这是最重要的部分仅仅是解释。有一个在这里的示例应用程序有更多的代码:
https://github.com/danylovolokh/VideoPlayerManager

请看到源代码的更多细节。谢谢!

1楼wooder111前天 15:29
抱歉早上挂* 写的 下午发现图片有墙 现在改好了... 希望更直观点.