Android VR Player(全景視頻播放器) [6]:視頻列表的實現-本地視頻

Android VR Player(全景視頻播放器) [6]:視頻列表的實現-本地視頻

(本篇博客參考《Android第一行代碼(第二版)》中關於RecyclerView的部分)

列表的實現方式

列表一般使用Listview來實現,但是Listview使用時需要做一些技巧性的優化,否者性能會很差,而且Listview擴展性不太好,所以我們可以使用Android提供的更強大的滾動控件,RecyclerView,來實現視頻列表。本篇博客先分享本地視頻列表的實現,下篇博客將分享如何實現網絡視頻列表。


RecyclerView實現本地視頻列表

添加依賴

新建項目,然後和前面使用bottomnavigationbar一樣,在項目app的build.gradle文件的dependencies {}閉包中添加相應的依賴:

compile 'com.android.support:recyclerview-v7:25.3.1'

不要添加到項目的build.gradle文件中了。我發現Android Studio新建項目後的視圖是“Android”,開發時,我們一般使用“Project”,會讓我們對整個工程的目錄結構看得比較清晰。如下圖所示:
這裏寫圖片描述
點擊下拉菜單,選擇“Project”。


準備VideoItem和VideoItemAdapter

可以不那麼準確的理解,視頻列表就是一個數組,這個數組裏面有很多元素,每個元素就是一個VideoItem類的實例。VideoItem類包含一個視頻列表項的基本信息,比如視頻截圖,視頻長度,視頻名字等等,一個不完整的示例如下:

public class VideoItem {
    public String name;
    public String path;
    public Bitmap thumb;
    public String createdTime;
    public String duration;
    VideoItem(String strPath, String strName, String strCreatedTime,String strDuration,Bitmap thumb) {
        this.path = strPath;
        this.name = strName;
        this.createdTime = strCreatedTime;
        this.duration = strDuration;
        this.thumb = thumb;
    }
    ...
     public String getName() {
        return name;
    }

    public Bitmap getThumb() {
        return thumb;
    }
    .....

上面的類很簡單,包含視頻的名字,路徑等屬性,我們提供一個帶參數的構造方法,當然,我們還需要提供相關的Getter和Setter方法。在Android Studio中,使用Alt+Insert來自動生成一些方法,比如Getter和Setter,Override方法等等。具體應用時,還需要考慮一些具體的情況,增加一些屬性和其他的一些必要的方法。

現在我們已經有了“數組元素”,下一步就是把這些元素添加到一個List中,有了這個List,下一步就是如何展示這個List。但是,沒法直接在RecyclerView和ListView這樣的View中展示一個List,所以,我們還需要一個適配器,VideoItemAdapter。

到這裏,我們需要理一下思路,VideoItem構成的VideoItemList,準備好了要展示數據,這是數據處理階段;VideoItemAdapter負責把VideoItemList中的數據加載到RecyclerView中,這是View處理階段。既然涉及到View,自然要有相關的佈局。這裏需要的是VideoItem的佈局。這裏需要一點類比的思想,VideoItemList包含很多數據子項,它們是一些VideoItem;而RecyclerView包含很多View子項,它們是一些ViewItem的view。怎麼把數據展示到View中,這就是VideoItemAdapter的工作。

一個簡單的videoItem的佈局可以是:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:paddingTop="5dp"
    android:orientation="vertical">

    <ImageView
        android:id="@+id/video_thumb"
        android:layout_alignParentLeft="true"
        android:layout_marginLeft="15dp"
        android:layout_width="120dp"
        android:layout_height="90dp"
        android:scaleType="fitXY"
        android:src="@mipmap/ic_launcher"/>

