1. 說明
本文很早就開始寫了,不過中間把電腦借給小伙伴了,后來就忘了這件事了…
內容已同步到Gitee倉庫
以往的文章
- 服務端:Android音樂播放器開發–服務端
- 登錄:Android音樂播放器開發–登錄
- 注冊:Android音樂播放器開發–注冊
- 修改密碼:Android音樂播放器開發–修改密碼
- 播放:Android音樂播放器開發–播放
(適用于平時做個小課設的小伙伴們)
本部分內容實作效果如下:

2. 改動
由于我設計不周,播放功能實作時沒有考慮到本次內容的實作,現針對播放功能的實作做出部分改動,
因為不同Activity之間互相呼叫內部的方法比較復雜,現在將可以復用的部分程式拿出來構建一個工具類(作為橋梁的功能)
新建一個工具類,命名為MusicPlayUtil
構建單例模式,保證其它類拿到的物件只有一個,
//這里私有化了無參構造,其它類不可以new該物件
private MusicPlayUtil(){
}
public static MusicPlayUtil musicPlayUtil = new MusicPlayUtil();
public static MusicPlayUtil getInstance(){
return musicPlayUtil;
}
系結MainActivity和PlayerControl,因為需要使用到它們其中的變數和方法,
private MainActivity mainActivity = null;
private PlayerControl mPlayerControl = null;
//系結MainActivity
public void setMainActivity(MainActivity activity){
this.mainActivity = activity;
mPlayerControl = mainActivity.mPlayerControl; //MainActivity已經呼叫了PlayerControl,這里直接使用
}
將MainActivity中顯示音樂界面的方法提取到了工具類里,
這一部分內容在前文里已經做了說明,這里再簡單介紹一下,歌曲播放界面初始化時,除了初始化功能按鈕等內容,還需要初始化有關歌曲的元素,所謂’有關歌曲’,是因為不同的歌曲所展示的內容是不同的,包括歌曲封面、歌曲名稱和演唱者等資訊,因此在播放器初始化和后面的切換歌曲時都需要重新初始化有關歌曲的部分界面,我們還可以看到,方法傳遞了一個引數playState,這個引數相當于在詢問播放器“是否需要播放?”,因為我們不希望用戶剛打開界面就已經在播放歌曲了,沒有哪個播放器是這么做的,而在切換歌曲的時候需要自動播放,這里做了個區分,而這個引數是靜態的(static),所以可以直接呼叫,
//設定有關歌曲的界面
public void setMusicView(MainActivity.IsPlay playState){
try {
JSONObject musicInfo = (JSONObject) mainActivity.sMusicList.get(mainActivity.musicId);
String name = musicInfo.optString("name");
String author = musicInfo.optString("author");
String img = musicInfo.optString("img");
mainActivity.playAddress=musicInfo.optString("address");
mainActivity.mMusicPic.setImageUrl(IMG+img, R.mipmap.ic_launcher,R.mipmap.ic_launcher);
mainActivity.mMusicName.setText(name);
mainActivity.mMusicArtist.setText(author);
} catch (Exception e) {
e.printStackTrace();
}
if(playState == MainActivity.IsPlay.play){
if ( mPlayerControl != null) {
mPlayerControl.stopPlay();
}
mPlayerControl.playOrPause(playState);
}
}
然后就是在播放界面設定和獲取部分變數
//獲取歌曲串列
public JSONArray getMusicList(){
return mainActivity.sMusicList;
}
//獲取歌曲id
public int getMusicId(){
return mainActivity.musicId;
}
//獲取歌曲總數
public int getMusicNum(){
return mainActivity.songNum;
}
//設定歌曲id
public void setMusicId(int id){
mainActivity.musicId = id;
}
工具類的內容就這些,那么怎么用呢?
- 獲取工具類物件
private MusicPlayUtil musicPlayUtil = MusicPlayUtil.getInstance(); //獲取工具類實體化的物件
- 在MainActivity中,初始化時需要將自身這個物件作為引數傳遞給工具類,保證工具類也可以修改部分UI
musicPlayUtil.setMainActivity(this);
- 另外在初始化或者切換歌曲時呼叫工具類的
setMusicView方法(將自己原有的這個方法刪掉)
musicPlayUtil.setMusicView(IsPlay.notPlay);
3. 界面設計
界面主體是一個ListView,它以串列的形式展示內容,當資料量足夠多時會出現滾動條,并且能夠根據資料量自適應螢屏;而ListView的元素都是歌曲對應的基本資訊,
- 標題欄
標題欄仿照登錄時介紹的main_title_bar.xml新建了一個表頭檔案,命名為title_bar.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="56dp"
android:background="@color/colorPrimary"
>
<ImageButton
android:id="@+id/ib_title_back"
android:src="@drawable/go_back_selector"
android:background="@null"
android:layout_marginLeft="10dp"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_alignParentLeft="true"
android:layout_centerVertical="true"
/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="播放串列"
android:layout_centerVertical="true"
android:layout_centerHorizontal="true"
android:textColor="#fff"
android:textSize="20sp"
android:id="@+id/tv_title"
/>
</RelativeLayout>

