说明
该项目实现两个应用交互,client端包含界面,界面上显示可播放的音乐名列表,底部包含上一首、下一首、播放、暂停的button。server端通过media provider提供本地音乐列表,提供播放、暂停、上一首下一首的功能。
本项目的不足之处在于我没写seekbar拖动条。
作为开发者完成本项目的步骤是:
- 确定需求和绘制UI界面,此处主要界面包括ListView和其他一些图片文字及button
- 完成客户端读取本地音乐list的功能,此处使用cursor从contentresolver()获取MediaStore的数据信息。
- AIDL实现客户端和服务端的通信。要注意此处需要传递parcelable自定义类型Music,此处需重点学习
- 按照功能调试完成系统,其他注意事项在详细过程中说明
一、UI界面绘制
activity_main.xml
主布局,我使用了LinearLayout嵌套RelativeLayout,下次我会使用ConstraintLayout,听说使用起来更为简单。
另外我使用了ListView展示歌曲列表,RecyclerView 或许是更为合适的选择,实际上我使用了它但是未能成功,下次一定
<?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"
>
<ListView
android:id="@+id/lv_music"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="11"
>
</ListView>
<RelativeLayout
android:id="@+id/btnGroup"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_gravity="center_horizontal"
android:layout_weight="1"
android:orientation="horizontal"
>
<ImageView
android:id="@+id/tv_icon"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_centerVertical="true"
android:src="@drawable/music"
/>
<TextView
android:id="@+id/tv_song"
android:layout_width="150dp"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:layout_marginTop="10dp"
android:layout_toRightOf="@+id/tv_icon"
android:scrollbars="vertical"
android:singleLine="true"
android:text="@string/song_name"
android:textSize="16sp"
android:textStyle="bold"
/>
<TextView
android:id="@+id/tv_singer"
android:layout_width="150dp"
android:layout_height="wrap_content"
android:layout_below="@+id/tv_song"
android:layout_alignLeft="@+id/tv_song"
android:layout_marginLeft="10dp"
android:scrollbars="vertical"
android:singleLine="true"
android:text="@string/singer_name"
android:textSize="14sp"
android:textStyle="bold"
/>
<Button
android:id="@+id/btn_last"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_centerVertical="true"
android:layout_toLeftOf="@+id/btn_play"
android:background="@drawable/last"
/>
<Button
android:id="@+id/btn_play"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_centerVertical="true"
android:layout_toLeftOf="@+id/btn_next"
android:background="@drawable/pause"
/>
<Button
android:id="@+id/btn_next"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:background="@drawable/next"
/>
</RelativeLayout>
</LinearLayout>
此处我使用了CardView做优化,实际上这个控件需要导包。方法是在此module的build.gradle中添加
implementation ‘androidx.cardview:cardview:1.0.0’
即可
item_music.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:layout_marginTop="10dp"
android:layout_marginRight="10dp"
app:cardBackgroundColor="@color/colorPink"
app:cardCornerRadius="10dp"
app:cardElevation="1dp"
app:contentPadding="10dp"
>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
>
<TextView
android:id="@+id/item_tv_num"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:text="1"
android:textSize="24sp"
android:textStyle="bold"
/>
<TextView
android:id="@+id/item_tv_song"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="20dp"
android:layout_toRightOf="@id/item_tv_num"
android:singleLine="true"
android:text="@string/song_name"
android:textSize="18sp"
android:textStyle="bold"
/>
<TextView
android:id="@+id/item_tv_singer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/item_tv_song"
android:layout_alignLeft="@id/item_tv_song"
android:layout_marginTop="10dp"
android:text="@string/singer_name"
android:textColor="#888"
android:textSize="14sp"
/>
<TextView
android:id="@+id/item_tv_line"
android:layout_width="2dp"
android:layout_height="20dp"
android:layout_alignTop="@+id/item_tv_singer"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:layout_toRightOf="@id/item_tv_singer"
android:background="#888"
/>
<TextView
android:id="@+id/item_tv_durtion"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/item_tv_singer"
android:layout_alignParentRight="true"
android:text="04:30"
android:textColor="#888"
android:textSize="14sp"
/>
</RelativeLayout>
</androidx.cardview.widget.CardView>
附上strings:
<resources>
<string name="app_name">musicClient</string>
<string name="song_name">昨日晴空</string>
<string name="singer_name">尤长靖</string>
</resources>
二、获取本地音乐列表
public static List<Music> getMusicData(Context context) {
Cursor cursor = context.getContentResolver().query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, null, null,
null, MediaStore.Audio.AudioColumns.IS_MUSIC);
if (cursor != null) {
while (cursor.moveToNext()) {
Music music = new Music();
music.song = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DISPLAY_NAME));
music.singer = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST));
music.path = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DATA));
music.duration = cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DURATION));
music.size = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.SIZE));
if (music.size > 1000 * 800) {
// 切割标题,分离出歌曲名和歌手 (本地媒体库读取的歌曲信息不规范)
if (music.song.contains("-")) {
String[] str = music.song.split("-");
music.singer = str[0];
music.song = str[1];
}
musicList.add(music);
}
}
cursor.close();
}
return musicList;
}
三、实现AIDL通信
IMyAidlInterface.aidl
// IMyAidlInterface.aidl
package com.example.musicservice;
import com.example.musicservice.Music;
// Declare any non-default types here with import statements
interface IMyAidlInterface {
/**
* Demonstrates some basic types that you can use as parameters
* and return values in AIDL.
*/
void startMusic();
void playMusic();
void pauseMusic();
void nextMusic();
void lastMusic();
void changeIndex(int index);
boolean getPlayState();
List<Music> getList();
String formatTime(int time);
}
Music.aidl
// Music.aidl
package com.example.musicservice;
// Declare any non-default types here with import statements
parcelable Music;
Muisc.class如下
package com.example.musicservice;
import android.os.Parcel;
import android.os.Parcelable;
/**
* Created by PsycheWang on 2021/10/19.
*/
public class Music implements Parcelable {
public String song = null;//歌名
public String singer = null;//歌手
public String path = null;//地址
public int duration = 0;//时长
public long size;//大小
public static final Creator<Music> CREATOR = new Creator<Music>() {
@Override
public Music createFromParcel(Parcel parcel) {
return new Music(parcel);
}
@Override
public Music[] newArray(int i) {
return new Music[0];
}
};
public Music() {
}
public Music(Parcel in) {
this.song = in.readString();
this.singer = in.readString();
this.path = in.readString();
this.duration = in.readInt();
this.size = in.readLong();
}
public String getSong() {
return song;
}
public void setSong(String song) {
this.song = song;
}
public String getSinger() {
return singer;
}
public void setSinger(String singer) {
this.singer = singer;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
public int getDuration() {
return duration;
}
public void setDuration(int duration) {
this.duration = duration;
}
public long getSize() {
return size;
}
public void setSize(long size) {
this.size = size;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel parcel, int i) {
parcel.writeString(this.song);
parcel.writeString(this.singer);
parcel.writeString(this.path);
parcel.writeInt(this.duration);
parcel.writeLong(this.size);
}
@Override
public String toString() {
return "Music{" +
"song='" + song + '\'' +
", singer='" + singer + '\'' +
", path='" + path + '\'' +
", duration=" + duration +
", size=" + size +
'}';
}
}
需要注意的是,AIDL使用必须将服务端的接口和类原封不动地copy到客户端,包括包名。
四、附上服务端代码
MyService.class如下
package com.example.musicservice;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.media.MediaPlayer;
import android.os.IBinder;
import android.os.RemoteException;
import android.provider.MediaStore;
import android.util.Log;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public class MyService extends Service {
private static final String TAG = "MyService";
private static List<Music> musicList = new ArrayList<>();
private int mIndex;
private static MediaPlayer mMediaPlayer = new MediaPlayer();
Context context;
public MyService() {
}
@Override
public void onCreate() {
super.onCreate();
context = this;
musicList = getMusicData(this);
}
@Override
public IBinder onBind(Intent intent) {
return new Mybinder();
}
class Mybinder extends IMyAidlInterface.Stub {
@Override
public void startMusic() throws RemoteException {
mMediaPlayer.reset();
try {
mMediaPlayer.setDataSource(musicList.get(mIndex).getPath());
mMediaPlayer.prepare();
mMediaPlayer.start();
} catch (IOException e) {
e.printStackTrace();
}
}
//点击播放按钮
@Override
public void playMusic() throws RemoteException {
if (mMediaPlayer != null) {
mMediaPlayer.start();
}
}
//点击暂停按钮
@Override
public void pauseMusic() throws RemoteException {
if (mMediaPlayer != null) {
mMediaPlayer.pause();
}
}
@Override
public void nextMusic() throws RemoteException {
if (mIndex == musicList.size() - 1) {
mIndex = 0;
} else {
mIndex++;
}
MyService.this.playmyMusic();
}
@Override
public void lastMusic() throws RemoteException {
if (mIndex == 0) {
mIndex = musicList.size() - 1;
} else {
mIndex--;
}
MyService.this.playmyMusic();
}
@Override
public void changeIndex(int index) throws RemoteException {
mIndex = index;
}
@Override
public boolean getPlayState() throws RemoteException {
if (mMediaPlayer.isPlaying()) {
return true;
} else {
return false;
}
}
@Override
public List<Music> getList() throws RemoteException {
for (Music music : musicList) {
Log.d(TAG, "getList: " + music.toString());
}
return musicList;
}
@Override
public String formatTime(int time) throws RemoteException {
if (time / 1000 % 60 < 10) {
return time / 1000 / 60 + ":0" + time / 1000 % 60;
} else {
return time / 1000 / 60 + ":" + time / 1000 % 60;
}
}
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (null != intent) {
Log.d(TAG, "onStartCommand接收到的数据是:" + intent.getStringExtra("data"));
}
return super.onStartCommand(intent, flags, startId);
}
public void playmyMusic() {
mMediaPlayer.reset();
try {
mMediaPlayer.setDataSource(musicList.get(mIndex).getPath());
mMediaPlayer.prepare();
mMediaPlayer.start();
} catch (IOException e) {
e.printStackTrace();
}
}
public static List<Music> getMusicData(Context context) {
Cursor cursor = context.getContentResolver().query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, null, null,
null, MediaStore.Audio.AudioColumns.IS_MUSIC);
if (cursor != null) {
while (cursor.moveToNext()) {
Music music = new Music();
music.song = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DISPLAY_NAME));
music.singer = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST));
music.path = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DATA));
music.duration = cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DURATION));
music.size = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.SIZE));
if (music.size > 1000 * 800) {
// 切割标题,分离出歌曲名和歌手 (本地媒体库读取的歌曲信息不规范)
if (music.song.contains("-")) {
String[] str = music.song.split("-");
music.singer = str[0];
music.song = str[1];
}
musicList.add(music);
}
}
cursor.close();
}
return musicList;
}
@Override
public void onDestroy() {
if (mMediaPlayer != null) {
mMediaPlayer.stop();
mMediaPlayer.release();
mMediaPlayer = null;
}
super.onDestroy();
Log.d(TAG, "onDestroy: 服务ondestroy");
}
}
MainActivity.class如下:
package com.example.musicservice;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import android.Manifest;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.util.Log;
public class MainActivity extends AppCompatActivity {
private static final String TAG = "serviceMainActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
/**
* Android6.0之后需要动态申请权限
*/
if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.READ_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED ||
ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(MainActivity.this, new String[]{
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE
}, 1);
}
Log.d(TAG, "onCreate: 开启应用");
startService(new Intent(this, MyService.class));
}
@Override
protected void onDestroy() {
super.onDestroy();
stopService(new Intent(this, MyService.class));
}
}
五、附上客户端代码
MAinActivity.class
package com.example.musicclient;
import androidx.appcompat.app.AppCompatActivity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.text.method.ScrollingMovementMethod;
import android.util.Log;
import android.view.View;
import android.widget.AdapterView;
import android.widget.Button;
import android.widget.ListView;
import android.widget.TextView;
import com.example.musicservice.IMyAidlInterface;
import com.example.musicservice.Music;
import com.example.musicservice.MyAdapter;
import java.util.ArrayList;
import java.util.List;
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private static final String TAG = "clientMainActivity";
Button mBtnPlay, mBtnNext, mBtnLast;
TextView mtvSinger, mtvSong;
ListView mlvMusic;
Context context;
public static IMyAidlInterface myAidlInterface;
private List<Music> list = new ArrayList<>();
final Intent intent = new Intent();
private int mIndex = 0;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
init();
context = this;
//ComponentName的参数1:目标app的包名,参数2:目标app的Service完整类名
intent.setComponent(new ComponentName("com.example.musicservice", "com.example.musicservice.MyService"));
bindService(intent, connection, BIND_AUTO_CREATE);
setListener();
}
ServiceConnection connection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
myAidlInterface = IMyAidlInterface.Stub.asInterface(iBinder);
if (myAidlInterface != null) {
try {
list = myAidlInterface.getList();
Log.d(TAG, "onServiceConnected: list.size() = " + list.size());
MyAdapter myAdapter = new MyAdapter(context, list);
mlvMusic.setAdapter(myAdapter);
} catch (RemoteException e) {
e.printStackTrace();
}
}
}
@Override
public void onServiceDisconnected(ComponentName componentName) {
myAidlInterface = null;
}
};
private void init() {
mBtnPlay = findViewById(R.id.btn_play);
mBtnLast = findViewById(R.id.btn_last);
mBtnNext = findViewById(R.id.btn_next);
mtvSinger = findViewById(R.id.tv_singer);
mtvSong = findViewById(R.id.tv_song);
mlvMusic = findViewById(R.id.lv_music);
mBtnPlay.setOnClickListener(this);
mBtnLast.setOnClickListener(this);
mBtnNext.setOnClickListener(this);
}
private void setListener() {
mlvMusic.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {
mIndex = i;
mtvSong.setText(list.get(mIndex).getSong() + "");
mtvSinger.setText(list.get(mIndex).getSinger() + "");
Log.d(TAG, "onItemClick: 播放歌曲" + mIndex);
try {
myAidlInterface.changeIndex(mIndex);
Log.d(TAG, "onItemClick: 传输索引" + mIndex);
myAidlInterface.startMusic();
mBtnPlay.setBackgroundResource(R.drawable.play);
} catch (RemoteException e) {
e.printStackTrace();
}
}
});
}
public void onClick(View view) {
switch (view.getId()) {
case R.id.btn_play:
try {
if (myAidlInterface.getPlayState()) {
//正在播放
myAidlInterface.pauseMusic();
Log.d(TAG, "onClick: pauseMusic");
mBtnPlay.setBackgroundResource(R.drawable.pause);
} else {
myAidlInterface.playMusic();
Log.d(TAG, "onClick: playMusic");
mBtnPlay.setBackgroundResource(R.drawable.play);
}
} catch (RemoteException e) {
e.printStackTrace();
}
break;
case R.id.btn_last:
if (mIndex == 0) {
mIndex = list.size() - 1;
} else {
mIndex--;
}
try {
mtvSong.setText(list.get(mIndex).getSong() + "");
mtvSinger.setText(list.get(mIndex).getSinger() + "");
Log.d(TAG, "onClick: last" + mIndex);
myAidlInterface.lastMusic();
Log.d(TAG, "onClick: lastMusic");
} catch (RemoteException e) {
e.printStackTrace();
}
break;
case R.id.btn_next:
if (mIndex == list.size() - 1) {
mIndex = 0;
} else {
mIndex++;
}
try {
mtvSong.setText(list.get(mIndex).getSong() + "");
mtvSinger.setText(list.get(mIndex).getSinger() + "");
Log.d(TAG, "onClick: next" + mIndex);
myAidlInterface.nextMusic();
} catch (RemoteException e) {
e.printStackTrace();
}
break;
}
}
@Override
protected void onDestroy() {
unbindService(connection);
super.onDestroy();
}
}
MyAdapter如下:
package com.example.musicservice;
import android.content.Context;
import android.os.RemoteException;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.TextView;
import com.example.musicclient.MainActivity;
import com.example.musicclient.R;
import java.util.ArrayList;
import java.util.List;
/**
* Created by PsycheWang on 2021/10/13.
*/
public class MyAdapter extends BaseAdapter {
private static final String TAG = "MyAdapter";
private Context context;
private List<Music> list;
public MyAdapter(Context context, List<Music> musicList) {
list = new ArrayList<>();
this.context = context;
this.list = musicList;
Log.d(TAG, "MyAdapter: " + list.size());
}
@Override
public int getCount() {
return list.size();
}
@Override
public Object getItem(int i) {
return list.get(i);
}
@Override
public long getItemId(int i) {
return i;
}
@Override
public View getView(int i, View view, ViewGroup viewGroup) {
ViewHolder holder;
if (view == null) {
view = View.inflate(context, R.layout.item_music, null);
}
if (view.getTag() != null) {
holder = (ViewHolder) view.getTag();
} else {
holder = new ViewHolder();
holder.song = (TextView) view.findViewById(R.id.item_tv_song);
holder.singer = (TextView) view.findViewById(R.id.item_tv_singer);
holder.duration = (TextView) view.findViewById(R.id.item_tv_durtion);
holder.position = (TextView) view.findViewById(R.id.item_tv_num);
}
view.setTag(holder);
holder.song.setText(list.get(i).getSong());
holder.singer.setText(list.get(i).getSinger());
//时间需要转换一下
holder.duration.setText(list.get(i).getDuration() + "");
int duration = list.get(i).duration;
String time = null;
try {
time = MainActivity.myAidlInterface.formatTime(duration);
} catch (RemoteException e) {
e.printStackTrace();
}
holder.duration.setText(time);
holder.position.setText(i + 1 + "");
return view;
}
class ViewHolder {
TextView song;//歌曲名
TextView singer;//歌手
TextView duration;//时长
TextView position;//序号
}
}