分类目录归档:视频播放器

视频播放器 后记

后记

一个简单但是功能比较完备的视频播放器已经开发完成了。

最后,可以在菜单栏中增加一个“关于”功能,通过它启动一个介绍这个应用的说明Activity。我们在这里就简单的展示下开发者信息和应用版本信息。

通过这一章节的学习,我们对安卓系统又有了进一步的认识,应该具备了开发一个简单安卓应用的能力。

安卓开发之路一步一步走到现在,我们开始渐入佳境。有了这前面两个篇章的铺垫,相信大家已经算是对安卓开发入门了。

对于本章开篇中提到的那些“遗憾”之处,也许就是我们下一个篇章深入开发的起点。

视频播放器 第9节 开放视频播放功能

第9节 开放视频播放功能

安卓系统的开发性不只是源码的开放,各个应用之间也是可以互相利用的。比如,我们在图片浏览器里面浏览照片,发现好的,可以通过分享按钮,通过另外一个应用-“微信”,把照片分享到“朋友圈”。这里的“微信”,就是可以被别的应用利用的工具。

类似的,我们也可以把这个视频播放器,做成这种可以被别的程序使用的工具。例如当用户使用文件浏览器浏览磁盘上的文件时,发现一个视频文件,点击后,系统可以提示用户用我们的播放器来播放。

要实现这样的修改非常的容易,只需要修改AndroidManifest.xml中注册的播放器Activity属性,

  1. VideoPlayer的过滤器添加action标签,值设置为android.intent.action.View
  2. VideoPlayer的过滤器添加data标签,让VideoPlayer关注video类型的数据;

这样,如果别的任何应用使用android.intent.action.View来试图打开一个视频文件,我们的VideoPlayer就能满足这样的要求,就会被系统作为推荐打开方式,提供给用户选择。

视频播放器 第8节 横屏的播放界面

第8节 横屏的播放界面

在设备旋转成横屏的时候,视频将自动进行全屏播放。

8.1 播放器横屏布局

我们要为全屏播放界面设置一个新的布局,这个布局里面只用来播放视频,不需要显示任何视频信息,

8.2 全屏的设置

因为横竖屏的VideoPlayer都是一套代码实现的,所以需要判断当前Activity是横屏还是竖屏,

VideoPlayer创建的时候,我们要对这个Activity进行全屏的设置,

因为要使应用全屏,所以需要修改Activity所属的窗口-Window的属性。以此告诉系统需要隐藏状态栏和导航栏。

还需要隐藏ActionBar

8.3 横竖屏转换的状态保存

在旋转的过程中,Activity将经历这些的生命周期:
用户可以与Activity交互,此时屏幕进行旋转,从竖屏变横屏->
onSaveInstanceState()->
onPause()->
onStop()->
onDestroy()->
onCreate()->
onStart()->
onRestoreInstanceState()->
onResume()->
用户可以与Activity交互;

可见,这个Activity要被重新创建一次,所以当视频正在播放的时候,我们要保存好视频当前播放的位置,Activity重建以后才能从之前播放到的位置继续播放。

我们要在onSaveInstanceState()里面保存当前播放的位置,

onRestoreInstanceState()里面取出打断播放时的位置,并存储到mLastPlayedTime里面,当Activity在onResume()的时候,就能够跳转到播放点开始播放了。

视频播放器 第7节 竖屏的播放界面

第7节 竖屏的播放界面

播放视频的功能放在一个单独的Activity当中。我们将为它们设置横竖屏两种布局。

  1. 在竖屏的时候,上半部分播放视频,下半部分显示视频信息;

  2. 在设备旋转成横屏的时候,视频进行全屏播放;

7.1 启动视频播放界面

当点击视频列表的视频项时,就启动播放器播放对应的视频。这里我们要创建一个名字叫做VideoPlayer的Activity,用它来完成视频播放的任务。

另外,还要为ListView添加一个数据项点击时的监听函数,

  1. 实现ListViewOnItemClickListener接口;
  2. 把要播放视频地址的URI放入Intent
  3. 通过Intent,启动视频播放器的Activity-VideoPlayer;

