自定义views

Exoplayer 音频播放器实现(结合RecyclerView

2018-11-02  本文已影响623人  2878681bca59

前言:

最近应公司项目需要,需要实现一个音频列表播放的功能。Google了一下并没有特别适合的方案,大部分的实现方式都是用自带的Mediaplayer去实现的.Android框架提供了MediaPlayer作为一个快速的解决方案,可以用最少的代码来播放媒体。Android还提供低级别的媒体api框架,如MediaCodec、AudioTrack和MediaDrm,可用于构建自定义媒体播放器解决方案。那么我们当时为什么不直接选择MediaPlayer来进行拓展开发呢?最直接的原因是因为MediaPlayer所支持的音频格式非常有限,想要播放其他格式的视频还要进行相应的转换。这样就无形当中造成了额外的麻烦。后来在和同事一起讨论并且看了许多文章后我们决定使用Exoplayer来实现。

什么是ExoPlayer?

ExoPlayer是google推出的针对Android一个应用级的媒体播放器,针对音频播放和视频播放它提供了多种可选择的Android Media API,用于支持本地或者网络的媒体源。ExoPlayer支持Android MediaPlayer API目前不支持的功能,包括DASH和SmoothStreaming自适应回放。与MediaPlayer API不同,ExoPlayer易于定制和扩展,并可通过Play Store应用程序更新进行更新。

如何使用?

实际上在基本的使用上Exoplayer的git网站上讲解很清楚,我在这里不去做过多的描述,直接简单说一下它的相关组件以及使用。这里我所使用的版本是2.9.0版本,所以在很多类的命名上以及方法函数上发生了一些变化,会与网络上一些其他文章有一些出入。

ExoPlayer库的核心是Player接口。Player暴露了普遍使用的高级媒体播放器api功能,比如缓冲媒体、播放、暂停和拖动条的功能。实现的目的是关于对(并因此加以很少的限制)所播放的媒体类型、存储方式和存储方式、以及如何呈现的方式进行很少的假设。ExoPlayer实现不是直接实现媒体的加载和渲染,而是将这项工作委托给创建播放器或准备播放时注入的组件。

1. 用于定义要播放的媒体的MediaSource,加载media,并从中读取加载的media 。MediaSource在播放开始时通过Player.prepare注入。

2.TrackSelector该类可以对当前的音频视频进行操作,比如设置音轨,设置约束曲目选择,禁用渲染器等。

3.创建媒体连接源,ConcatenatingMediaSource,可以向其加入一个媒体源列表。

准备工作:

将exoplayer添加为依赖项

dependencies {

      implementation 'com.google.android.exoplayer:exoplayer:2.9.0'    

}

确保您的项目根目录中的build.gradle文件中包含JCenter和Google存储库。

repositories {

     jcenter()

    google()

}

支持Java 8

compileOptions {

    sourceCompatibility JavaVersion.VERSION_1_8

    targetCompatibility JavaVersion.VERSION_1_8

}

因为我们在应用中使用了recylcerview所以在开发的过程还要引入一下相关的依赖库。

创建工程并使用

首先我们最终要实现的功能是一个多条音频数据列表的UI,如下图所示

图片01

多条音频源,点击其中一条开始播放,在上下滑动时不影响音频播放,在点击另一条数据时,之前播放的音频停止,并且回退到初始状态。

基于这些需求我们一开始所的出发点是希望可以自定义一个带有player的UI控件,但这一想法很快便被我们的实验给PASS了。我们在创建的demo中创建了多个SimpleExoPlayer的实例,然后在布局文件中引入PlayerView。

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout

    xmlns:android="http://schemas.android.com/apk/res/android"

    xmlns:tools="http://schemas.android.com/tools"

    xmlns:app="http://schemas.android.com/apk/res-auto"

    android:layout_width="match_parent"

    android:layout_height="match_parent"

    tools:context="jiang.jay.exoplayerdemo.MainActivity">

    <com.google.android.exoplayer2.ui.PlayerView

        android:id="@+id/player_view"

        android:layout_width="match_parent"

        android:layout_height="wrap_content"

        app:player_layout_id="@layout/layout_simple_playview"

        app:hide_on_touch="false"

        app:hide_during_ads="false"

        app:show_timeout="1000000000"/>

</LinearLayout>

注意:PlayerView支持UI的自定义,根据设置的player_layout_id可以改写UI的样式,但需要注意的是,所引用的布局文件中的id要与默认布局的id对应,否则会出错。

然后我们在RecyclerView的ViewHolder里面去实现初始化Player,但是在运行后发现,当点击第一天音频源时,音频开始播放,点击第二条时,没有播放第二条音频且第二条的PlayerView也没有任何播放的UI变化。到这里我们遇到了第一个问题,如何实现多个player播放不同的source? 然后我们google了一下发现了一写问题。

ExoPlayer 和 Recyclerview联合使用

为了节省学习成本,我们开始改变策略,使用另一种方案,整个页面中有且仅有1个player,通过改变数据去更新UI。这么一来我们就解决了多个player无法共存的问题。同时我们也应该可以想到如果当一个页面中初始并创建多个player时所带来的开销。

与此同时我发现现有的playerview也没有办法自定义出我们想要实现的UI, 所以我们放弃使用PlayerView,而采取了模仿PlayerView的实现去写一个控制器,通过这个控制器我们来控制我们自己写的UI布局中的控件。

创建AudioController

音频控制器,是我的同事根据PlayerView的源码,模仿得出的一个用于控制音频并且通过它来绘制UI,更新UI的控制器。

AudioController

与RecyclerView相结合

这里我着重说两个问题,第一当我们开始播放某一条数据时,我们需要根据音频进度的变化,UI状态变化去同时去改版对用的数据条目中对应的字段。这里我们创建了一个MediaEntity的实体类

public class MediaEntity {

    public final static int STATUS_PLARY = 1;

    public final static int STATUS_PAUSE = 0;

    public final static int STATUS_END = 2;

    String uri;

    String startTime;

    String endTime;

    boolean playStatus;

    long duration;

    public MediaEntity(String uri, String startTime, String endTime, boolean playStatus,

            long duration) {

        this.uri = uri;

        this.startTime = startTime;

        this.endTime = endTime;

        this.playStatus = playStatus;

        this.duration = duration;

    }

值得注意的是,即使我们不传起始时间、终止时间、时长,AudioController也可以在准备播放后帮我们计算出相应的数据值。这里我们一开始就传入是为了还没有播放时就展示出来,建议在开发的过程中我们的数据源如果有条件的话还是先计算出音频时长再设置,这样就不会出现一开始还没有播放时不现实上述三个值的情况。

第二,我们如何去刷新UI,恐怕大部分的同学会使用notifyItemChanged(int position)这个方法来实现,但是这样会有一个问题就是,当我们的布局中有一些UI不需要实时刷新时,这样造成了不必要的浪费。所以我们采取的是findByView的方式来刷新,通过getViewByPosition(RecyclerView recyclerView, int position, int viewId)的方式扎到对应的控件然后set相应的数值。要注意的是当某个item在刷新时所在的viewHolder中的item view被销毁时,我们这时就只能用notifyItemChanged(int position)来进行UI刷新了,因为此时getViewByPosition获取View为null, 这种情况会发生在当两个先后点击的item不在同一页时。

最后附上完整的代码地址

Demo

因为这一块网上的资料极少,故写了一个Demo方便大家进行参考,希望在开发中遇到同样困扰的朋友可以帮到你们,并希望有看过这篇文章的朋友欢迎大家来交流讨论。

上一篇下一篇

猜你喜欢

热点阅读