    <TextView
        android:id="@+id/video_title"
        android:layout_toRightOf="@+id/video_thumb"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="this is titile"
        android:textSize="16sp"
        android:layout_marginLeft="12dp"
        />
    <TextView
        android:id="@+id/video_date"
        android:layout_below="@+id/video_title"
        android:layout_toRightOf="@+id/video_thumb"
        android:layout_marginBottom="5dp"
        android:layout_marginLeft="12dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="video size"
        android:textSize="12sp"
        />
    <TextView
        android:id="@+id/video_duration"
        android:layout_below="@+id/video_title"
        android:layout_alignParentRight="true"
        android:layout_marginRight="15dp"
        android:layout_marginBottom="5dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="video time last"
        android:textSize="12sp"
        />

</RelativeLayout>

它的顯示效果爲:
這裏寫圖片描述


VideoItemAdapter繼承自RecyclerView.Adapter,並且指定泛型爲VideoItemAdapter.ViewHolder,ViewHolder是在VideoItemAdapter中定義的內部類。ViewHolder的構造函數需要傳入一個View,這個View就是我們的RecyclerView包含的View子項的佈局。VideoItemAdapter的完整代碼如下:

package com.example.renkangchen.testlist;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.List;
/**
 * Created by renkangchen on 17-6-2.
 */
public class VideoItemAdapter extends RecyclerView.Adapter<VideoItemAdapter.ViewHolder>{
    private List<VideoItem> mVideoList;
    static class ViewHolder extends RecyclerView.ViewHolder{
        View videoListView;

        TextView textView_title;
        TextView textView_createTime;
        TextView textView_duration;
        ImageView thumb;

        public ViewHolder(View view){
            super(view);
            videoListView = view;
            textView_title = (TextView)view.findViewById(R.id.video_title);
            textView_createTime = (TextView)view.findViewById(R.id.video_date);
            textView_duration = (TextView)view.findViewById(R.id.video_duration);
            thumb = (ImageView)view.findViewById(R.id.video_thumb);
        }
    }

    public VideoItemAdapter(List<VideoItem> videoList){
        mVideoList = videoList;
    }

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.video_item,parent,false);
        final ViewHolder holder = new ViewHolder(view);
        holder.videoListView.setOnClickListener(new View.OnClickListener(){
            @Override
            public void onClick(View v) {
                int position = holder.getAdapterPosition();
                VideoItem videoItem = mVideoList.get(position);
                Toast.makeText(v.getContext(),"click on"+videoItem.getName(),Toast.LENGTH_SHORT).show();
            }
        });
        holder.thumb.setOnClickListener(new View.OnClickListener(){
            @Override
            public void onClick(View v) {
                int position = holder.getAdapterPosition();
                VideoItem videoItem = mVideoList.get(position);
                Toast.makeText(v.getContext(),"click on image "+videoItem.getName(),Toast.LENGTH_SHORT).show();
            }
        });
        return holder;
    }

    @Override
    public void onBindViewHolder(ViewHolder holder, int position) {
        VideoItem videoItem = mVideoList.get(position);
        holder.textView_title.setText(videoItem.getName());
        holder.textView_createTime.setText(videoItem.getCreatedTime());
        holder.textView_duration.setText(videoItem.getStrDuration());
        holder.thumb.setImageBitmap(videoItem.getThumb());
    }

    @Override
    public int getItemCount() {
        return mVideoList.size();
    }

}

看代碼應該會更清楚些。我們用ViewHolder來得到一個videoItem的佈局;VideoItemAdapter的構造方法用來傳入要展示的數據; onCreateViewHolder方法用來創建ViewHolder的實例;onBindViewHolder方法會在一個RecyclerView的子項被滾動到屏幕範圍內時調用,它用來給RecyclerView的子項賦值;getItemCount返回RecyclerView的子項數目。需要注意的是,我們必須 重寫onCreateViewHolder,onBindViewHolder,getItemCount這三個方法。到這裏,我們應該更明白這個VideoItemAdapter的作用了。

另外,我們在onCreateViewHolder的方法中添加了Recyclerview的點擊監聽。代碼應該比較容易理解,不過需要注意的是,不同於ListView只能爲Recyclerview子項整體添加,我們可以分別爲Recyclerview子項的不同部分添加監聽,這也是Recyclerview的優勢所在。

用過ListView的同學應該有感覺,就是雖然兩種方法的Adapter代碼量差不多,但明顯,Recyclerview的Adapter更容易理解些。而且不用自己去做一些優化工作,使用起來相對比較容易。


AsyncTask異步任務讀取本地視頻信息

之前的部分已經準備好了“碗筷”,現在就等“上菜”了。這裏的“菜”便是我們從手機存儲空間(這裏不使用內存這樣的概念是爲了防止混淆)中讀取到的視頻信息。之前的博客中分享過,爲了避免ANR,像讀取存儲空間,訪問網絡資源這樣比較耗時的操作,都不能放主線程中,而應該開闢子線程去完成。但是多線程的使用並不是一件輕鬆的事情,因此,Android提供了AsyncTask類來方便我們更加輕鬆地完成在子線程中進行UI的更新工作。