- ListView
這一部分只加了一個ListView組件(activity_music_list.xml)
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:orientation="vertical"
tools:context="cn.sjcup.musicplayer.activity.MusicListActivity">
<include layout="@layout/title_bar"></include>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/lv_music" />
</FrameLayout>
</LinearLayout>

- Item布局
這部分是ListView顯示的元素布局,(activity_item.xml)
Item顯示了歌曲的基本資訊,包括歌曲封面、歌名和演唱資訊,另外加了一個標記,用于標識當前所播的歌曲,
<?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="65dp">
<com.loopj.android.image.SmartImageView
android:id="@+id/siv_img"
android:layout_width="80dp"
android:layout_height="60dp"
android:layout_alignParentLeft="true"
android:layout_marginBottom="5dp"
android:layout_marginLeft="5dp"
android:layout_marginTop="5dp"
android:scaleType="centerCrop"
android:src="@mipmap/ic_launcher">
</com.loopj.android.image.SmartImageView>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/tv_name"
android:layout_marginLeft="5dp"
android:layout_marginTop="10dp"
android:layout_toRightOf="@id/siv_img"
android:ellipsize="end"
android:maxLength="20"
android:singleLine="true"
android:text="歌曲"
android:textColor="#000000"
android:textSize="18sp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/tv_author"
android:layout_below="@id/tv_name"
android:layout_marginLeft="5dp"
android:layout_marginTop="5dp"
android:layout_toRightOf="@id/siv_img"
android:ellipsize="end"
android:maxLength="16"
android:singleLine="true"
android:text="作者"
android:textColor="#99000000"
android:textSize="14sp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/tv_type"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:layout_marginBottom="5dp"
android:layout_marginRight="10dp"
android:text="播放中"
android:textColor="#99000000"
android:textSize="12sp"/>
</RelativeLayout>

