實驗目的
- 學會使用MediaPlayer
- 學習RxJava,使用RxJava更新UI
- 學會使用Service進行背景工作
- 學會使用Service與Activity進行通信
效果
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsISWXFmb1kmTDZVaRNkVD5kRWlnT0UVaOVTVp5kRWNUTDZVaOVTVp5kRWNlTtxmMaZ3ZEpFc502YrVzVZZ3aU10dJpHT5Z1RkpnRXJmd4cVY1l0Vk9mUYFmb1kmY2RWMaVHbyEWdG12U2RjMihFZtJGc01mYoBHMMRXOykVdR5mYsJlbiZnTtNGbOhFZpZFShBDbyoVdjhVW5lTeMZTTINGMShUYvwlbj5yZtlmbkN3YuQnclZnbvN2Ztl2Lc9CX6MHc0RHaiojIsJye.jpg)
RxJava簡介
GitHub位址:
ReactiveX團隊
RxJava
RxAndroid
RxJava
是主體,其實還有
RxAndroid
、
RxGo
、
RxPY
、
RxSwift
等适配
這裡使用的是
RxAndroid
和
RxJava
RxAndroid并不包括全部的RxJava,而是側重Android的特性進行添加,是以防止缺少依賴庫,還是都得使用
其它代碼環境
這裡使用的是 Android手機應用開發(八) | 制作簡單音樂播放 的大部分代碼
隻是将更新UI的操作從
Handler
更改為
RxJava
注冊服務 AndroidManifest.xml
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="wang.janking.mymusic">
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application
...
<service
android:name=".MusicService"
android:exported="true" />
...
</application>
</manifest>
建立服務 MusicService.java
MusicService.java
package wang.janking.mymusic;
import android.app.Service;
import android.content.Intent;
import android.media.MediaPlayer;
import android.net.Uri;
import android.os.Binder;
import android.os.Environment;
import android.os.IBinder;
import android.os.Parcel;
import android.os.RemoteException;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import android.widget.Toast;
import java.io.IOException;
public class MusicService extends Service {
public final IBinder binder = new MyBinder();
private MediaPlayer mp = new MediaPlayer();
private int isFinish = -1;
private final int PLAY_CODE = 1, STOP_CODE = 2, SEEK_CODE = 3, NEWMUSIC_CODE = 4, CURRENTDURATION_CODE = 5, TOTALDURATION = 6;
@Nullable
@Override
public IBinder onBind(Intent intent) {
try {
mp.setDataSource(Environment.getExternalStorageDirectory() + "/data/山高水長.mp3");
mp.prepare();
mp.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mp) {
isFinish = 1;
}
});
} catch (IOException e) {
Log.e("prepare error", "getService: " + e.toString());
}
return binder;
}
public class MyBinder extends Binder {
@Override
protected boolean onTransact(int code, @NonNull Parcel data, @Nullable Parcel reply, int flags) throws RemoteException {
switch (code) {
//service solve
case PLAY_CODE:
play_pause();
break;
case STOP_CODE:
stop();
break;
case SEEK_CODE:
mp.seekTo(data.readInt());
break;
case NEWMUSIC_CODE:
newMusic(Uri.parse(data.readString()));
reply.writeInt(mp.getDuration());
case TOTALDURATION:
reply.writeInt(mp.getDuration());
break;
case CURRENTDURATION_CODE:
reply.writeInt(mp.getCurrentPosition());
reply.writeInt(isFinish);
break;
}
return super.onTransact(code, data, reply, flags);
}
}
public void play_pause() {
if (mp.isPlaying()) {
mp.pause();
} else {
mp.start();
isFinish = -1;
}
}
public void stop() {
if (mp != null) {
mp.stop();
try {
mp.prepare();
mp.seekTo(0);
} catch (Exception e) {
Log.d("stop", "stop: " + e.toString());
}
}
}
public void newMusic(Uri uri){
try{
mp.reset();
mp.setDataSource(this, uri);
mp.prepare();
}
catch (Exception e){
Log.d("New Music", "new music: " + e.toString());
}
}
@Override
public void onDestroy() {
super.onDestroy();
if(mp!= null){
mp.reset();
mp.release();
}
}
}
布局檔案 activity_main.xml
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout 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=".MainActivity">
<de.hdodenhof.circleimageview.CircleImageView
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/profile_image"
android:layout_width="match_parent"
android:layout_height="290dp"
android:layout_marginTop="30dp"
android:src="@drawable/img"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/music_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="山高水長"
android:layout_marginTop="10dp"
android:textColor="@android:color/black"
app:layout_constraintTop_toBottomOf="@id/profile_image"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<TextView
android:id="@+id/music_singer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="中山大學合唱團"
android:layout_marginTop="3dp"
app:layout_constraintTop_toBottomOf="@id/music_title"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/playing_status"
android:orientation="horizontal"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
android:layout_marginTop="10dp"
app:layout_constraintTop_toBottomOf="@id/music_singer">
<TextView
android:id="@+id/current_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="00:00"/>
<SeekBar
android:id="@+id/seekbar"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginEnd="5dp"
android:layout_marginStart="5dp"
android:layout_weight="1" />
<TextView
android:id="@+id/total_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="99:99"/>
</LinearLayout>
<ImageButton
android:id="@+id/select_music"
android:src="@mipmap/file"
android:padding="0dp"
app:layout_constraintTop_toBottomOf="@id/playing_status"
android:layout_marginTop="20dp"
android:layout_marginStart="20dp"
android:scaleType="fitXY"
app:layout_constraintStart_toStartOf="parent"
android:layout_width="40dp"
android:layout_height="40dp"/>
<LinearLayout
app:layout_constraintTop_toBottomOf="@id/playing_status"
app:layout_constraintStart_toEndOf="@id/select_music"
android:layout_marginTop="30dp"
app:layout_constraintEnd_toStartOf="@id/quit"
android:layout_width="wrap_content"
android:orientation="horizontal"
android:layout_height="wrap_content">
<ImageButton
android:id="@+id/play_pause"
android:src="@mipmap/play"
android:padding="0dp"
android:scaleType="fitStart"
android:layout_width="50dp"
android:layout_marginEnd="30dp"
android:layout_height="50dp" />
<ImageButton
android:id="@+id/stop"
android:src="@mipmap/stop"
android:padding="0dp"
android:scaleType="fitXY"
android:layout_width="50dp"
android:layout_height="50dp" />
</LinearLayout>
<ImageButton
android:id="@+id/quit"
android:src="@mipmap/back"
android:padding="0dp"
android:layout_marginTop="20dp"
app:layout_constraintEnd_toEndOf="parent"
android:scaleType="fitXY"
android:layout_marginEnd="20dp"
app:layout_constraintTop_toBottomOf="@id/playing_status"
android:layout_width="40dp"
android:layout_height="40dp" />
</android.support.constraint.ConstraintLayout>
代碼檔案
package wang.janking.mymusic;
import android.Manifest;
import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.media.MediaMetadataRetriever;
import android.os.IBinder;
import android.os.Parcel;
import android.os.RemoteException;
import android.support.annotation.NonNull;
import android.support.v4.app.ActivityCompat;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.text.format.DateFormat;
import android.util.Log;
import android.view.KeyEvent;
import android.view.View;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.SeekBar;
import android.widget.TextView;
import org.reactivestreams.Subscriber;
import java.text.SimpleDateFormat;
import java.util.Date;
import io.reactivex.Observable;
import io.reactivex.ObservableEmitter;
import io.reactivex.ObservableOnSubscribe;
import io.reactivex.Observer;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.observers.DisposableObserver;
import io.reactivex.schedulers.Schedulers;
public class MainActivity extends AppCompatActivity {
//
private IBinder mBinder;
private final int PLAY_CODE = 1, STOP_CODE = 2, SEEK_CODE = 3, NEWMUSIC_CODE = 4, CURRENTDURATION_CODE = 5, TOTALDURATION = 6;
private int total_duration = 0;
//讀寫權限
private static String[] PERMISSIONS_STORAGE = {
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE};
//請求狀态碼
private static int REQUEST_PERMISSION_CODE = 1;
ImageView imageView;
SeekBar seekbar;
ImageButton play_pause, stop, select, quit;
TextView current_time, total_time, music_title, music_singer;
boolean isPlay = false;
boolean isStop = false;
private SimpleDateFormat time = new SimpleDateFormat("mm:ss");
private String time_format = "mm:ss";
private ServiceConnection sc = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
mBinder = service;
//與服務通信
Parcel data = Parcel.obtain();
Parcel reply = Parcel.obtain();
try {
mBinder.transact(TOTALDURATION, data, reply, 0);
}catch (Exception e){
Log.d("SERVICE CONNECTION", "onServiceConnected: " + e.toString());
}
total_duration = reply.readInt();
seekbar.setProgress(0);
seekbar.setMax(total_duration);
//兩種方法實作毫秒轉時間
//total_time.setText(time.format(new Date(ms.mp.getDuration())));
total_time.setText(DateFormat.format(time_format, total_duration));
current_time.setText(time.format(new Date(0)));
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
};
/*public Runnable myThread = new Runnable() {
@Override
public void run() {
Message msg = handler.obtainMessage();
if(isStop){
msg.what = -1;
handler.sendMessage(msg);
return;
}
try{
//與服務通信
Parcel data = Parcel.obtain();
Parcel reply = Parcel.obtain();
mBinder.transact(CURRENTDURATION_CODE, data, reply, 0);
msg.arg1 = reply.readInt();
}catch (Exception e){
Log.d("Run", "run: " + e.toString());
return;
}
handler.sendMessage(msg);
}
};
@SuppressLint("HandlerLeak")
private final Handler handler = new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what) { // 根據消息類型進行操作
case -1:
handler.removeCallbacks(myThread);
seekbar.setProgress(0);
current_time.setText(time.format(0));
imageView.setRotation(0);
break;
default:
if(msg.arg1 >= total_duration)
stop.performClick();
seekbar.setProgress(msg.arg1);
current_time.setText(time.format(new Date(msg.arg1)));
imageView.setPivotX(imageView.getWidth()/2);
imageView.setPivotY(imageView.getHeight()/2);//支點在圖檔中心
imageView.setRotation(msg.arg1/30);
handler.postDelayed(myThread, 1);
}
}
};*/
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ActivityCompat.requestPermissions(this, PERMISSIONS_STORAGE, REQUEST_PERMISSION_CODE);
play_pause = findViewById(R.id.play_pause);
stop = findViewById(R.id.stop);
select = findViewById(R.id.select_music);
quit = findViewById(R.id.quit);
seekbar = findViewById(R.id.seekbar);
imageView = findViewById(R.id.profile_image);
current_time = findViewById(R.id.current_time);
total_time = findViewById(R.id.total_time);
music_singer = findViewById(R.id.music_singer);
music_title = findViewById(R.id.music_title);
imageView.setPivotX(imageView.getWidth()/2);
imageView.setPivotY(imageView.getHeight()/2);//支點在圖檔中心
//設定一些UI
setSomething();
}
void setSomething(){
play_pause.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//與服務通信
Parcel data = Parcel.obtain();
Parcel reply = Parcel.obtain();
try{
mBinder.transact(PLAY_CODE, data, reply, 0);
}catch (RemoteException e){
Log.e("STOP:", "onClick: " + e.toString() );
}
if(isPlay){
isPlay = false;
play_pause.setImageResource(R.mipmap.play);
}
else {
isPlay = true;
isStop = false;
play_pause.setImageResource(R.mipmap.pause);
//訂閱觀察者
//......
}
}
});
stop.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
isPlay = false;
isStop = true;
play_pause.setImageResource(R.mipmap.play);
seekbar.setProgress(0);
current_time.setText(time.format(0));
imageView.setRotation(0);
//與服務通信
Parcel data = Parcel.obtain();
Parcel reply = Parcel.obtain();
try{
mBinder.transact(STOP_CODE, data, reply, 0);
}catch (RemoteException e){
Log.e("STOP:", "onClick: " + e.toString() );
}
}
});
seekbar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if(fromUser){
imageView.setPivotX(imageView.getWidth()/2);
imageView.setPivotY(imageView.getHeight()/2);//支點在圖檔中心
imageView.setRotation(progress/30);
current_time.setText(time.format(progress));
//與服務通信
Parcel data = Parcel.obtain();
Parcel reply = Parcel.obtain();
data.writeInt(progress);
try{
mBinder.transact(SEEK_CODE, data, reply, 0);
}catch (RemoteException e){
Log.e("STOP:", "onClick: " + e.toString() );
}
}
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
}
});
select.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("audio/*");
intent.addCategory(Intent.CATEGORY_OPENABLE);
startActivityForResult(intent,1);
}
});
quit.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
finish();
}
});
}
@Override
public void onDestroy(){
super.onDestroy();
if(sc != null){
unbindService(sc);
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQUEST_PERMISSION_CODE) {
Intent intent = new Intent(this, MusicService.class);
bindService(intent, sc, BIND_AUTO_CREATE);
}
}
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (data != null) {
try{
//與服務通信
Parcel send_data = Parcel.obtain();
Parcel reply = Parcel.obtain();
send_data.writeString(data.getData().toString());
try{
mBinder.transact(NEWMUSIC_CODE, send_data, reply, 0);
}catch (RemoteException e){
Log.e("STOP:", "onClick: " + e.toString() );
}
//設定資訊
total_duration = reply.readInt();
seekbar.setProgress(0);
seekbar.setMax(total_duration);
total_time.setText(DateFormat.format(time_format, total_duration));
current_time.setText(time.format(new Date(0)));
//設定歌曲資訊
MediaMetadataRetriever mmr = new MediaMetadataRetriever();
mmr.setDataSource(MainActivity.this,data.getData());
music_title.setText(mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE));
music_singer.setText(mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ARTIST));
byte[] picture = mmr.getEmbeddedPicture();
if(picture.length!=0){
Bitmap bitmap = BitmapFactory.decodeByteArray(picture, 0, picture.length);
imageView.setImageBitmap(bitmap);
}
mmr.release();
//自動播放
isPlay = false;
play_pause.performClick();
//不自動播放
//模拟停止
//stop.performClick();
}catch (Exception e){
Log.d("Open file", "onActivityResult: " + e.toString());
}
}
super.onActivityResult(requestCode, resultCode, data);
}
//點選傳回鍵
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode==KeyEvent.KEYCODE_BACK){
moveTaskToBack(true);
return false;
}
return super.onKeyDown(keyCode, event);
}
}
添加依賴
有兩種辦法
- 直接在
裡添加Build.gradle(Module app)
//這是截止到目前的最新版 implementation 'io.reactivex.rxjava2:rxandroid:2.1.0' implementation 'io.reactivex.rxjava2:rxjava:2.2.4'
- 下載下傳
Jar
包手動添加依賴
這個我試過,但是從一些小網站上下載下傳的缺少很多東西,而且很多jar包在CSDN上下載下傳要積分!!!知識怎麼能收費呢!
但是直接添加依賴代碼的方法可能會出現下載下傳失敗的情況,因為要經過谷歌的庫……
這樣子需要更改
Build.gradle(Module Project)
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
// maven庫
def cn = "http://maven.aliyun.com/nexus/content/groups/public/"
def abroad = "http://central.maven.org/maven2/"
// 先從url中下載下傳jar若沒有找到,則在artifactUrls中尋找
maven {
url cn
artifactUrls abroad
}
maven { url "http://maven.aliyun.com/nexus/content/repositories/jcenter"}
// 保留google源
google()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.1.4'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
// maven庫
def cn = "http://maven.aliyun.com/nexus/content/groups/public/"
def abroad = "http://central.maven.org/maven2/"
// 先從url中下載下傳jar若沒有找到,則在artifactUrls中尋找
maven {
url cn
artifactUrls abroad
}
maven { url "http://maven.aliyun.com/nexus/content/repositories/jcenter"}
// 保留google源
google()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
添加被觀察者(Observable)
現在新的版本不是簡單添加一個
Observable
就好,而是需要寄放在
CompositeDisposable
裡面
是以在
MainActivity.java
中添加兩個成員變量
//RxJAVA變量
private CompositeDisposable mCompositeDisposable = new CompositeDisposable();
private Observable<Integer> observable = Observable.create(new ObservableOnSubscribe<Integer>() {
@Override
public void subscribe(ObservableEmitter<Integer> observableEmitter) throws Exception {
while (true) {
//與服務通信
Parcel data = Parcel.obtain();
Parcel reply = Parcel.obtain();
try {
//擷取資訊
mBinder.transact(CURRENTDURATION_CODE, data, reply, 0);
//每隔一毫秒查詢一次歌曲的狀态
Thread.sleep(1);
} catch (Exception exception){
Log.d("SERVICE CONNECTION", "onServiceConnected: " + exception.toString());
}
//讀取目前進度
observableEmitter.onNext(reply.readInt());
//reply.readInt() == 1 讀取 isFinish判斷歌曲是否被動停止
// isStop 判斷歌曲是否主動停止
if(reply.readInt() == 1 || isStop)
break;
}
observableEmitter.onComplete();
}
});
這裡其實做的主要操作就是每隔一毫秒讀取歌曲的進度,然後調用
onNext()
讓觀察者更新UI,如果歌曲已經停止就調用
onComplete()
結束觀察過程
因為我想實作歌曲播放完畢自動回到起點,同時UI置位,是以這裡用了很多變量(如,,
isFinish
isStop
等)
但是我知道這些變量有備援,沒有精簡,但是功能是沒問題的!
添加觀察者(DisposableObserver)
現在的版本不是叫
Observer
了,而是加了個
Disposable
,表示可處理,就把它當做對事件的具體處理來了解就好了
更改
MainActivity.java
播放按鈕的監聽事件
//觀察者
play_pause.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//...
if(isPlay){
//...
}
else {
//...
//訂閱觀察者
DisposableObserver<Integer> disposableObserver = new DisposableObserver<Integer>() {
@Override
//設定UI
public void onNext(Integer integer) {
Log.d("onNext", "" + integer);
seekbar.setProgress(integer);
current_time.setText(time.format(new Date(integer)));
imageView.setPivotX(imageView.getWidth()/2);
imageView.setPivotY(imageView.getHeight()/2);//支點在圖檔中心
imageView.setRotation(integer/30);
}
//模拟點選停止按鍵
@Override
public void onComplete() {
stop.performClick();
}
@Override
public void onError(Throwable e) {
Log.d("onError", "" + e.toString());
}
};
//在新線程監聽
observable.subscribeOn(Schedulers.newThread())
//在主線程更新
.observeOn(AndroidSchedulers.mainThread())
//綁定
.subscribe(disposableObserver);
//管理DisposableObserver的容器
mCompositeDisposable.add(disposableObserver);
}
}
});
為什麼這個觀察者變量不像被觀察者一樣作為一個成員變量呢?
因為它們隻能訂閱一次!
這裡是每次點選播放按鈕就開始播放,并且開始監聽UI改變,然後歌曲播放完畢(或者點選停止按鈕)就調用
onComplete()
方法,那麼這對觀察者和被觀察者的生命也就終止了……
但是重新播放或者選擇新的歌曲的話會報錯
12-03 19:59:08.238 28919-28919/wang.janking.mymusic E/AndroidRuntime: FATAL EXCEPTION: main
Process: wang.janking.mymusic, PID: 28919
io.reactivex.exceptions.ProtocolViolationException: It is not allowed to subscribe with a(n) wang.janking.mymusic.MainActivity$1 multiple times. Please create a fresh instance of wang.janking.mymusic.MainActivity$1 and subscribe that to the target source instead.
是以要在每次需要監聽給的時候動态建立一個局部變量
disposableObserver
以後再調用的時候就又是一個新的變量了
銷毀
如果
Activity
要被銷毀時,我們的背景任務沒有執行完,那麼就會導緻
Activity
不能正常回收,而對于每一個
Observer
,都會有一個
Disposable
對象用于管理,而
RxJava
提供了一個
CompositeDisposable
類用于管理這些
Disposable
,我們隻需要将其将入到該集合當中,在
Activity
的
onDestroy
方法中,調用它的
clear
方法,就能避免記憶體洩漏的發生。
修改
onDestroy
方法
@Override
public void onDestroy(){
super.onDestroy();
//清除所有的觀察者
mCompositeDisposable.clear();
if(sc != null){
unbindService(sc);
}
}