这里使用的Intent是安卓系统当中连接各个组件之间的桥梁,它可以,

  1. 唤醒指定的组件,让它开始运行。例如在Activity A启动Activity B,或者启动一个Service A;
  2. 向各个组件传递数据;

7.2 VideoView

播放视频可以使用Android SDK提供的现成的控件VideoView。它是对media playersurface的封装,对于初次进行视频播放开发的我们,使用VideoView是最简单和方便的,不用关注太多细节上的实现方式。

VideoView的使用,非常简单,

  1. 在布局文件中放置一个VideoView控件,

  2. 在Activity当中,获取布局文件中的VideoView,让后设置要播放的视频地址,

  3. 使用VideoView提供的接口,控制视频播放的流程,

  4. 还可以为VideoView添加控制面板-MediaController,这个面板集成了播放进度拖动、暂停、继续播放等功能,还可以自动隐藏或显示。如果VideoView有父布局,那么为它添加的MediaController是附着在父布局的底部的。因此为了界面美观,我们在布局文件中,将VideoView单独放到一个FrameLayout当中。

    这里我们使用Android SDK自带的Media Controller

7.3 竖屏布局

我们将竖屏的布局设计成上下两个部分,上面用VideoView播放视频,下面用一个TableLayout展示视频信息,

这里把VideoView当到了FrameLayout当中,让播放的内容居中显示,并为这个区域设置了背景颜色。

7.4 设置视频信息

  1. 通过启动VideoPlayerIntent,获取视频播放的地址,

  2. 通过播放地址,查询该视频相关的其他信息-视频标题、视频文件大小、视频尺寸、视频创建时间,

7.5 设置播放

获取VideoView控件,并为VideoView设置播放的视频。为播放界面添加了Android SDK自带的控制面板-MediaController

7.6 视频的暂停与恢复

有的时候,用户需要暂停播放的视频,可以通过控制面板上的按钮操作;但有的时候,正在播放的视频会被系统打断,例如来了一个电话,电话Activity优先级最高,会显示出来,让视频播放器暂停播放;或者用户在播放的时候,按下了Home键,切换到了主屏幕,也需要暂停播放视频。当这些操作结束以后,再次把VideoPlayer切换到前台的时候,我们会希望视频继续从刚才被打断的地方继续播放。

我们已经了解了Activity的生命周期,
从一个Activity创建出来,到显示,再到用户主动退出销毁这个Activity,它将经历:
onCreate()->
onStart()->
onResume()->
用户可以与Activity交互->
onPause()->
onStop()->
onDestroy()
那么就可以选取其中的onResume()onPause(),进行视频播放暂停和继续播放的操作。

onPause()中,暂停播放的视频,记录下当前播放的位置;在onResume()中,从上次记录下中断的位置开始播放,

视频播放器 第6节 视频列表的横屏

第6节 视频列表的横屏

设备在横放或者竖放的时候都会自动调整应用的布局,进行对应的横屏显示或者竖屏显示。我们的视频列表也是如此。

我们可以用两种方法处理设备旋转的问题,

  1. 让Activity不跟随设备方向的旋转而旋转,它只有竖屏(或只有横屏)的界面。要做到这一点很容易,在AndroidManifest.xml文件中,给这个Activity组件加上android:screenOrientation="portrait"
    (保持竖屏)或者android:screenOrientation="landscape"
    (保持横屏)的属性就可以了;

  2. 让Activity跟随设备方向的旋转而旋转。

我们准备采用第二种处理方式。

6.1 Activity周期切换

屏幕方向变化时,Activity的生命周期也将发生变化。

从一个Activity创建出来,到显示,再到用户主动退出销毁这个Activity,它将经历:
onCreate()->
onStart()->
onResume()->
用户可以与Activity交互->
onPause()->
onStop()->
onDestroy()

如果一个Activity从创建出来,到显示,然后旋转,那么它将经历:
onCreate()->
onStart()->
onResume()->
用户可以与Activity交互,此时屏幕进行旋转,从竖屏变横屏->
onSaveInstanceState()->
onPause()->
onStop()->
onDestroy()->
onCreate()->
onStart()->
onRestoreInstanceState()->
onResume()->
用户可以与Activity交互;