4. 功能設計
4.1定義變數
//控制元件
private ImageButton mBack;
private ListView mMusicList;
private TextView mState;
//獲取到的資料
private JSONArray musicList;
private JSONObject musicInfo;
//獲取工具類實體化物件
private MusicPlayUtil musicPlayUtil = MusicPlayUtil.getInstance();
4.2 初始化界面
在初始化時,分為了界面初始化和事件初始化
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_music_list);
//初始化界面
initView();
//設定相關事件
initEvent();
}
初始化界面,系結界面控制元件,將歌曲資訊填充到ListView中
//初始化界面
private void initView() {
mBack = findViewById(R.id.ib_title_back);
mMusicList = findViewById(R.id.lv_music);
fillData(); //填充資料
}
資料填充使用setAdapter方法,這是ListView本身自帶的方法,通過名稱可以看出,Adapter意為配接器,我們將配接器填充到ListView中,引數是實體化后的MusicAdapter物件,MusicAdapter物件繼承了BaseAdapter,并重寫了四個方法,
四個方法分別為:
- int getCount() 填充的item個數
- Object getItem(int position) 指定索引對應的item資料項
- long getItemId(int position) 指定索引對應item的id值
- View getView(final int position, View convertView, ViewGroup parent) 填充每個item的可視內容并回傳
我們可以簡單的理解為,配接器先從getCount里確定填充的數量,然后回圈執行getView方法將條目一個一個繪制出來,所以必須重寫的是getCount和getView方法,而getItem和getItemId是呼叫某些函式才會觸發的方法,如果不需要使用時可以暫時不修改,
具體的介紹可以參考—>博客
下面是填充資料的這一部分整體的程式,稍后會拆開介紹,
//填充資料
private void fillData() {
mMusicList.setAdapter(new MusicAdapter());
}
private class MusicAdapter extends BaseAdapter {
@Override
public int getCount() {
return musicPlayUtil.getMusicNum();
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder; //ViewHolder是定義的區域定義類,作為資訊傳遞的載體
musicList = musicPlayUtil.getMusicList();
try{
musicInfo = musicList.getJSONObject(position);
}catch (Exception e){
e.printStackTrace();
}
if (convertView == null){
convertView = LayoutInflater.from(
getApplicationContext()).inflate(R.layout.activity_item,parent,false);
holder = new ViewHolder();
holder.siv = convertView.findViewById(R.id.siv_img);
holder.tv_name = convertView.findViewById(R.id.tv_name);
holder.tv_author = convertView.findViewById(R.id.tv_author);
holder.tv_type = convertView.findViewById(R.id.tv_type);
convertView.setTag(holder);
}else {
holder = (ViewHolder) convertView.getTag();
}
holder.siv.setImageUrl(IMG+musicInfo.optString("img"), R.mipmap.ic_launcher,
R.mipmap.ic_launcher);
holder.tv_name.setText(musicInfo.optString("name"));
holder.tv_author.setText(musicInfo.optString("author"));
holder.tv_type.setText("");
if (musicPlayUtil.getMusicId()==position){
holder.tv_type.setText("播放中");
holder.tv_type.setTextColor(Color.RED);
mState=holder.tv_type;
}
return convertView;
}
@Override
public Object getItem(int position) {
return null;
}
@Override
public long getItemId(int position) {
return 0;
}
class ViewHolder{
TextView tv_name;
TextView tv_author;
TextView tv_type;
SmartImageView siv;
}
}
分別講解一下,
因為需要頻繁的傳遞資訊,這里定義了一個區域內部類,分別對應了item布局的4個控制元件(拓展一下:內部類分為靜態內部類、成員內部類、區域內部類、匿名內部類,感興趣的小伙伴可以去復習一下)
class ViewHolder{
TextView tv_name;
TextView tv_author;
TextView tv_type;
SmartImageView siv;
}
getCount方法,回傳的是填充的item的數量,在本專案中,就是歌曲的數量,歌曲數量已經在MainActivity中獲取,這里可以通過工具類直接獲取到這個值,
@Override
public int getCount() {
return musicPlayUtil.getMusicNum();
}
getView方法,回傳的是填充的view資訊,這一部分是本文最難理解的,引數position相當于一個標識,用來表示進行操作的是哪一個item,從0開始計數;convertView就是待回傳的view資訊,初始化時為null,我們要加工的也就是這個物件,
首先,定義了一個類holder,使用時需要實體化一個ViewHolder(上面定義的區域內部類)作為資訊傳遞的載體,也就是填充得item數量決定了ViewHolder的實體化物件的數量,所有歌曲資訊可以通過上面定義的工具類獲取,然后通過position獲取到對應的歌曲資訊(JSONArray里的object也是從0開始計數的,我們可以認為是一一對應的),
下面我們做個測驗,檢測一下什么時候呼叫getView方法,
------------------------------------------------------------Test---------------------------------------------------------
我們在getView方法中輸出position,也就是每次呼叫getView方法時都會輸出當前的position,

這時我們打開音樂串列,可以看到顯示了11首歌曲

輸出顯示呼叫了11次getView方法,position對應了0–>10

然后向上滑,加載其它的歌曲,上滑的同時,其它內容通過getView方法被加載,position對應11->16

現在我們想一個問題,此時下滑,也就是希望顯示之前的歌曲資訊,getView會被重新呼叫嗎?(1. 第一種想法當然是認為之前的歌曲資訊已經加載出來了,當然不會再呼叫了;2. 第二種想法就是會呼叫吧)
事實是加載之前的資訊會重新呼叫getView