AsyncTask類包含doInBackground,onProgressUpdate,onPostExecute等幾個方法,使用時,我們把需要異步執行的任務放在doInBackground中,onProgressUpdate在異步任務更新時調用,用來動態更新UI,onPostExecute在異步任務結束時調用。AsyncTask類雖然已經幫我們簡化了很多的工作,但是要用好AsyncTask也不是一件容易,有特別多需要注意的地方,有興趣的同學可以自己搜索一下相關資料,或者看一下參考鏈接中的資料。

這裏主要分享一下如何在doInBackground中掃描本地的視頻信息。

private class VideoUpdateTask  extends AsyncTask<Void, VideoItem, Void> {
        List<VideoItem> mDataList = new ArrayList<VideoItem>();
        ....
 }

首先我們創建一個VideoUpdateTask,它繼承自AsyncTask,繼承時,我們可以指定三個泛型參數,

參數 含義
Params 執行AsyncTask需要傳入的參數,用於後臺任務
Progress 後臺任務執行過程中,使用這裏指定的類型作爲進度的單位,用於在界面上顯示進度
Result 任務執行完畢時,使用這裏指定的類型作爲結果返回類型

於是我們可以這樣定義

private class VideoUpdateTask  extends AsyncTask<Void, VideoItem, Void> 

Params參數指定爲Void,表示我們不需要向後臺任務傳入什麼參數;Progress參數指定爲VideoItem,表示更新的單位爲一個VideoItem;Result參數指定爲Void,表示後臺任務結束後,我們不需要返回什麼結果。

需要注意的是,聲明VideoUpdateTask時,需要使用

private AsyncTask<Void, VideoItem, Void> mVideoUpdateTask;

而不是

private AsyncTask mVideoUpdateTask;

或者直接聲明成

private VideoUpdateTask mVideoUpdateTask;

這涉及到多態的一些知識,這裏不作展開。
在 doInBackground(Void… params)的參數爲Void類型的可變參數,如果你指定的Params爲String,那麼就應該寫成 doInBackground(String… params)。

 @Override
        protected Void doInBackground(Void... params) {
            Uri uri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
            String[] searchKey = new String[] {
                    MediaStore.Video.Media.TITLE,
                    MediaStore.Images.Media.DATA,
                    MediaStore.Images.Media.DATE_ADDED,
                    MediaStore.Video.Media.DURATION
            };
            String where = null;//scan all video in the media store
            String [] keywords = null;
            String sortOrder = MediaStore.Video.Media.DEFAULT_SORT_ORDER;

            ContentResolver resolver = getContentResolver();
            Cursor cursor = resolver.query(uri, searchKey, where, keywords, sortOrder);

            if(cursor != null)
            {
                while(cursor.moveToNext() && ! isCancelled())
                {
                    String path = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA));

                    String name = cursor.getString(cursor.getColumnIndex(MediaStore.Video.Media.TITLE));
                    String createdTime = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATE_ADDED));
                    Long duration = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATION));
                    int duration2second = (int)(duration/1000);
                    Bitmap thumb = ThumbnailUtils.createVideoThumbnail(path, MediaStore.Images.Thumbnails.MINI_KIND);

                    VideoItem data = new VideoItem(path, name, createdTime,duration2second,thumb);
                    Log.d(TAG, "doInBackground: video item ==== "+data.getName());
                    publishProgress(data);
                    mDataList.add(data);
                }
                cursor.close();
            }
            return null;
        }

這裏的“掃描”其實準確地講是從系統提供的媒體庫中讀取視頻相關的數據,媒體庫爲我們提供了相應的內容提供器作爲訪問接口。內容提供器作爲Android四大組件(內容提供器,活動,服務,廣播接收器)之一,無疑是十分重要的知識,通過這個例子我們便可以瞭解內容提供器的用法,不過暫時我們不需要寫自己的內容提供器,而是先試着用其他應用提供的內容提供器。要訪問內容提供器的內容,就一定要使用ContextResolver:

  ContentResolver resolver = getContentResolver();

通過getContentResolver()來獲取ContentResolver的一個實例。ContentResolver爲我們提供了一系列的方法來進行增刪查改操作(CRUD:Create,Retrieve,Update,Delete)。這裏我們需要的方法爲query,即查詢系統媒體庫中的視頻信息。

 Cursor cursor = resolver.query(uri, searchKey, where, keywords, sortOrder);