可以看到,旋转的时候onCreate()函数会被再次调用。在这里,如果拥有横屏布局文件,onCreate()中的setContentView()将会使用横屏的布局,如果没有,依然使用默认的布局文件。

6.2 屏幕旋转产生的问题

如果视频列表从竖屏变成横屏,那么会先执行onDestroy(),再进行一次onCreate()创建的过程。我们在界面上就会看到,一个已经展现了所有视频信息的列表,在旋转之后,又要重新开始查询一次。

这显然没有必要。因此我们需要对视频列表界面,做一点修改,让它不要每次旋转就去完全刷新。

AndroidManifest.xml文件中,给这个Activity组件加上android:screenOrientation="orientation|screenSize"的属性就可以了;

这种Activity从创建出来,到显示,然后旋转,那么它将经历:
onCreate()->
onStart()->
onResume()->
用户可以与Activity交互,此时屏幕进行旋转,从竖屏变横屏->
onConfigurationChanged();

如此一来,这个Activity在旋转时就不会重走销毁、创建的过程了,而只是在旋转后经历一个onConfigurationChanged()。这种情况下,如果存在它对应的横屏布局文件,那么这个布局也不会被使用到,因为onCreate()并没有被调用到。

视频播放器 第5节 刷新与停止刷新列表

第5节 刷新与停止刷新列表

虽然经过我们的假设,忽略了很多不需要关注的视频文件,但设备上依然有可能有很多的满足了我们假设条件的视频存在,这时就需要一个“取消刷新”的功能。

如果视频还没有刷新完,就被取消了,然后又希望继续刷新,那么还需要一个手动开始“刷新”的功能。

因此,准备在ActionBar的右上角,设置一个菜单项,让用户可以“刷新”,也能“停止刷新”。

5.1 添加刷新菜单项

在制作“计算器”的文档里面,我们已经介绍了如何添加ActionBar菜单项,以及如何响应菜单项的点击,

定义菜单项到res\xml\menu.xml文件中,app:showAsAction属性设置成walways

5.2 实现“刷新”与“停止刷新”的功能

  1. onCreateOptionsMenu()函数中,使用定义的菜单,获取“刷新”功能的菜单项,根据当前VideoUpdateTask的状态,来确定要显示的菜单名字,

  2. onOptionsItemSelected()中,根据当前VideoUpdateTask的状态,来确定如何响应用户的点击操作,

  3. VideoUpdateTask被成功取消工作后,它的onCancelled()函数会被调用;在VideoUpdateTask工作顺利完成后,它的onPostExecute()函数会被调用,它们是在主线程中运行的,所以可以在这里修改界面,

5.3 刷新遗留的问题

5.3.1 问题1

当应用启动后,VideoUpdateTask开始更新视频信息,此时用户点击“暂停刷新”,任务停止了,然后用户又点击“刷新”。我们会发现以前被列出的视频再次被列了出来。

原来,刷新的时候,没有将已经显示的视频与没有显示的视频区分开,已经添加过的又被重新添加了。

解决的办法就是,在添加视频信息到数据列表里面之前,先检查一下这些视频是否已经被添加过了,如果添加过了,那就不用再添加了。

  1. 重写VideoItem的比较方法,让比较两个VideoItem的原则变成:只要文件所在的路径是相同的,就认为这两个比较项指的是同一个,

  2. VideoUpdateTaskdoInBackground()函数中,添加视频信息到ListView的数据集-mVideoItemList之前,先判断里面是否已经包含了这个视频,如果没有包含,才发送给主线程更新界面;比较视频是否相同的依据,就是前面重写的equals()函数,

5.3.2 问题2

问题1的解法又引入了另一个问题:如果有的视频是重复的,那么能不能在给它创建视频缩略图的时候,先忽略掉它,因为创建视频缩略图是个非常消耗内存和时间的事情。

所以我们要修改一下创建缩略图的策略,不要让它已开始就创建,而是让我们根据条件来决定是否要创建缩略图。

  1. 修改VideoItem的设计成,

  2. VideoUpdateTaskdoInBackground()函数中,

