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. 測試
測試成功,放一個測試的動圖