query需要的幾個參數爲:

參數 含義
uri 指定查詢的表名
projection 指定查詢的列名
selection 指定where的約束條件
selectionArgs 指定爲where中的佔位符提供具體的值
orderBy 指定查詢結果的排序方式
Uri uri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
String[] searchKey = new String[] {
      MediaStore.Video.Media.TITLE,
      MediaStore.Images.Media.DATA,
      MediaStore.Images.Media.DATE_ADDED,
      MediaStore.Video.Media.DURATION
      };
String where = null;//scan all video in the media store
String [] keywords = null;
String sortOrder = MediaStore.Video.Media.DEFAULT_SORT_ORDER;

uri我們直接使用MediaStore.Video.Media.EXTERNAL_CONTENT_URI,其中EXTERNAL_CONTENT_URI常量是一個Uri.parse()解析後的結果,所以直接使用就可以了;searchKey 中我們指定需要查詢的列是TITLE,DATA,DATE_ADDED等幾列;where條件指定爲null,表示查詢媒體庫所有的視頻,當然也可以指定查詢特定路勁下的視頻,可以用這樣的方式

String where = MediaStore.Video.Media.DATA + " like \"%"+getString(R.string.search_path)+"%\"";

來查詢特定路徑下的視頻;keywords同樣設爲null;sortOrder使用默認排序方式就好了。

查詢的結果返回一個Cursor類型對象,Cursor翻譯爲光標或者是遊標,在Android中,它是每行的集合。得到了返回的cursor,我們就可以把視頻信息從cursor中逐個讀取出來了

 if(cursor != null) {
                while(cursor.moveToNext() && ! isCancelled()){
                    String path = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA));
                    String name = cursor.getString(cursor.getColumnIndex(MediaStore.Video.Media.TITLE));
                    String createdTime = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATE_ADDED));
                    Long duration = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATION));
                    int duration2second = (int)(duration/1000);
                    Bitmap thumb = ThumbnailUtils.createVideoThumbnail(path, MediaStore.Images.Thumbnails.MINI_KIND);
                    VideoItem data = new VideoItem(path, name, createdTime,duration2second,thumb);
                    Log.d(TAG, "doInBackground: video item ==== "+data.getName());
                    publishProgress(data);
                    mDataList.add(data);
    }
    cursor.close();
}

讀取的方法就是通過移動moveToNext()遊標的位置來遍歷返回的Cursor對象中的所有行,然後在取出每一行中對應列的數據。需要注意最後的close操作,不要忘記了這個。有過關係數據庫使用經歷的同學會比較容易理解上面這些操作。注意到每次讀取完一行數據後我們都使用了

publishProgress(data);
//publishProgress源碼
  @WorkerThread
    protected final void publishProgress(Progress... values) {
        if (!isCancelled()) {
            getHandler().obtainMessage(MESSAGE_POST_PROGRESS,
                    new AsyncTaskResult<Progress>(this, values)).sendToTarget();
        }
    }

publishProgress方法用來發送UI更新消息,它被調用後,onProgressUpdate很快會被調用,不可以在 doInBackground中調用onProgressUpdate方法,涉及到UI更新的操作都不可放在 doInBackground。onProgressUpdate可以是:

 @Override
        protected void onProgressUpdate(VideoItem... values) {
            VideoItem data = values[0];
            mVideoList.add(data);
            mVideoItemAdapter.notifyDataSetChanged();
        }

VideoItem 就是我們聲明AsyncTask時,指定的三個泛型

在MainAcivity中使用RecyclerView

要在在MainAcivity中使用RecyclerView,當然先要在MainAcivity的佈局中添加這個view:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.example.renkangchen.testlist.MainActivity">

     <android.support.v7.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    </android.support.v7.widget.RecyclerView>

</FrameLayout>

RecyclerView的使用沒有太大的差別

private LinearLayoutManager mLinearLayoutManager;

在onCreate中

mRecyclerView = (RecyclerView)findViewById(R.id.recyclerView);