5.3.3 问题3

如果用户暂停刷新后,设备中的视频因为别的原因被删除了一个,即使采用问题1的解决办法也不会有效果,因为ListView中的保存的视频信息个数比真实刷新到的要多,没有把ListView中多余的数据给清除掉。

所以,要把ListView中所有多余的视频清除,只能在视频查询完成后才能进行,而且这些真正存在的视频信息还得保存下来,在最后的比较中会使用到。

  1. 将真正存在的视频信息,再保存一份,

  2. VideoUpdateTask结束工作以后(无论是取消还是顺利完成),会调用到onCancelled() onPostExecute(),所以可以在它们共同调用的updateResult()中清除ListView中多余的视频信息,


至此,视频刷新的功能就完成了。

视频播放器 第4节 展示视频列表

第4章 展示视频列表

在应用界面当中,经常需要使用列表来展示内容。
Android SDK提供了ListView控件,来实现这种效果。

ListView需要和Adapter配合使用,ListView负责内容的显示,Adapter负责为ListView提供要展示的数据。

4.1 ListView的使用方法

使用ListView展示内容,通常分下面几个步骤,

  1. 在布局文件中设置ListView布局;

  2. 在Activity界面创建的时候(例如onCreate()当中),通过代码获取ListView

  3. 创建一个Adapter负责为ListView提供数据。Android SDK提供了很多类型的AdapterArrayAdapter CursorAdapter SimpleAdapter等等,它们都是BaseAdapter的子类,简化了Adapter的使用。要显示的数据和显示这项数据项的布局要设置给Adapter

    Android SDK提供了一些常用的数据项布局方式android.R.layout.simple_list_item_1 android.R.layout.simple_list_item_2等等。我们也可以自己设计每一项的布局方式。

  4. Adapter设置给ListView,数据将以列表的形式被展示,

  5. 为显示的每个item添加,点击时代响应处理函数;

综合以上的代码,就是,

4.2 ListView内容的更新

如果需要展示的数据有变化,就需要更新ListView,需要注意,

  1. 界面的更新需要在主线程进行(UI线程),如果在其他线程更新,系统有可能报错,并提示你“不能在非UI线程更新界面元素”;
  2. 修改了Adapter中要展示的数据后,需要使用AdapternotifyDataSetChanged(),界面就会刷新,看到修改的效果;
    例如,

4.3 自定义Adapter

虽然Android SDK为我们提供了好几种现成的Adapter使用,但有时它们也并不能完全符合我们的要求,要么用起来还是麻烦,要么大材小用。另外,为了把ListView介绍的全面一些,我们准备自定义一个Adapter。

4.3.1 定义数据项的布局

为了让列表的数据项按照我们设计的模样显示,我们需要为它设计一个布局,并把视频信息展示上去。

数据项的布局定义在res\layout\video_item.xml文件中,

  1. 数据项显示在水平布局的LinearLayout中;
  2. 视频缩略图用ImageView控件显示,给它的android:scaleType属性设置center,让缩略图居中放置,背景设置成应用主题的色调colorPrimary
  3. 其他视频信息包含标题和创建时间,将它们竖直排列放在一个LinearLayout中,占用高度按照2:1分配,前者使用主题中较大的字体?android:attr/textAppearanceMedium,后者使用主题中较小的字体?android:attr/textAppearanceSmall
  4. 至于各个组件之间的间隔,根据自己的视觉偏好调整就好了,用android:paddingandroid:layout_margin设置;

4.3.2 定义Adapter

