前一篇已經將MainActivity編寫好了,其中主頁面的ViewPager控件嵌入的是兩個fragment,分別是VideoListFragment 和 AudioListFragment。今天主要理一下視頻這一模塊,包括:
通過ContentProvider獲取視頻列表數據(使用AsyncQueryHandler異步獲取)
自定義播放頁面
播放頁面邏輯處理(播放、暫停、切換、音量等)
效果圖
結構圖
獲取視頻列表數據
分析:
觀察視頻列表,我們需要這些數據:視頻名稱(TITLE),視頻時長(DURATION),視頻大小(SIZE),當然每一個視頻都有一個唯一的路徑(DATA),這個也需要。
那麼就可以將這些視頻共有的屬性封裝成一個JavaBean。這裏用遊標cursor去查視頻數據,適配器就繼承CursorAdapter,它是BaseAdapter的子類。在newView()方法裏,加載每一項的佈局文件;在bindView()方法中,如果view不爲空,設置數據,否則會先調用newView()方法生成一個View。
public class VideoListAdapter extends CursorAdapter {
public VideoListAdapter(Context context, Cursor c, boolean autoRequery) {
super(context, c, autoRequery);
}
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
return View.inflate(context, R.layout.item_video_list, null);
}
@Override
public void bindView(View view, Context context, Cursor cursor) {
ViewHolder holder = getHolder(view);
VideoItem videoItem = VideoItem.fromCursor(cursor);
holder.tvName.setText(videoItem.getTitle());
holder.tvDuration.setText(StringUtil.formatVideoDuration(videoItem.getDuration()));
holder.tvSize.setText(Formatter.formatFileSize(context, videoItem.getSize()));
}
private ViewHolder getHolder(View view) {
ViewHolder holder = (ViewHolder) view.getTag();
if (holder == null) {
holder = new ViewHolder(view);
view.setTag(holder);
}
return holder;
}
private static class ViewHolder {
private TextView tvName, tvDuration, tvSize;
public ViewHolder(View view) {
tvName = (TextView) view.findViewById(R.id.tv_name);
tvDuration = (TextView) view.findViewById(R.id.tv_duration);
tvSize = (TextView) view.findViewById(R.id.tv_size);
}
}
}
說明:bindView()方法中通過cursor獲取數據的方法封裝到VideoItem的JavaBean中了,主要是爲代碼整潔,具體代碼如下:
public static VideoItem fromCursor(Cursor cursor) {
VideoItem videoItem = new VideoItem();
videoItem.setDuration(cursor.getLong(cursor.getColumnIndex(Media.DURATION)));
videoItem.setPath(cursor.getString(cursor.getColumnIndex(Media.DATA)));
videoItem.setSize(cursor.getLong(cursor.getColumnIndex(Media.SIZE)));
videoItem.setTitle(cursor.getString(cursor.getColumnIndex(Media.TITLE)));
return videoItem;
}
- 視頻列表的適配器寫好了,如何獲取數據呢?想一想,如果視頻很多的話,能在主線程操作嗎?顯然不能,android給我們提供了這樣一個類:AsyncQueryHandler,它是個抽象類,我們需要寫一個類去繼承它。(它給我們提供了一個查詢完成的回調方法,我們可以在這個方法裏更新listview)
class SimpleQueryHandler extends AsyncQueryHandler{
public SimpleQueryHandler(ContentResolver cr) {
super(cr);
}
/**
* token: 查詢的標識
*/
@Override
protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
super.onQueryComplete(token, cookie, cursor);
if(cookie!=null && cookie instanceof CursorAdapter){
CursorAdapter adapter = (CursorAdapter) cookie;
adapter.changeCursor(cursor);//相當於notifyDatasetChange
CursorUtil.printCursor(cursor);
}
}
}
- 獲取數據的方法如下:(羅列出要查詢的列後,通過我們自己寫的queryHandler的startQuery()就行了,就這麼簡單)
@Override
protected void initData() {
adapter = new VideoListAdapter(getActivity(), null, false);
lv.setAdapter(adapter);
queryHandler = new SimpleQueryHandler(getActivity().getContentResolver());
String[] projection = { Media._ID, Media.TITLE, Media.SIZE,
Media.DURATION, Media.DATA };
queryHandler.startQuery(0, adapter, Media.EXTERNAL_CONTENT_URI,
projection, null, null, null);
}
至此,就可實現上面視頻列表的效果圖的樣子了。
自定義播放頁面
接來下實現播放視頻的Activity。
- 視頻爲什麼能播放?肯定是在點擊listview的一個item傳了數據過來了呀,是傳當前視頻的信息嗎?進一步想想,播放視頻的頁面是可以切換上一個下一個視頻的啊,所以傳過來的不能只是一個視頻信息,得是所有視頻信息的一個集合。還得傳一個當前視頻的position來作爲要播放的一個標記。
@Override
protected void initListener() {
lv.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Cursor cursor = (Cursor) adapter.getItem(position);
ArrayList<VideoItem> videoList = cursorToList(cursor);
Bundle bundle = new Bundle();
bundle.putInt("currentPosition", position);
bundle.putSerializable("videoList", videoList);
enterActivity(VitamioVideoPlayerActivity.class, bundle);
}
});
}
/**
* 將cursor中的所有記錄轉爲對象放入集合中
* @param cursor
* @return
*/
private ArrayList<VideoItem> cursorToList(Cursor cursor){
cursor.moveToPosition(-1);
ArrayList<VideoItem> list = new ArrayList<VideoItem>();
while(cursor.moveToNext()){
list.add(VideoItem.fromCursor(cursor));
}
return list;
}
數據是傳過來了,別忙處理,先將VideoPlayerActivity的佈局分析一下(開頭的結構圖已經列出該頁面的佈局了。這裏再講一下具體的佈局方法)
- 首先,像這種層疊式佈局用RelativeLayout或者FrameLayout都可以,這裏用RelativeLayout,它更靈活些。
- 裏面首先是個VideoView,寬高都是鋪滿屏幕,但是系統提供的這個視頻控件只能支持部分格式的視頻,我們知道視頻有很多格式,如果你這個播放器不能支持絕大多數,那肯定沒人會用,所以,這裏使用一個開源的框架vitamio,支持所有格式視頻。
- 其次,上下的佈局文件可分別寫在不同文件中,最後include進來。
- 爲增加用戶體驗,可在增加一個正在加載的頁面和一個播放卡頓時的緩衝頁面。
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black">
<io.vov.vitamio.widget.VideoView
android:id="@+id/vv"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerHorizontal="true" />
<include
android:id="@+id/layout_top_control"
layout="@layout/layout_top_control" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true">
<include
android:id="@+id/layout_bottom_control"
layout="@layout/layout_bottom_control" />
</LinearLayout>
<LinearLayout
android:id="@+id/ll_loading"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/bg_player_loading_background"
android:gravity="center"
android:orientation="horizontal">
<ProgressBar
android:layout_width="20dp"
android:layout_height="20dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:singleLine="true"
android:text="正在加載中..."
android:textColor="@color/white"
android:textSize="16sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/ll_buffering"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:visibility="gone">
<ProgressBar
android:layout_width="20dp"
android:layout_height="20dp" />
</LinearLayout>
</RelativeLayout>
頭部控制面板的佈局:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_player_status"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingLeft="5dp"
android:paddingRight="5dp">
<TextView
android:id="@+id/tv_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="視頻名稱"
android:textColor="@color/white"
android:textSize="14sp" />
<ImageView
android:id="@+id/iv_battery"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@mipmap/ic_battery_0" />
<TextView
android:id="@+id/tv_system_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="5dp"
android:text="系統時間"
android:textColor="@color/white"
android:textSize="14sp" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_player_top_control"
android:gravity="center_vertical"
android:orientation="horizontal">
<ImageView
android:id="@+id/iv_voice"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/selector_btn_voice" />
<SeekBar
android:id="@+id/sb_volume"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginRight="25dp"
android:maxHeight="6dp"
android:minHeight="6dp"
android:progressDrawable="@drawable/video_progress_drawable"
android:thumb="@mipmap/progress_thumb" />
</LinearLayout>
</LinearLayout>
底部控制面板佈局:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_player_bottom_seekbar"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingLeft="5dp"
android:paddingRight="5dp">
<TextView
android:id="@+id/tv_current_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="00:00"
android:textColor="@color/white"
android:textSize="14sp" />
<SeekBar
android:id="@+id/sb_Video"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
android:layout_weight="1"
android:maxHeight="6dp"
android:minHeight="6dp"
android:progressDrawable="@drawable/video_progress_drawable"
android:thumb="@mipmap/progress_thumb" />
<TextView
android:id="@+id/tv_total_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="總時間"
android:textColor="@color/white"
android:textSize="14sp" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_player_bottom_control"
android:gravity="center"
android:orientation="horizontal">
<RelativeLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1">
<ImageView
android:id="@+id/iv_exit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:background="@drawable/selector_btn_exit" />
</RelativeLayout>
<RelativeLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1">
<ImageView
android:id="@+id/iv_pre"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:background="@drawable/selector_btn_pre" />
</RelativeLayout>
<RelativeLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1">
<ImageView
android:id="@+id/iv_play"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:background="@drawable/selector_btn_pause" />
</RelativeLayout>
<RelativeLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1">
<ImageView
android:id="@+id/iv_next"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:background="@drawable/selector_btn_next" />
</RelativeLayout>
<RelativeLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1">
<ImageView
android:id="@+id/iv_screen"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:background="@drawable/selector_btn_fullscreen" />
</RelativeLayout>
</LinearLayout>
</LinearLayout>
至此,視頻播放器頁面自定義的佈局就弄好了。
至於播放頁面邏輯,有很多很多,準備在下一篇博文中在去整理。先放一張MediaPlayer的類圖:瞭解一下音視頻在播放前中後各個方法的調用。
補充
引入庫工程
android studio在導入外部庫工程的時候,網上有很多方法,我是這樣做的:就以vitamio爲例,將下載好的壓縮解壓,找到vitamio文件夾,然後整體複製到android studio的工作區間中,clean一下project,這個資源庫會報錯,應該是編譯版本的問題,打開vitamio的build.gradle,修改裏面的編譯的sdk版本,就可以了,附張圖吧
引用.9圖片
在上面很長很長的佈局文件中,如果仔細看的話,會發現在引用資源圖片時,有的是@mipmap,有的是@drawable,是這樣的:在android studio下引用的 .9 圖片放在mipmap文件夾下面不能被引用,我的做法是,新建了一個drawable-xhdpi文件夾,將.9圖片放進去,就能正常引用了。自定義SeekBar樣式:
<SeekBar
android:id="@+id/sb_volume"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginRight="25dp"
android:maxHeight="6dp"
android:minHeight="6dp"
android:progressDrawable="@drawable/video_progress_drawable"
android:thumb="@mipmap/progress_thumb" />
thumb就是進度條上的那個小圓點的圖片
video_progress_drawable代碼如下:
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<!--SeekBar的背景-->
<item
android:id="@android:id/background"
android:drawable="@drawable/progress_background">
</item>
<!--SeekBar第二級進度的樣式-->
<item android:id="@android:id/secondaryProgress">
<clip>
<shape>
<corners android:radius="5dip" />
<solid android:color="#666" />
</shape>
</clip>
</item>
<!--SeekBar進度的樣式-->
<item
android:id="@android:id/progress"
android:drawable="@mipmap/video_progress">
</item>
</layer-list>