經過簡單的測驗,我們可以得出一個小結論,界面中可以顯示出多少個item,就會呼叫多少次getView方法加載資訊,也就是哪個item進入可是范圍,就會呼叫getView方法資訊,前面介紹道,理論上有多少條資訊就應該有對應相應數量的item,但是考慮到手機的記憶體是有限的,如果數量過多,占用記憶體就會過大,實際上的操作只會初始化特定數量的item(依螢屏長度而定),在滑動程序中,item重復使用,使用getView方法不斷賦予新的資訊,
------------------------------------------------------------ENDTest---------------------------------------------------------
關于setTag和getTag的使用,在setTag之前,convertView需要找到xml中定義的layout,這里是activity_item,相當于系結了一個目標item,然后使用findViewById系結item中的組件,再對每個組件賦予不同的值,前面介紹道,上滑又下滑后,前面加載的資訊會再次使用getView方法“繪制”一遍,但已經系結的組件沒有必要再系結一次,因為這個程序比較消耗資源,因此在一次使用findViewById系結后,呼叫setTag保存起來,再次加載時,同于同樣的資訊,直接使用getTag獲取到已系結組件的ViewHolder,而無需重復系結組件,
if (convertView == null){
convertView = LayoutInflater.from(
getApplicationContext()).inflate(R.layout.activity_item,parent,false);
holder = new ViewHolder();
holder.siv = convertView.findViewById(R.id.siv_img);
holder.tv_name = convertView.findViewById(R.id.tv_name);
holder.tv_author = convertView.findViewById(R.id.tv_author);
holder.tv_type = convertView.findViewById(R.id.tv_type);
convertView.setTag(holder);
}else {
holder = (ViewHolder) convertView.getTag();
}
對于初學者來說,這個比較難理解,建議多看幾篇文章,以上很多內容都是本人自己的理解,如果有不對的地方敬請指出,
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder; //ViewHolder是定義的區域內部類,作為資訊傳遞的載體
musicList = musicPlayUtil.getMusicList();
try{
musicInfo = musicList.getJSONObject(position);
}catch (Exception e){
e.printStackTrace();
}
if (convertView == null){
convertView = LayoutInflater.from(
getApplicationContext()).inflate(R.layout.activity_item,parent,false);
holder = new ViewHolder();
holder.siv = convertView.findViewById(R.id.siv_img);
holder.tv_name = convertView.findViewById(R.id.tv_name);
holder.tv_author = convertView.findViewById(R.id.tv_author);
holder.tv_type = convertView.findViewById(R.id.tv_type);
convertView.setTag(holder);
}else {
holder = (ViewHolder) convertView.getTag();
}
holder.siv.setImageUrl(IMG+musicInfo.optString("img"), R.mipmap.ic_launcher,
R.mipmap.ic_launcher);
holder.tv_name.setText(musicInfo.optString("name"));
holder.tv_author.setText(musicInfo.optString("author"));
holder.tv_type.setText("");
if (musicPlayUtil.getMusicId()==position){
holder.tv_type.setText("播放中");
holder.tv_type.setTextColor(Color.RED);
mState=holder.tv_type;
}
return convertView;
}
4.3定義事件
在這個界面,我們希望實作兩個點擊事件,1. 點擊“回傳”按鈕,可以回傳播放界面;2. 點擊一個歌曲資訊,可以播放對應的歌曲,
//初始化事件
private void initEvent() {
mMusicList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
if (mState == null) {
mState = view.findViewById(R.id.tv_type);
mState.setText("播放中");
mState.setTextColor(Color.RED);
musicPlayUtil.setMusicId(position);
musicPlayUtil.setMusicView(MainActivity.IsPlay.play);
finish();
} else {
mState.setText("");
mState = view.findViewById(R.id.tv_type);
mState.setText("播放中");
mState.setTextColor(Color.RED);
musicPlayUtil.setMusicId(position);
musicPlayUtil.setMusicView(MainActivity.IsPlay.play);
finish();
}
}
});
mBack.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
MusicListActivity.this.finish();
}
});
}
第一個事件,回傳播放界面比較簡單,這里不再贅述,
關于第二個事件,定義的變數mState表示的是歌曲的播放狀態,如果在播放狀態,就顯示“播放中”(如下圖),否則不顯示內容,

第一次點擊時,mState是null,并沒有被賦值,view就是被點擊的item,第一步就是系結被點擊的item的組件,更改它的內容,回應點擊事件,然后呼叫工具類中的方法修改歌曲id,這里的position和前面介紹的可以認為是一樣的,由于position從0開始,和我定義的歌曲id是一樣的,這里直接把它當作歌曲id了,如果不一致,這里需要再加一些處理,找到對應的歌曲id,再呼叫setMusicView播放對應的歌曲,至于finish方法,是做出點擊事件后關閉播放串列界面,回傳播放界面,如果你認為沒有必要可以不加,
mState如果不是null,那么之前是已經系結過別的item了,修改之前系結的播放狀態為空字串,表示沒有播放,再重新系結新的item,后面內容就與上面相似了,不再贅述,
5. 測驗
測驗成功,放一個測驗的動圖

轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/236126.html
標籤:其他