所有Adapter都是继承自BaseAdapter的,我们这里的Adapter准备继承自它的一个子类ArrayAdapter。因为ArrayAdapter在最原始的基础上作出了改进,我们再在它的基础上做一些小的调整就可以用了,而不用完全从头来过。

  1. 继承ArrayAdapter,将显示的数据类型指定成VideoItem;重新构造函数,传入Context,数据项布局使用的布局ID,要显示的数据列表;重写它的getView()方法;

  2. 在构造函数中,保存好布局ID以后使用,通过Context获取Inflater,为以后数据项布局的创建做准备,

  3. getView()函数中,创建数据项的布局,并为他们赋值,最后将这个布局返回给ListView,让它显示,

    这里的convertView就是数据项所代表的那个布局,当ListView刚创建,还没有产生任何数据项的时候,它就是为null的,此时我们就需要创建一个布局,并通过getView()将这个布局返回给ListView

    假如ListView上的数据项布局已经足够了,那么这里传入的convertView就不会再是“null”,而是之前的某个数据项布局,我们就不必为此重新创建了,只需要更新上面的内容就好。这样提高了界面刷新的效率。

    当然,这里还能通过其他方法减少使用findViewById(),进一步提高效率,不过目前就不改进了,先把功能实现完成。

4.4 使用VideoItemAdapter

  1. 在Video List的Activity创建之时,我们在onCreate()中创建并设置VideoAdapter

  2. VideoUpdateTaskonProgressUpdate中,将获取的新的视频信息,添加到数据列表中,并使用notifyDataSetChanged()刷新,


    至此,视频列表的界面就能看到视频列表了。

视频播放器 第3节 异步方式获取视频信息

第3节 异步方式获取视频信息

获取视频信息所需要的时间是个不能确定的事情。如果视频很少,也许几十毫秒就能完成,如果视频很多(比如几十个),也许就要花二十多秒。

安卓应用只有一个主线程-各个组件都是在这个线程中运行。作为组件的之一的Activity就是在这个线程中更新应用界面的,例如,用户点击界面上的一个按钮,按钮得到响应,整个过程就是在这个主线程里。所以这个主线程绝对不可以做耗时的操作。假如在按钮中做了耗时的操作,那么当它进行耗时操作的时候,你去点击界面上的其它按钮是不会有反应的,就好像程序冻在了那里。

我们的代码一旦连续占用这个线程超过一定的时间,系统就会弹出“程序无响应的”提示,这个提示叫做ANR-Applicatin No Response。

因此,我们可以考虑把获取视频信息的操作放到一个单独的线程thread中进行。

这就好比你在正在做一件事情A,突然另一件事情B来打扰你,你不得不停下手头的工作来完成,做完了才能继续之前的工作;这时如果有另外一个人(另一个线程)来帮助你,把事情B全部包揽了,那你就不用分心了。当另一个人把事情B做完后,告诉你一声就可以了。

3.1 异步操作

启动一个新的线程,分担耗时工作的方法是一种异步操作:我让你帮我做一件事情,布置任务后,我就去做其他的事情了,等你做完了再告诉我结果;

与它对应的是同步操作:我让你帮我做一件事情,布置任务后,我啥也不做,就等着你做完了告诉我结果;

获取视频信息是个异步操作,启动一个新线程-工作线程thread-查询视频信息,查询完成后,工作线程再将结果通知到主线程,让主线程将查询到结果的结果显示到界面上。界面的更新一定要在主线程中进行,不能在别的线程修改,否则系统后提示运行错误,这一点相当重要。因此我们一定要将查询的结果发送给主线程,让主线程处理界面的更新。

3.2 异步操作的方案

安卓系统提供的异步操作方案有:

  1. 创建工作线程thread和Handler,利用Handler在工作线程和主线程之间传递数据;
  2. 使用AsyncTask帮助类,AsyncTask中封装了工作线程,通过AsyncTask完成工作线程和主线程之间的数据传递;

这里虽然将AsyncTask看成是一个单独的方案,但实际上它也是通过方案1实现的,只不过对于使用者来讲更加方便而已。

这里我们选择方案2。因为,

  1. 使用场景简单,只是单个任务的异步操作,没有多个线程之间的数据同步考虑;
  2. 使用方便,不用考虑太多的新线程创建的细节;

3.3 AsyncTask的使用

3.3.1 AsyncTask的用法

