这是我参与11月更文挑战的第13天,活动详情查看:2021最后一次更文挑战
文章目录
开发步骤
【1】显示软件界面
1)设计布局:包括 activity_main.xml 和 ListView 的模板
a)先设计底部按钮区域
b)设计中间歌曲信息显示区域
c)添加ListView控件
2)读取数据(完成 MusicDao 相关类)
a)先设计实体类 Music
b)设计 IDao 接口
c)创建 MusicDao 实现 IDao 接口,并重写抽象方法
d)创建 MusicDaoFactory,提供静态方法返回 MusicDao 的对象
3)实现 Adapter(完成 MusicAdapter 相关类)
4)显示 ListView
【2】开发 Service
1)创建并注册 Service
2)在 Service 内部完成播放控制相关方法,例如 play() 等
【3】实现基本控制
【4】实现播放信息显示
【5】实现细节,修复 bug
展示音乐列表
布局文件
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
android:id="@+id/lv_music_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="@+id/ll_music_info" />
<LinearLayout
android:id="@+id/ll_music_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_above="@+id/sk_progress"
android:padding="10dp">
<TextView
android:id="@+id/tv_music_info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="请选择播放歌曲" />
</LinearLayout>
<SeekBar
android:id="@+id/sk_progress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_above="@+id/ll_button_layout" />
<LinearLayout
android:id="@+id/ll_button_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:gravity="center">
<ImageButton
android:id="@+id/ib_previous"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@android:drawable/ic_media_previous" />
<ImageButton
android:id="@+id/ib_play"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@android:drawable/ic_media_play" />
<ImageButton
android:id="@+id/ib_next"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@android:drawable/ic_media_next" />
</LinearLayout>
</RelativeLayout>
item_music.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp">
<TextView
android:id="@+id/tv_music_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:maxLines="1"
android:text="title"
android:textColor="#222222"
android:textSize="15dp" />
<TextView
android:id="@+id/tv_music_path"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@+id/tv_music_title"
android:maxLines="1"
android:text="path......"
android:textColor="#999999"
android:textSize="15dp" />
</RelativeLayout>
目录结构
Music类
public class Music {
private String title;
private String path;
public Music() {
}
public Music(String title, String path) {
this.title = title;
this.path = path;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
@Override
public String toString() {
return "Music [Title=" + title + ",Path=" + path;
}
}
IDao
public interface IDao<T> {
/**
* 获取数据
* @return 数据的List集合
*/
List<T> getData();
}
MusicDao
public class MusicDao implements IDao<Music> {
public List<Music> getData() {
List<Music> musics = new ArrayList<>();
if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
File musicDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC);
File[] files = musicDir.listFiles();
if (files != null && files.length > 0) {
for (int i = 0; i < files.length; i++) {
if (files[i].isFile()) {
String fileName = files[i].getName();
if (fileName.toLowerCase().endsWith(".mp3")) {
Music music = new Music();
music.setPath(files[i].getAbsolutePath());
music.setTitle(fileName.substring(0, fileName.length() - 4));
musics.add(music);
}
}
}
}
}
return musics;
}
}
MusicDaoFactory
public class MusicDaoFactory {
//如果是单例,就用getInstance();
public static IDao<Music> newInstance(Context context) {
return new MusicDao();
}
}
BaseAdapter
public class BaseAdapter<T> extends android.widget.BaseAdapter {
private Context context;
private List<T> data;
private LayoutInflater inflater;
public BaseAdapter(Context context, List<T> data) {
setContext(context);
setData(data);
setInflater();
}
public Context getContext() {
return context;
}
public void setContext(Context context) {
if (context == null) {
throw new IllegalArgumentException("参数Context不允许为null");
}
this.context = context;
}
public List<T> getData() {
return data;
}
public void setData(List<T> data) {
if (data == null) {
data = new ArrayList<>();
}
this.data = data;
}
public LayoutInflater getInflater() {
return inflater;
}
public void setInflater() {
inflater = LayoutInflater.from(context);
}
@Override
public int getCount() {
return data.size();
}
@Override
public Object getItem(int i) {
return null;
}
@Override
public long getItemId(int i) {
return 0;
}
@Override
public View getView(int i, View view, ViewGroup viewGroup) {
return view;
}
}
MusicAdapter
public class MusicAdapter extends BaseAdapter<Music> {
public MusicAdapter(Context context, List data) {
super(context, data);
}
@Override
public View getView(int i, View view, ViewGroup viewGroup) {
Music music = getData().get(i);
if (view == null) {
view = getInflater().inflate(R.layout.item_music, null);
}
TextView title = view.findViewById(R.id.tv_music_title);
TextView path = view.findViewById(R.id.tv_music_path);
title.setText(music.getTitle());
path.setText(music.getPath());
return view;
}
}
AndroidManifest.xml
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
MainActivity
public class MainActivity extends AppCompatActivity {
private ListView listView;
private TextView tvMusicInfo;
private SeekBar skMusicProgress;
private ImageButton ibPrevious;
private ImageButton ibPlay;
private ImageButton ibNext;
private List<Music> musics;
private MusicAdapter adapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
musics = MusicDaoFactory.newInstance(this).getData();
adapter = new MusicAdapter(this, musics);
listView.setAdapter(adapter);
setListeners();
}
private void setListeners() {
}
private void initView() {
listView = findViewById(R.id.lv_music_list);
tvMusicInfo = findViewById(R.id.tv_music_info);
skMusicProgress = findViewById(R.id.sk_progress);
ibPrevious = findViewById(R.id.ib_previous);
ibPlay = findViewById(R.id.ib_play);
ibNext = findViewById(R.id.ib_next);
}
}
运行程序
对比2.0优化
我们在 MusicAdapter 中输出 View 和 TextView,然后上下滑动观察日志
Log.d("TTT",title.toString());
Log.d("TTT",path.toString());
我们已经知道整个 View 是可以重复使用的,所以即使 title 和 path 每次都是新 new出来,但还是重复使用的:
TextView title = view.findViewById(R.id.tv_music_title);
TextView path = view.findViewById(R.id.tv_music_path);
所以我们反复的声明变量是没有意义的。
我们的 MusicAdapter 优化如下
public class MusicAdapter extends BaseAdapter<Music> {
public MusicAdapter(Context context, List data) {
super(context, data);
}
@Override
public View getView(int i, View view, ViewGroup viewGroup) {
Music music = getData().get(i);
ViewHolder holder;
if (view == null) {
view = getInflater().inflate(R.layout.item_music, null);
holder = new ViewHolder();
holder.title = view.findViewById(R.id.tv_music_title);
holder.path = view.findViewById(R.id.tv_music_path);
view.setTag(holder);
} else {
holder = (ViewHolder) view.getTag();
}
holder.title.setText(music.getTitle());
holder.path.setText(music.getPath());
return view;
}
class ViewHolder {
TextView title;
TextView path;
}
}
尽量使用 ViewHolder 这个类名,不要加private,不要加static
我们之前学到使用 static 有2个理由
1、使用频率太高
2、为了完成特定的设计模式或程序结构
这里一个都不符合,所以不加