另外mRecyclerView還需要用setLayoutManager方法來設置佈局管理器(這個是必需的,比如你想橫着顯示列表,而不是豎着,比如你想要瀑布效果,都可以通過指定佈局管理器來實現);用setAdapter方法來設置Adapter(這個也是必需的);用setItemAnimator來設置添加和移除的動畫(這個是非必需的);用addItemDecoration來設置分割線的(這個也是非必需的,可以定製自己的分割線樣式,而不是像Listview那樣只能用默認的)。之前說過RecyclerView十分強大,意味着它是更自由的,所以和Listview比起來它似乎是更復雜了些,但這種“複雜”也讓我們可以實現更多的定製的效果。

這裏我們就簡單些,只使用兩個必要的方法,把RecyclerView的佈局管理器指定爲LinearLayoutManager,Adapter當然就是我們之前寫的VideoItemAdapter了。

然後用異步任務VideoUpdateTask來”掃描“本地媒體庫信息,並進行UI更新。需要注意的是,讀取媒體庫是幾組敏感操作中的一個,所以必須要要進行權限申請,6.0以後需要動態申請權限,而不是直接在AndroidManifest中申請就可以了。

 if(Build.VERSION.SDK_INT >= 23) {
            if ((ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE) !=
                    PackageManager.PERMISSION_GRANTED) && (ContextCompat.checkSelfPermission(MainActivity.this,         Manifest.permission.READ_EXTERNAL_STORAGE) !=
                    PackageManager.PERMISSION_GRANTED)) {
                //get the permission
                ActivityCompat.requestPermissions(MainActivity.this,new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},1);
                ActivityCompat.requestPermissions(MainActivity.this,new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},1);
            }//if without permission
            else{
                updateVideoList();
            }
        }//if higher sdk 23
        else{
            updateVideoList();
        }
...
  @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        switch (requestCode){
            case 1:
                if(grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED){
                    updateVideoList();
                }else{
                    Toast.makeText(this,"You denied the Permission",Toast.LENGTH_SHORT).show();
                }break;
            default:
                break;
        }
    }

首先做個判斷,6.0以後才需要動態申請,否者直接用AndroidManifest中獲得的權限即可。這裏申請了兩個權限WRITE_EXTERNAL_STORAGE和READ_EXTERNAL_STORAGE,雖然只會用到讀權限但讀寫往往在一起的,還是一起申請了吧。但動態權限機制引入的目的就是爲了權限被濫用,所以規範來講還是應該做到用什麼權限就申請什麼權限,而不是一來就要申請一大堆權限。onRequestPermissionsResult返回申請的結果,申請成功的話,我們就調用updateVideoList()這個方法:

  public void updateVideoList(){
        mVideoList.clear();
        mVideoUpdateTask = new VideoUpdateTask();
        mVideoUpdateTask.execute();
    }

它先清除mVideoList中的數據,然後新建一個VideoUpdateTask,並使用execute()方法來執行這個異步任務。

如果用戶拒絕了權限申請,我們需要進行相應的操作反饋。如果是6.0以前的系統,那就直接調用updateVideoList()就好了。


結果

build工程,然後運行:
這裏寫圖片描述
提示請求權限,這裏我們選擇同意。
這裏寫圖片描述
可以看到,成功地讀取了本地的視頻文件(這裏本地只有一個視頻,爲了展示列表的效果,就複製了幾次,幾個視頻只是名字不同)。
這裏寫圖片描述
點擊其中一個視頻列表項,觸發點擊事件,
這裏寫圖片描述
點擊其中一個視頻列表項的視頻縮略圖,,觸發另外一個點擊事件。

總結一下,本篇博文涉及的內容較多,自己也感覺不論ListView還是RecyclerView都是Android衆多控件中最常用也是最複雜難用的。時間比較緊張,寫得很粗糙,疏漏錯誤之處,還請大家指正。下面是自己做此部分時閱讀的一些參考資料。


Reference

手把手教你做視頻播放器(三)-展示視頻列表
第4章 展示視頻列表
Android RecyclerView 使用完全解析 體驗藝術般的控件
深入理解AsyncTask的工作原理
解讀ContentResolver和ContentProvider
Android 中關於 【Cursor】 類的介紹
Android四大基本組件介紹與生命週期
Requesting Permissions(自備梯子)
Java多態性理解
Java Object類


參考源碼

因爲寫博客時爲了方便說明,會去掉一部分代碼,所以建議下載完整的工程源碼:
鏈接: https://pan.baidu.com/s/1boUg5P1 密碼: h4rf

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章