AsyncTask需要被继承成为一个新的子类来使用,在被继承时,要指定三种参数的类型-Param Progress Result,还需要实现doInBackground(Param...)函数,此外通常还要实现onProgressUpdate(Progress...) onPostExecute(Result) 两个回调函数。

  1. doInBackground(Param... params)函数:传入参数的Param类型就是AsyncTask中指定的Param类型。它运行在新创建的工作线程当中。

    使用MyTask时,要在主线程中使用excute()方法传入不定长参数,让Task运行起来,

    不定长参数会以数组的形式传递到doInBackground()函数当中,

  2. onProgressUpdate(Progress... progresses)函数:传入参数的Progress类型就是AsyncTask中指定的Progress类型。

    doInBackground()中执行的是一个很耗时的工作,有时需要向主线程报告当前的运行状况,这就要使用到publishProgress()函数,publishProgress()也是使用的不定长参数,

    不定长参数会以数组的形式传递到onProgressUpdate()函数当中,

  3. onPostExecute(Result result)函数:传入参数的Result类型就是AsyncTask中指定的Result类型。

    doInBackground()函数返回的类型也是Result

    返回的结果作为参数传递给onPostExecute()函数,

  4. onCancel()函数会在调用者取消AsyncTask的工作的时候被触发。

    要取消AsyncTask的工作,首先要在主线程中调用cancel()方法,

    因为在doInBackground()中执行的是一个很耗时的工作,需要时不时的检查自己是否被取消执行了,

    最后,onCancelled()函数会被触发,这个函数会在主线程中被执行,


综合上面的分析,自定义一个AsyncTask的方法如下,

使用一个AsyncTask的方法如下,

3.3.2 获取视频信息的AsyncTask

根据我们的需要,自己定义个AsyncTaskVideoUpdateTask

  1. 不需要为新创建的线程传入参数;所以Param设置成Object
  2. 因为查询的过程很长,所以需要时不时通知主线程查询的状态,每查询到一条,就将视频数据传递给主线程;所以Progress设置成VideoItem
  3. 查询的结果已经在查询的过程中发送给了主线程,全部完成后,不需要再传递什么结果给主线程了,所以Result设置成Void
  4. 将查询视频信息的操作放到doInBackground()中进行,这是一个新创建的工作线程;
  5. 工作线程中,每发现一个视频,就通知给主线程;

在视频列表Activity创建的时候,启动VideoUpdateTask,开始查询符合我们要求的视频信息。

在视频列表Activity退出的时候,判断VideoUpdateTask是否还在运行,如果还在运行,就让它停止,

onCreate()onDestroy()是Activity生命周期的一部分,当一个Activity被创建的时候会调用到onCreate(),当Activity被退出销毁的时候会调用到onDestroy()。所以在这两个地方使用VideoUpdateTask是一个合适的选择。

视频播放器 第2节 获取视频信息

第2节 获取视频信息

要知道设备上有哪些可以被播放的视频文件,一般来讲有两个方法,

  1. 遍历设备磁盘上所有的目录,根据文件的后缀名,把这些目录中所有的视频文件都找出来;
  2. 向安卓系统提供的Media Provider提出查询请求,从而获取我们希望的视频文件信息;

从“减小开发难度,利用安卓系统自身功能,选择最简单的方案”的角度出发,我们采用Media Provider

2.1 ContentProvider

ContentProvider是安卓系统的四大组件之一,为别的组件(Activity、Service)“提供内容”。它就像是一个拥有某种数据的网站,安卓系统运行的其它组件可以通过“网址”访问这个网站,获取需要查询的数据。

  1. ContentProvider可以是私有的,只能为它所在的应用提供数据访问请求;
  2. ContentProvider也可以是公开的,为别的应用程序提供数据访问请求。
    当我们自己开发一个ContentProvider的时候,就要在应用的AndroidManifest.xml文件中声明它的存在,

从这里也可以看出,它和Activity的地位是一样的,所以与Activity一样并称为安卓系统的四大组件之一。

2.2 系统级的ContentProvider

安卓系统上,有一个叫做Media ProviderContentProvider。它作为系统级别的应用程序在系统上运行,专门负责收集多媒体文件(音频、视频、文件)相关的信息。

