媒體播放器:MediaPlayer
Android 系統(tǒng)提供了幾種播放音頻和視頻的方式,其中最常用的就是 MediaPlayer,和其他功能組件一樣都有很多第三方框架提供更加豐富完備的功能,但是基本用法和時(shí)序基本都是參照 MediaPlayer 來設(shè)計(jì)的,本節(jié)就來看看 MediaPlayer 的使用方法。
1. MediaPlayer 的狀態(tài)
MediaPlayer 有一套完善的狀態(tài)機(jī),通常出現(xiàn)一些奇怪的報(bào)錯(cuò)或者 Crash 大概率就是狀態(tài)流轉(zhuǎn)出了問題,而市面上大多數(shù)的播放器也會(huì)遵循 Android 官方設(shè)計(jì)的這套狀態(tài)機(jī)來實(shí)現(xiàn)。首先看看所有的狀態(tài):
- Idle:
空閑態(tài),剛創(chuàng)建或者調(diào)用了reset()
之后的狀態(tài),此時(shí)不能進(jìn)行播放 - Initailized:
初始化態(tài),僅僅設(shè)置了媒體源,但還未進(jìn)行任何網(wǎng)絡(luò)資源的拉取或者媒體流的解析,此時(shí)仍然不能播放 - Preparing:
準(zhǔn)備中,觸發(fā)了媒體流的下載以及媒體流的解析,但均未完成,處于準(zhǔn)備中,尚不能進(jìn)行播放 - Prepared:
準(zhǔn)備好,已經(jīng)將媒體資源拉取并解析完成,隨時(shí)可以開始播放 - Started:
播放態(tài),在媒體資源準(zhǔn)備好之后,調(diào)用了start()
觸發(fā)了媒體的播放,則進(jìn)入視頻 / 音頻播放 - Paused:
暫停態(tài),這個(gè)很好理解,視頻 / 音頻播放暫停,此時(shí)可以隨時(shí)調(diào)用start()
繼續(xù)播放回到Started
狀態(tài) - PlaybackCompleted:
播放結(jié)束態(tài),視頻 / 音頻播放到結(jié)尾,自然結(jié)束 - Stoped:
停止態(tài),在播放或者暫停過程中主動(dòng)調(diào)用stop()
停止播放,注意它和暫停態(tài)不同,“Stoped”態(tài)不能直接回到播放態(tài);它和
播放結(jié)束態(tài)也不同,“Stoped”一定是由開發(fā)者主動(dòng)觸發(fā)的 - End:
釋放態(tài),播放器調(diào)用release()
觸發(fā)播放器資源的釋放,此時(shí)播放器資源被回收將不能使用 - Error:
錯(cuò)誤態(tài),如果由于某種原因 MediaPlayer 出現(xiàn)了錯(cuò)誤,會(huì)觸發(fā)OnErrorListener.onError()
事件,此時(shí) MediaPlayer 即進(jìn)入 Error 狀態(tài),及時(shí)捕捉并妥善處理這些錯(cuò)誤是很重要的,可以幫助我們及時(shí)釋放相關(guān)的軟硬件資源,也可以改善用戶體驗(yàn)。通過setOnErrorListener
可以設(shè)置該監(jiān)聽器。如果MediaPlayer進(jìn)入了Error狀態(tài),可以通過調(diào)用reset()來恢復(fù),使得MediaPlayer重新返回到 Idle 狀態(tài)。
下面可以對(duì)照著狀態(tài)看看官方給的狀態(tài)機(jī)流轉(zhuǎn)圖:
這個(gè)圖非常經(jīng)典,建議大家收藏此文章,今后使用 MediaPlayer 過程中出現(xiàn)任何問題都可以看看狀態(tài)機(jī)是否出現(xiàn)異常。
2. MediaPlayer 常用 API
使用 MediaPlayer 的 API 之前一定要先熟悉熟悉再熟悉上一小節(jié)的狀態(tài)機(jī)時(shí)序圖,否則盲目使用 API 會(huì)出現(xiàn)很多狀態(tài)錯(cuò)誤的異常發(fā)生。
setDataSource(FileDescriptor fd):
設(shè)置音頻 / 視頻資源地址
- isPlaying():
判斷當(dāng)前視頻 / 音頻是否正在播放 - seekTo(position):
直接跳轉(zhuǎn)到視頻 / 音頻的某個(gè)時(shí)間點(diǎn) - getCurrentPosition():
獲取當(dāng)前的播放進(jìn)度 - getDuration():
獲取媒體文件的總時(shí)長 - reset():
重置 MediaPlayer,此后會(huì)進(jìn)入 Idle 態(tài) - release():
釋放播放器,在不使用的時(shí)候調(diào)用,節(jié)省系統(tǒng)資源 - setVolume(float leftVolume, float rightVolume):
設(shè)置媒體音量 - selectTrack(int index):
設(shè)置媒體軌道 - getTrackInfo():
返回一個(gè)數(shù)組,包含所有的軌道信息
3. MediaPlayer 使用步驟
Android 系統(tǒng)為 MediaPlayer 適配了多種場景,也為不同的場景提供了不同的使用方式,但大體上有幾個(gè)步驟:
- 創(chuàng)建播放器
創(chuàng)建通常有兩種方法:
MediaPlayer mediaPlayer = MediaPlayer.create(this, R.raw.song);
MediaPlayer mediaPlayer = new MediaPlayer();
第一種方式直接在創(chuàng)建的時(shí)候傳入媒體流地址,而第二種僅僅是創(chuàng)建一個(gè)“Idle”態(tài)的閑置播放器
- 設(shè)置媒體源
Android 提供下面的方法設(shè)置媒體數(shù)據(jù)源
mediaPlayer.setDataSource(www.mc.com/mc.mp3);
如果在創(chuàng)建 MediaPlayer 的時(shí)候就設(shè)置了媒體文件,那么可以跳過這一步
- 開始播放
在設(shè)置好媒體源地址之后,就可以開始播放了:
mediaPlayer.start();
-
播放控制
在播放過程中可以調(diào)用一些控制 API 進(jìn)行播放狀態(tài)的控制 -
結(jié)束播放
調(diào)用stop()
可以結(jié)束播放,并且記得在不用的時(shí)候還要調(diào)用release()
4. 播放器使用示例
本節(jié)來用 MediaPlayer 實(shí)現(xiàn)一個(gè)簡單的播放器,并通過幾個(gè) API 來實(shí)現(xiàn)基本的播放控制。
4.1 MediaPlayer 的使用
首先看看 MainActivity:
package com.emercy.myapplication;
import android.app.Activity;
import android.media.MediaPlayer;
import android.os.Bundle;
import android.os.Handler;
import android.view.View;
import android.widget.Button;
import android.widget.SeekBar;
import android.widget.TextView;
import android.widget.Toast;
import java.util.concurrent.TimeUnit;
public class MainActivity extends Activity {
private Button b1, b2, b3, b4;
private MediaPlayer mediaPlayer;
private double startTime = 0;
private double finalTime = 0;
private Handler myHandler = new Handler();
private int forwardTime = 5000;
private int backwardTime = 5000;
private SeekBar seekbar;
private TextView tx1, tx2, tx3;
public static int oneTimeOnly = 0;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
b1 = (Button) findViewById(R.id.button);
b2 = (Button) findViewById(R.id.button2);
b3 = (Button) findViewById(R.id.button3);
b4 = (Button) findViewById(R.id.button4);
tx1 = (TextView) findViewById(R.id.textView2);
tx2 = (TextView) findViewById(R.id.textView3);
tx3 = (TextView) findViewById(R.id.textView4);
mediaPlayer = MediaPlayer.create(this, R.raw.video);
seekbar = (SeekBar) findViewById(R.id.seekBar);
seekbar.setClickable(false);
b2.setEnabled(false);
b1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
int temp = (int) startTime;
if ((temp + forwardTime) <= finalTime) {
startTime = startTime + forwardTime;
mediaPlayer.seekTo((int) startTime);
Toast.makeText(getApplicationContext(), "前進(jìn)5秒", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(getApplicationContext(), "前方還剩不到5秒", Toast.LENGTH_SHORT).show();
}
}
});
b2.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(getApplicationContext(), "Pausing sound", Toast.LENGTH_SHORT).show();
mediaPlayer.pause();
b2.setEnabled(false);
b3.setEnabled(true);
}
});
b3.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(getApplicationContext(), "音頻播放", Toast.LENGTH_SHORT).show();
mediaPlayer.start();
finalTime = mediaPlayer.getDuration();
startTime = mediaPlayer.getCurrentPosition();
if (oneTimeOnly == 0) {
seekbar.setMax((int) finalTime);
oneTimeOnly = 1;
}
tx2.setText(String.format("%d min, %d sec",
TimeUnit.MILLISECONDS.toMinutes((long) finalTime),
TimeUnit.MILLISECONDS.toSeconds((long) finalTime) -
TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes((long)
finalTime)))
);
tx1.setText(String.format("%d min, %d sec",
TimeUnit.MILLISECONDS.toMinutes((long) startTime),
TimeUnit.MILLISECONDS.toSeconds((long) startTime) -
TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes((long)
startTime)))
);
seekbar.setProgress((int) startTime);
myHandler.postDelayed(UpdateSongTime, 100);
b2.setEnabled(true);
b3.setEnabled(false);
}
});
b4.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
int temp = (int) startTime;
if ((temp - backwardTime) > 0) {
startTime = startTime - backwardTime;
mediaPlayer.seekTo((int) startTime);
Toast.makeText(getApplicationContext(), "后退5秒", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(getApplicationContext(), "后方還剩不到5秒", Toast.LENGTH_SHORT).show();
}
}
});
}
private Runnable UpdateSongTime = new Runnable() {
public void run() {
startTime = mediaPlayer.getCurrentPosition();
tx1.setText(String.format("%d min, %d sec",
TimeUnit.MILLISECONDS.toMinutes((long) startTime),
TimeUnit.MILLISECONDS.toSeconds((long) startTime) -
TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.
toMinutes((long) startTime)))
);
seekbar.setProgress((int) startTime);
myHandler.postDelayed(this, 100);
}
};
}
通過MediaPlayer.create(this, R.raw.video)
創(chuàng)建一個(gè) MediaPlayer 對(duì)象并初始化媒體流地址,然后分別設(shè)置幾個(gè)控制 Button 的監(jiān)聽事件,實(shí)現(xiàn)播放器的前進(jìn)、后退、播放、暫停操作,當(dāng)中還有一個(gè)UpdateSongTime
的 Runnable 變量,用來每隔 100 毫秒更新一次播放進(jìn)度,實(shí)現(xiàn)播放進(jìn)度的同步刷新。
4.2 布局文件
布局文件就按照片一般播放器的擺放方式就可以,上面通常是頁面的主題和描述,下方就是進(jìn)度條、歌曲名稱時(shí)長等等。
<?xml version="1.0" encoding="utf-8"?>
<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:padding="30dp"
tools:context=".MainActivity">
<TextView
android:id="@+id/textview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"
android:text="音樂播放器"
android:textSize="35dp" />
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/textview"
android:layout_centerHorizontal="true"
android:text="慕課Android教程"
android:textColor="#ff7aff24"
android:textSize="35dp" />
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_alignParentBottom="true"
android:text="前進(jìn)" />
<Button
android:id="@+id/button2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_toRightOf="@id/button"
android:text="暫停" />
<Button
android:id="@+id/button3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@+id/button2"
android:layout_toEndOf="@+id/button2"
android:text="播放" />
<Button
android:id="@+id/button4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@+id/button3"
android:layout_toEndOf="@+id/button3"
android:layout_toRightOf="@+id/button3"
android:text="后退" />
<SeekBar
android:id="@+id/seekBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@+id/button"
android:layout_alignStart="@+id/textview"
android:layout_alignEnd="@+id/textview" />
<TextView
android:id="@+id/textView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@+id/seekBar"
android:text="超哥音樂選集"
android:textAppearance="?android:attr/textAppearanceSmall" />
<TextView
android:id="@+id/textView3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@+id/seekBar"
android:layout_alignEnd="@+id/button4"
android:text="02:23"
android:textAppearance="?android:attr/textAppearanceSmall" />
<TextView
android:id="@+id/textView4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/textView2"
android:layout_alignBottom="@+id/textView2"
android:layout_centerHorizontal="true"
android:text="超哥吉他"
android:textAppearance="?android:attr/textAppearanceMedium" />
</RelativeLayout>
當(dāng)然也可以按照你自己的設(shè)計(jì)去擺放播放器的布局樣式,編譯之后如下:
5. 小結(jié)
本節(jié)學(xué)習(xí)了 Android 內(nèi)置的最常用的播放器,最關(guān)鍵的是要熟悉它的狀態(tài)機(jī)時(shí)序圖,因?yàn)榭赡茉趯?shí)際開發(fā)中你會(huì)使用更強(qiáng)大的第三方播放器,但是基本時(shí)序仍然是參照 Android 官方設(shè)計(jì)的,在了解時(shí)序之后就可以按照本節(jié)的步驟使用各種 API 來播放音視頻了。在掌握了本節(jié)內(nèi)容之后,如果感興趣也可以研究研究市面上常用的開源播放器,可以讓你對(duì)播放器有更深的理解。