Android多媒体开发
Android多媒體開發(fā)系列文章
一、什么是多媒體
多媒體(duō méi tǐ) 的英文單詞是Multimedia,它由media和multi兩部分組成。一般理解為多種媒體的綜合
多媒體是計算機和視頻技術的結合,實際上它是兩個媒體;聲音和圖像,或者用現在的術語:音響和電視。
多媒體(Multimedia),在計算機系統(tǒng)中,組合兩種或兩種以上媒體的一種人機交互式信息交流和傳播媒體
使用的媒體包括文字、圖片、照片、聲音 (包含音樂、語音旁白、特殊音效)、動畫和影片,以及程式所提供的互動功能
多媒體是超媒體(Hypermedia)系統(tǒng)中的一個子集,而超媒體系統(tǒng)是使用超鏈接 (Hyperlink)構成的全球信息系統(tǒng)
全球信息系統(tǒng)是因特網上使用 TCP/IP 協議和 UDP/IP 協議
二、音樂播放器
Android 官方提供了MediaPlayer 核心類,用于播放音樂,其狀態(tài)流程如下圖所示。MediaPlayer 必須嚴格按照狀態(tài)圖操作,否則就會出現錯誤,這些錯誤都是底層拋出,嚴格按照狀態(tài)圖操作的話一般就不會出問題。
MediaPlayer,原生的API,可以播放音視頻,但是支持的格式比較少,實際開發(fā)中用的比較少,但是還是很有必要學習,熟悉API,因為Vitamio框架的API大部分跟原生的API是一樣的
1、MediaPlayer使用流程圖
2、MediaPlayer核心方法
| create() | 播放本地res/raw/目錄下的資源 |
| reset() | 重置為初始狀態(tài) |
| setAudioStreamType() | 設置音樂格式,例如:AudioManager.STREAM_MUSIC |
| setDataSource() | 設置音頻源,本地網絡資源均可 |
| prepare() | 播放前的準備工作 |
| prepareAsync() | 異步進行準備工作,播放網絡音頻的時候使用 |
| start() | 開始或恢復播放 |
| pause() | 暫停播放 |
| stop() | 停止播放 |
| release() | 釋放資源 |
| getDuration() | 獲取音樂最大長度(毫秒單位) |
| getCurrentPosition() | 獲取當前的播放進度 |
| seekTo() | 拖拽進度 |
| setDisplay() | 設置輸出畫面 |
| setOnPreparedListener() | 設置準備監(jiān)聽 |
為了演示MediaPlayer 的使用,我們需要提前準備一個mp3 文件放到sdcard 中
需求:制作一個播放器,能夠播放/暫停/停止音樂文件,并且添加一個SeekBar(可以拖拽的ProgressBar),當音樂播放時SeekBar 也會不斷的跟新當前的進度,當用戶拖動SeekBar 時可以更改播放的進度
布局文件
<?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="match_parent"android:orientation="vertical"><LinearLayout android:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="horizontal"><Button android:layout_width="0dp"android:layout_height="wrap_content"android:layout_weight="1"android:onClick="play"android:text="播放"/><Button android:layout_width="0dp"android:layout_height="wrap_content"android:layout_weight="1"android:onClick="pause"android:text="暫停"/><Button android:layout_width="0dp"android:layout_height="wrap_content"android:layout_weight="1"android:onClick="stop"android:text="停止"/></LinearLayout><SeekBar android:id="@+id/sb"android:layout_width="match_parent"android:layout_height="wrap_content"/> </LinearLayout>代碼實現
public class MainActivity extends Activity implements OnSeekBarChangeListener {private SeekBar sb;private MediaPlayer player;private int duration;// 播放器的幾個狀態(tài)private static final int PLAYING = 1;// 播放狀態(tài)private static final int PAUSING = 2;// 暫停狀態(tài)private static final int STOPPING = 3;// 停止狀態(tài)private volatile int CURRENT = 0;// 當前狀態(tài)private Timer timer;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);sb = (SeekBar) findViewById(R.id.sb);//設置拖動監(jiān)聽sb.setOnSeekBarChangeListener(this);}//播放public void play(View view) {if (player != null) {if (CURRENT == PLAYING) {Toast.makeText(this, "音樂已經在播放了", Toast.LENGTH_SHORT).show();return;} else if (CURRENT == PAUSING) {player.start();CURRENT = PLAYING;return;}}try {//創(chuàng)建一個播放器對象player = new MediaPlayer();//給播放器設置音樂路徑player.setDataSource("/mnt/sdcard/test.mp3");//設置音樂格式player.setAudioStreamType(AudioManager.STREAM_MUSIC);//準備player.prepare();//獲取音樂最大長度(毫秒單位)duration = player.getDuration();//給SeekBar 設置最大值sb.setMax(duration);//音樂開始播放player.start();//設置當前的狀態(tài)為播放CURRENT = PLAYING;if (timer == null) {//創(chuàng)建定時器timer = new Timer();}/*** 參數1:匿名內部類,相當于Runnable 類* 參數2:第一次延時多長時間(毫秒)后執(zhí)行,0 則代表立即執(zhí)行* 參數3:每隔多長時間(毫秒)執(zhí)行一次*/timer.schedule(new TimerTask() {@Overridepublic void run() {//該方法每1 秒被調用一次if (CURRENT == PLAYING) {runOnUiThread(new Runnable() {@Overridepublic void run() {//雙重判斷,盡可能避免線程問題,因為該段代碼時在主線程中的,//第一次判斷是在子線程中進行的if (player != null && CURRENT == PLAYING) {//獲取當前的播放進度int currentPosition = player.getCurrentPosition();//設置給SeekBarsb.setProgress(currentPosition);}}});}}}, 0, 1000);} catch (Exception e) {e.printStackTrace();Toast.makeText(this, "音樂播放失敗" + e, 0).show();}}/*** 暫停*/public void pause(View view) {if (player != null && CURRENT == PLAYING) {player.pause();CURRENT = PAUSING;}}/*** 停止*/public void stop(View view) {if (player != null) {if (CURRENT == PLAYING || CURRENT == PAUSING) {CURRENT = STOPPING;//取消定時器timer.cancel();timer = null;player.stop();player.reset();player.release();player = null;sb.setProgress(0);}}}/** 拖動過程中回調多次*/@Overridepublic void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {if (player == null) {sb.setProgress(0);} else {player.seekTo(progress);}}/** 開始拖動前回調一次*/@Overridepublic void onStartTrackingTouch(SeekBar seekBar) {if (player == null) {Toast.makeText(this, "音樂播放器還未開始", Toast.LENGTH_SHORT).show();}}/** 結束拖動后回調一次*/@Overridepublic void onStopTrackingTouch(SeekBar seekBar) {}@Overrideprotected void onDestroy() {super.onDestroy();stop(null);}}3、播放本地res/raw/目錄下的資源
MediaPlayer mediaPlayer = MediaPlayer.create(context, R.raw.sound_file_1); mediaPlayer.start(); // no need to call prepare(); create() does that for you4、播放本地URI資源
Uri myUri = ....; // initialize Uri here MediaPlayer mediaPlayer = new MediaPlayer(); mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); mediaPlayer.setDataSource(getApplicationContext(), myUri); mediaPlayer.prepare(); mediaPlayer.start();5、播放網絡資源
String url = "http://........"; // your URL here MediaPlayer mediaPlayer = new MediaPlayer(); mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); mediaPlayer.setDataSource(url); mediaPlayer.prepare(); // might take long! (for buffering, etc) mediaPlayer.start();6、異步準備
String url = "http://........"; // your URL hereMediaPlayer mediaPlayer = new MediaPlayer();mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);mediaPlayer.setDataSource(url);mediaPlayer.prepareAsync();mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {@Overridepublic void onPrepared(MediaPlayer mp) {mp.start();}});7、在后臺Service異步執(zhí)行播放任務
public class MyService extends Service implements MediaPlayer.OnPreparedListener {MediaPlayer mMediaPlayer;private static final String ACTION_PLAY = "com.example.action.PLAY";MediaPlayer mMediaPlayer = null;public int onStartCommand(Intent intent, int flags, int startId) {...if (intent.getAction().equals(ACTION_PLAY)) {mMediaPlayer = ... // initialize it heremMediaPlayer.setOnPreparedListener(this);mMediaPlayer.prepareAsync(); // prepare async to not block main thread}}/** Called when MediaPlayer is ready */public void onPrepared(MediaPlayer player) {player.start();} }public void initMediaPlayer() {// ...initialize the MediaPlayer here...mMediaPlayer.setOnErrorListener(this);}@Overridepublic boolean onError(MediaPlayer mp, int what, int extra) {// ... react appropriately ...// The MediaPlayer has moved to the Error state, must be reset!}8、在手機睡眠時使用喚醒鎖
mMediaPlayer = new MediaPlayer(); // ... other initialization here ... mMediaPlayer.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK);//wifi鎖 WifiLock wifiLock = ((WifiManager) getSystemService(Context.WIFI_SERVICE)).createWifiLock(WifiManager.WIFI_MODE_FULL, "mylock");wifiLock.acquire();//當暫停,不再需要網絡時釋放鎖 wifiLock.release();9、在前臺服務運行播放任務
String songName; // assign the song name to songName PendingIntent pi = PendingIntent.getActivity(getApplicationContext(), 0,new Intent(getApplicationContext(), MainActivity.class),PendingIntent.FLAG_UPDATE_CURRENT); Notification notification = new Notification(); notification.tickerText = text; notification.icon = R.drawable.play0; notification.flags |= Notification.FLAG_ONGOING_EVENT; notification.setLatestEventInfo(getApplicationContext(), "MusicPlayerSample","Playing: " + songName, pi); startForeground(NOTIFICATION_ID, notification);
10、處理音頻焦點
AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); int result = audioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC,AudioManager.AUDIOFOCUS_GAIN);if (result != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {// could not get audio focus. }requestAudioFocus() 方法的第一個參數是一個 AudioManager.OnAudioFocusChangeListener,當任何時候音頻焦點發(fā)生變化的時候,會回調 OnAudioFocusChangeListener的onAudioFocusChange()方法
class MyService extends Serviceimplements AudioManager.OnAudioFocusChangeListener {// ....public void onAudioFocusChange(int focusChange) {// Do something based on focus change...} }11、播放完畢時手動釋放資源
public class MyService extends Service {MediaPlayer mMediaPlayer;// ...@Overridepublic void onDestroy() {if (mMediaPlayer != null) mMediaPlayer.release();} }三、Mp3文件簡介
ID3
一般是位于一個mp3文件的開頭或末尾的若干字節(jié)內,附加了關于該mp3的歌手,標題,專輯名稱,年代,風格等信息,該信息就被稱為ID3信息,ID3信息分為兩個版本,v1和v2版。 其中:v1版的ID3在mp3文件的末尾128字節(jié),以TAG三個字符開頭,后面跟上歌曲信息。 v2版一般位于mp3的開頭,可以存儲歌詞,該專輯的圖片等大容量的信息。
V1與V2
- ID3V1記錄在MP3文件的末尾,長度固定
- ID3V2就記錄在MP3文件的首部。 ID3V2一共有4個版本,但流行的播放軟件一般只支持第3版,既ID3v2.3。
- 對ID3V2的操作比ID3V1要慢。而且ID3V2結構比ID3V1的結構要復雜得多,但比ID3V1全面且可以伸縮和擴展。
四、視頻播放器
1、SurfaceView
SurfaceView提供了一個繪畫的界面,你可以控制該界面的格式和大小,SurfaceView 負責在屏幕正確位置安置一個界面。SurfaceView的其中一個目的,是為了在子線程渲染屏幕,但需要注意幾點
- 所有SurfaceView 和SurfaceHolder.Callback 的方法,都必須在主線程調用
- 必須保證繪制線程,必須在SurfaceView 有效的情況下才能使用,也就是在SurfaceHolder.Callback.surfaceCreated() 和SurfaceHolder.Callback.surfaceDestroyed()之間調用
SurfaceView是View的子類,等同于TextView、ImageView等一系列控件。核心功能可以通過子線程進行界面的繪制,繪制需要注意的內容:
所有SurfaceView和SurfaceHolder.Callback的方法都應該在UI線程里調用,一般來說就是應用程序主線程。渲染線程所要訪問的各種變量應該作同步處理。
由于surface可能被銷毀,它只在SurfaceHolder.Callback.surfaceCreated()和 SurfaceHolder.Callback.surfaceDestroyed()之間有效,所以要確保渲染線程訪問的是合法有效的surface
- 雙緩沖技術:內存中有兩個畫布,A畫布顯示至屏幕,B畫布在內存中繪制下一幀畫面,繪制完畢后B顯示至屏幕,A在內存中繼續(xù)繪制下一幀畫面
- SurfaceView 是雙緩沖機制,一個用來緩沖數據,另一個用來展現數據,可以提高數據展示的速度,單緩沖需要先加載數據再去展示數據,但是雙緩沖在展示第一個頁面數據的時候已經把第二個頁面的數據加載好了
- 對畫面的實時更新要求較高,重量級組件,可見時才創(chuàng)建
- SurfaceView一旦不可見,就會被銷毀,一旦可見,就會被創(chuàng)建,銷毀時停止播放,再次創(chuàng)建時再開始播放
- 播放視頻也是用MediaPlayer,不過跟音頻不同,要設置顯示在哪個SurfaceView
2、使用MediaPlayer+SurfaceView 播放視頻
在該節(jié)中,視頻播放依然使用MediaPlayer 類,為了方便演示,我們直接使用本文中創(chuàng)建的工程,只需在布局文件添加SurfaceView 控件即可
布局文件
<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:paddingBottom="@dimen/activity_vertical_margin"android:paddingLeft="@dimen/activity_horizontal_margin"android:paddingRight="@dimen/activity_horizontal_margin"android:paddingTop="@dimen/activity_vertical_margin"tools:context=".MainActivity"><SurfaceView android:id="@+id/sv"android:layout_width="match_parent"android:layout_height="match_parent"/></RelativeLayout>實現代碼
public class MainActivity extends Activity {private MediaPlayer player;static int currentPosition;private SurfaceView sv;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);sv = (SurfaceView) findViewById(R.id.sv);//拿到surfaceview的控制器final SurfaceHolder sh = sv.getHolder();// Thread t = new Thread(){// @Override// public void run() {// try {// sleep(200);// } catch (InterruptedException e) {// e.printStackTrace();// }// runOnUiThread(new Runnable() {// @Override// public void run() {// MediaPlayer player = new MediaPlayer();// player.reset();// try {// player.setDataSource("sdcard/2.3gp");// player.setDisplay(sh);// player.prepare();// player.start();// } catch (Exception e) {// e.printStackTrace();// } // // }// });// // }// };// t.start();//SurfaceView是重量級組件,可見時才會創(chuàng)建//給SurfaceHolder設置CallBack,類似于偵聽,可以知道SurfaceView的狀態(tài)sh.addCallback(new Callback() {//surfaceView銷毀時調用@Overridepublic void surfaceDestroyed(SurfaceHolder holder) {//每次surfaceview銷毀時,同時停止播放視頻if(player != null){currentPosition = player.getCurrentPosition();player.stop();player.release();player = null;}}//surfaceView創(chuàng)建時調用@Overridepublic void surfaceCreated(SurfaceHolder holder) {//每次surfaceView創(chuàng)建時才去播放視頻if(player == null){player = new MediaPlayer();player.reset();try {player.setDataSource("sdcard/2.3gp");player.setDisplay(sh);player.prepare();player.start();player.seekTo(currentPosition);} catch (Exception e) {e.printStackTrace();}}}//surfaceView結構改變時調用@Overridepublic void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {}});}}使用VideoView 播放視頻
VideoView 跟MediaPlayer 相比播放視頻步驟要簡單的多,因為VideoView 原生提供了播放,暫停、快進、快退、進度條等方法。使用起來要方便的很多
1、設置布局文件,布局文件比較簡單,因此這里只給你VideoView 標簽
<VideoView android:id="@+id/vv"android:layout_width="match_parent"android:layout_height="match_parent" />2、設置VideoView 的播放文件路徑和媒體控制器,調用start 方法即可播放媒體文件
//實例化VideoView 對象 vv = (VideoView) findViewById(R.id.vv); //從界面獲取播放路徑 et_path = (EditText) findViewById(R.id.et_path);//給VideoView 設置視頻路徑 vv.setVideoPath(et_path.getText().toString()); //設置VideoView 控制器,我們當前類實現了MediaPlayerControl 接口 vv.setMediaController(new MediaController(this)); //開始播放vv.start();//設置當前播放器窗口設置為焦點vv.requestFocus();3、覆寫MediaPlayerControl 接口中的抽象方法
@Overridepublic void start() {}@Overridepublic void pause() {}@Overridepublic int getDuration() {return 0;}@Overridepublic int getCurrentPosition() {return 0;}@Overridepublic void seekTo(int pos) {}@Overridepublic boolean isPlaying() {return false;}@Overridepublic int getBufferPercentage() {return 0;}@Overridepublic boolean canPause() {return false;}@Overridepublic boolean canSeekBackward() {return false;}@Overridepublic boolean canSeekForward() {return false;}@Overridepublic int getAudioSessionId() {return 0;}@Overridepublic boolean onKeyDown(int keyCode, KeyEvent event) {return super.onKeyDown(keyCode, event);}@Overridepublic boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) {return super.onKeyMultiple(keyCode, repeatCount, event);}注意:上面的方法都是回調方法,我們可以在這些方法里面實現我們的業(yè)務邏輯。只有當我們給VideoView設置setMediaController 后控制器才會出現
收音機
- 播放協議: MMS
- MMS(Microsoft Media Server protocol)是一種串流媒體傳送協議 ,android并不支持這種流媒體協議
引入Vitamo框架進行播放
- 核心類:io.vov.vitamio.MediaPlayer
- 操作:同系統(tǒng)的MediaPlayer,代碼編寫與播放網絡音樂相近
視頻處理
電影文件有很多基本的組成部分。首先,文件本身被稱為容器Container,容器的類型決定了信息被存放在文件中的位置。AVI和Quicktime就是容器的例子。接著,你有一組流,例如,你經常有的是一個音頻流和一個視頻流。(一個流只是一種想像出來的詞語,用來表示一連串的通過時間來串連的數據元素)。在流中的數據元素被稱為幀Frame。每個流是由不同的編碼器來編碼生成的。編解碼器描述了實際的數據是如何被編碼Coded和解碼DECoded的,因此它的名字叫做CODEC。接著從流中被讀出來的叫做包Packets。包是一段數據,它包含了一段可以被解碼成方便我們最后在應用程序中操作的原始幀的數據。
七個模塊分別為:讀文件模塊,解復用模塊 ,視頻解碼模塊,音頻解碼音頻,顏色空間轉換模塊,視頻顯示模塊,音頻播放模塊
粗略的分為五類,分別是 Source filer, Demux flter, Decoder filter, Color Space converter filter,Render filter,各類 filter的功能與作用簡述如下
Source filter
Source filter 源過濾器的作用是為下級 demux filter 以包的形式源源不斷的提供數據流。在通常情況下,我們有多種方式可以獲得數據流,一種是從本地文件中讀取,一種是從網上獲取,Sourcefilter 另外一個作用就是屏蔽讀本地文件和獲取網絡數據的差別,在下一級的 demux filter 看來,本地文件和網絡數據是一樣的。
Demux filter
解復用過濾器的作用是識別文件類型,媒體類型,分離出各媒體原始數據流,打上時鐘信息后送給下級 decoder filter。為識別出不同的文件類型和媒體類型,常規(guī)的做法是讀取一部分數據,然后遍歷解復用過濾器支持的文件格式和媒體數據格式,做匹配來確定是哪種文件類型,哪種媒體類型;有些媒體類型的原始數據外面還有其他的信息,比如時間,包大小,是否完整包等等。demux filter 解析數據包后取出原始數據,有些類型的媒體不管是否是完整包都立即送往下級 decoder filter,有些類型的媒體要送完整數據包,此時可能有一些數據包拼接的動作;當然時鐘信息的計算也是 demux filter 的工作內容,這個時鐘用于各媒體之間的同步。在本例中,AVI Splitter 是 Demux filter。
Decoder filter
解碼過濾器的作用就是解碼數據包,并且把同步時鐘信息傳遞下去。對視頻媒體而言,通常是解碼成 YUV 數據,然后利用顯卡硬件直接支持 YUV 格式數據 Overlay 快速顯示的特性讓顯卡極速顯示。YUV格式是一個統(tǒng)稱,常見的有 YV12,YUY2,UYVY 等等。有些非常古老的顯卡和嵌入式系統(tǒng)不支持 YUV 數據顯示,那就要轉換成 RGB 格式的數據,每一幀的每一個像素點都要轉換,分別計算 RGB 分量,并且因為轉換是浮點運算,雖然有定點算法,還是要耗掉相當一部分 CPU,總體上效率底下;對音頻媒體而言,通常是解碼成 PCM 數據,然后送給聲卡直接輸出。在本例中,AVI Decompress 和 ACM Warper 是 decoder filter。
Color space converter filter
顏色空間轉換過濾器的作用是把視頻解碼器解碼出來的數據轉換成當前顯示系統(tǒng)支持的顏色格式。通常視頻解碼器解碼出來的是 YUV 數據,PC 系統(tǒng)是直接支持 YUV 格式的,也支持 RGB 格式,有些嵌入式系統(tǒng)只支持 RGB 格式的。在本例中,視頻解碼器解碼出來的是 RGB8 格式的數據,Color space converter filter 把 RGB8 轉換成 RGB32 顯示。
Render filter
渲染過濾器的作用就是在適當的時間渲染相應的媒體,對視頻媒體就是直接顯示圖像,對音頻就是播放聲音。視音頻同步的策略方法有好幾種,其中最簡單的一種就是默認視頻和音頻基準時間相同,這時音頻可以不打時鐘信息,通過計算音頻的采樣頻率,量化 bit 數,聲道數等基本參數就知道音頻 PCM 的數據速率,按照這個速率往前播放即可;視頻必須要使用同步時鐘信息來決定什么時候顯示。DirectShow 采用一個有序鏈表 ,把接收到的數據包放進有序鏈表中,啟動一個定時器,每次定時器時間到就掃描鏈表,比較時鐘信息,或者顯示相應的幀,或者什么也不做,每次接收到新的數據幀,首先判斷時鐘信息,如果是歷史數據幀就丟棄,如果是將來顯示數據幀就進有序鏈表,如果當前時間幀就直接顯示。如此這樣,保持視頻和音頻在人體感覺誤差范圍內相對的動態(tài)同步。在本例中 VideoRender 和 Default DirectSound Device 是 Render filter,同時也是 Sink filter
JetPlayer
JetPlayer jetPlayer = JetPlayer.getJetPlayer(); jetPlayer.loadJetFile("/sdcard/level1.jet"); byte segmentId = 0;// queue segment 5, repeat once, use General MIDI, transpose by -1 octave jetPlayer.queueJetSegment(5, -1, 1, -1, 0, segmentId++); // queue segment 2 jetPlayer.queueJetSegment(2, -1, 0, 0, 0, segmentId++);jetPlayer.play();TextureView
TextureView、Surfaceview、SurfaceTexture、GLSurfaceView
畫中畫
https://github.com/googlesamples/android-PictureInPicture
https://developer.android.google.cn/training/tv/playback/picture-in-picture.html?hl=zh-cn#handling_ui
總結
以上是生活随笔為你收集整理的Android多媒体开发的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Android数据存储与持久化
- 下一篇: Android多媒体开发:照相机