Media Provider在开机启动后,会在后台“监听”磁盘上文件的变化,特定情况下,会自动更新多媒体文件的信息,例如磁盘上是否增加了媒体文件,是否被删除了媒体文件,有的媒体文件名称是否发生了修改等等。

所以当任何应用想获取这类文件相关的信息时,就可以向Media Provider发起查询的请求。Media Provider帮我们完成了视频文件信息的收集,因此,我们就不用自己去遍历磁盘上的文件进行视频文件的收集和整理了。

除了Media Provider,系统还提供了

  1. Contacts Provider:用来查询联系人信息;
  2. Calendar Provider:用来提供日历相关信息的查询;
  3. Bookmark Provider:用来提供书签信息的查询;


其它的就不再一一列出了。

2.3 使用Media Provider的缺点

使用Media Provider有一点需要注意,Media Provider对磁盘多媒体文件的“监控”,并不是实时的。当删除磁盘上一个已有的视频文件时,Media Provider并不会马上知道,而是要等到下一次的扫描之后,才会更新这个信息。因此,如果使用Media Provider做为我们提供视频信息的来源,就要考虑到“一个视频刚好被修改了,但是还没有来得及在Media Provider中更新信息”的情况。

2.4 Media Provider查询视频文件

  1. 确定向Media Provider发出查询请求的地址-uri,它就像访问网站时,要输入的网址一样。系统提供了两个位置的uri,一个是指向内部存储的uri,一个是指向外部存储的uri。我们要查询的视频文件都是存放在外部存储地址上的;

  2. 确定要请求的视频文件信息。在视频列表中,我们需要展示视频的标题、创建时间,还需要播放它时使用的文件所在地址。这些信息在Media Provider中都对应着查询它们使用的字段名称;

  3. 确定查询的条件。我们之前假设过只关心那些叫做Video的目录。因此我们要确定的只是查询到的文件路径中,包含有/Video这个字段。

    这个条件参数的写法就和SQL数据库语言的语法一样。这里我们不打算讲SQL语法,只要知道在我们这个例子中这样使用就好了;

  4. 设定查询结果的排序方式,使用默认的排序方式就可以了,

  5. 获取ContentResolver对象,让它使用前面的参数向Media Provider发起查询请求;查询的结果存放在Cursor--指标当中;

  6. 遍历Cursor,得到它指向的每一条查询到的信息;当Cursor指向某条数据的时候,我们就获取它携带的每个字段的值;

  7. 存放获取的视频文件信息,创建一个VideoItem类

    获取到的视频文件创建的时间是Unix时间戳,就是一个类似于1464152901这样的数字。它是从1970年1月1日0时开始,所经过的秒数。
    所以要将视频文件创建的时间从Unix格式的时间戳,转换成“年月日分”这种可读的形式,

    获取视频文件的缩略图,Android SDK提供了一个利用视频文件地址获取视频缩略图的工具,用起来非常简单的,通过它将得到缩略图的Bitmap;

    图片生成的尺寸可以通过第二个参数设置,

    1. MINI_KIND表示小的缩略图;
    2. FULL_SCREEN_KIND表示大尺寸的缩略图;
    3. MICRO_KIND表示超小图的缩略图;

    这里我们采用的是MINI_KIND

  8. Cursor使用完了之后要把它关闭掉,


整理一下前面的各个步骤,获取外部存储上Video目录中所有视频文件的方式如下,

最后一点千万不要忘记,要在应用的AndroidManifest.xml文件中,添加读取外部存储器的权限,

视频播放器 第1节 设计方案

第1节 设计方案

1.1 可能的功能点

虽然是一个简单视频播放器,但是麻雀虽小五脏俱全,可能需要备以下功能:

  1. 将设备上存在的视频展现出来;
  2. 能显示某个视频详细的信息,例如视频文件大小、视频分辨率等等;
  3. 选择某个视频后,能够播放视频;
  4. 播放视频时能够控制视频的暂停、继续、快进、快退;
  5. 可以全屏幕的播放视频;
  6. 可以提供给系统使用,作为播放视频文件的可选播放器;
  7. 如果有电话接入,播放器能够自动暂停播放;
  8. 可以管理视频文件,例如删除某个或者多个视频;
  9. 视频可以悬浮于其它应用之上播放,一边刷微信,一边看视频;

1.2 功能广度与功能深度的权衡

每个功能看上去一句话就说完了,但是仔细分析起来,却有很多细节需要考虑。
例如功能1,如果将设备上所有的视频都展现出来,那么,

  1. 可能会消耗很长的时间;
  2. 每个视频都要有缩略图,如果搜寻这些视频文件的时候,同时为它们创建缩略图,也许会非常消耗内存,假如视频非常多,那么有可能会耗尽安卓系统分配给一个应用最大的内存使用量,导致程序崩溃;
  3. 播放器在查找视频文件并显示出来的过程中,如果用户通过别的工具把这个已经显示到列表的文件删除了,例如播放器正在刷新列表,把文件1.mp4显示到了列表中,这时用户通过PC端的手机管理软件,把1.mp4这个文件从设备上删除了(播放器刷新视频文件的过程还没有结束),那么手机的显示列表上要能马上发现这个意外,再次更新;
  4. 也可能设备上有很多短小的视频文件,可能只有几十K或者几百K,根本就不是用户关心的视频,例如微信上接受的短小视频,用户就不会使用我们的视频播放器进行播放;
  5. 也可能用户根本不愿意播放器找出某些“敏感”的视频文件;

这都是我们在开发中需要去实实在在解决的问题。功能1遇到的这些问题,我们都可以通过良好的设计来解决它。不过代价也许就是,

  1. 增加了程序的复杂度,花费更多的开发时间;
  2. 为了解决一个问题,而引入另外一个更佳不易解决的问题、引入另外一个潜在的bug;
  3. 开发钻进了死胡同,到处都是要解决的问题,不知如何开始了;

要知道,这还只是功能1中提出的问题,不知道别的功能点还会将一个应用的复杂度增加到什么样的地步。

所以,对项目规划的设计师和程序的开发者来说,需要在单个功能完备的深度与多个功能的实现广度上,作出智慧的取舍。

好在软件的发布并不是一锤子买卖,可以在发布以后,再对它进行升级。所以我们通常先把最为重要的功能做出来,而对于这些最重要的功能则采用最容易实现的方案。

以后再对功能做出扩充,对实现的方案加以修改,逐步变的越来越完美。这个过程也更适合初学者一步一步的掌握程序设计的思路、积累开发的经验,知道同样一个功能,怎么设计更加合适,为什么要这样设计。

于是,对于我们第一个版本的视频播放器,我们将尽量采用最简单的设计,不去太多的考虑它的程序结构和执行效果,重点是用简单而正确的方式把功能做出来,以后再在新的版本中迭代优化。

1.3 现阶段的功能

为了简化问题,我们先对视频播放器允许的环境和功能做出这样的假设:

  1. 只读取特定目录下的视频文件,例如名称叫做Video的目录;
  2. 假设用户没有那么BT:在展示视频列表的时候,用别的工具,把已经展示上去的文件给删除了;
  3. 在列举视频文件的时候,因为耗时可能很长,所以可以选择暂停;
  4. 视频文件的数量不会多的特别离谱,上千上万个视频是不会遇到的;
  5. 只播放系统原生支持的视频格式;
  6. 播放器在横屏的时候,自动切换成全屏;在竖屏的时候,占用部分区域,剩下的区域显示视频详细信息;
  7. 没有视频的播放记录,用户主动退出的情况下,下次再播放同样的视频会从头开始;
  8. 没有视频的管理功能,不能改变视频文件的位置,不能删除它,不能修改文件名字;
  9. 没有悬浮播放的功能;

因此,对视频播放器的界面进行了如下的设计:

1.4 关于遗憾

对于那些没有在这个阶段加入的功能,期待以后加入吧;

对于那些为了简化开发难度、减少开发时间而采用的简单设计,期待在下一版程序中优化和完善吧;

当然,别忘了这个应用的目标是:教会大家更多的安卓开发技能。所以在选择实现方案的时候,着重于原理的展示,而不一定采用执行效率最优的方案。