安卓開發個人小作品(3) - 多功能音樂播放器

這次介紹一個多功能音樂播放器,記得是大二那年寒假寫的,實現的主要功能就是音樂播放,帶進度條控制,掃描本地音樂,上一曲下一曲,播放類型(單曲循環,順序播放,隨機播放),APP主題換膚,背景圖更換等,功能都比較基礎,基本上如果你不會的話,跟着我的思路,應該都是能實現的,預計會在以後加入歌詞的功能。

在開始前,先放一張最後的效果圖吧,我個人喜歡的風格,簡約,美觀。

目錄

1.實現掃描本地音樂

2.音樂的播放與控制

3.關聯進度條seekbar,自定義seekbar

4.單曲循環,順序播放,隨機播放的實現

5.設置喜愛音樂

6.播放列表背景圖設置與保存

7.實現APP主題換膚的功能

正文

1.實現掃描本地音樂

這裏爲了將每個系統裏面存放的音樂抽象出來,也是爲了方便管理,先定義一個音樂類Song,代碼如下

public class Song {
	/** * 歌手 */
	private String singer;
	/** * 歌曲名 */
	private String song;
	/** * 歌曲的地址 */
	private String path;
	/** * 歌曲長度 */
	private int duration;
	/** * 歌曲的大小 */
	private long size;

	public String getSinger() {
		return singer;
	}

	public void setSinger(String singer) {
		this.singer = singer;
	}

	public String getSong() {
		return song;
	}

	public void setSong(String song) {
		this.song = song;
	}

	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;
	}
}

然後我們再寫一個工具類,這個工具類實現的功能就是掃描系統中的本地音樂,返回一個List<Song>集合,供我們使用,代碼如下

public class MusicUtils {
	/**
	 * 掃描系統裏面的音頻文件,返回一個list集合
	 */
	public static List<Song> getMusicData(Context context) {
		List<Song> list = new ArrayList<>();
		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()) {
				Song song = new Song();
				song.setSong( cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DISPLAY_NAME)));
				song.setSinger( cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST)));
				song.setPath(cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DATA)));
				song.setDuration( cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DURATION)));
				song.setSize( cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.SIZE)));
				if (song.getSize() > 1000 * 800) {//過濾掉短音頻
					// 分離出歌曲名和歌手
					if (song.getSong().contains("-")) {
						String[] str = song.getSong().split("-");
						song.setSinger( str[0]);
						song.setSong( str[1]);
					}
					list.add(song);
				}
			}
			// 釋放資源
			cursor.close();
		}
		return list;
	}

	//格式化時間
	public static String formatTime(int time) {
		if (time / 1000 % 60 < 10) {
			return time / 1000 / 60 + ":0" + time / 1000 % 60;
		} else {
			return time / 1000 / 60 + ":" + time / 1000 % 60;
		}
	}
}

然後,在佈局裏定義一個Listview,再給Listview寫一個適配器,一般繼承自BaseAdapter,adapter代碼如下

public class MyAdapter extends BaseAdapter {
	private Context context;
	private List<Song> list;
	private int position_flag = 0;

	public MyAdapter(MainActivity mainActivity, List<Song> list) {
		this.context = mainActivity;
		this.list = list;
	}

	@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 = null;
		if (view == null) {
			holder = new ViewHolder();
			// 引入佈局
			view = View.inflate(context, R.layout.list_item, null);
			// 實例化對象
			holder.song = (TextView) view.findViewById(R.id.item_mymusic_song);
			holder.singer = (TextView) view
					.findViewById(R.id.item_mymusic_singer);
			holder.duration = (TextView) view
					.findViewById(R.id.item_mymusic_duration);
			holder.position = (TextView) view
					.findViewById(R.id.item_mymusic_postion);

			view.setTag(holder);
		} else {
			holder = (ViewHolder) view.getTag();
		}
		// 給控件賦值
		String string_song = list.get(i).getSong();
		if (string_song.length() >= 5
				&& string_song.substring(string_song.length() - 4,
						string_song.length()).equals(".mp3")) {
			holder.song.setText(string_song.substring(0,
					string_song.length() - 4).trim());
		} else {
			holder.song.setText(string_song.trim());
		}

		holder.singer.setText(list.get(i).getSinger().toString().trim());
		// 時間轉換爲時分秒
		int duration = list.get(i).getDuration();
		String time = MusicUtils.formatTime(duration);
		holder.duration.setText(time);

		return view;
	}

	class ViewHolder {
		TextView song;// 歌曲名
		TextView singer;// 歌手
		TextView duration;// 時長
		TextView position;// 序號
	}

}

adapter裏面的列表項佈局代碼如下

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="60dp"
    android:orientation="horizontal">

    <TextView
        android:id="@+id/item_mymusic_postion"
        android:layout_width="30dp"
        android:layout_height="30dp"
        android:layout_alignBottom="@+id/item_mymusic_singer"
        android:layout_gravity="center_vertical"
        android:gravity="center"
        android:layout_margin="10dp"
        android:text="1"
        android:textSize="18sp" />

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="60dp">

        <TextView
            android:id="@+id/item_mymusic_song"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_alignParentTop="true"
            android:ellipsize="end"
            android:maxLines="1"
            android:layout_marginRight="10dp"
            android:layout_marginTop="5dp"
            android:text="歌曲名"
            android:textSize="18sp" />

        <TextView
            android:id="@+id/item_mymusic_singer"
            android:layout_width="wrap_content"
            android:layout_height="30dp"
            android:layout_alignParentBottom="true"
            android:layout_alignParentLeft="true"
            android:layout_toLeftOf="@+id/item_mymusic_duration"
            android:gravity="bottom"
            android:text="歌手"
            android:ellipsize="end"
            android:maxLines="1"
            android:layout_marginBottom="5dp"
            android:textSize="16sp" />

        <TextView
            android:id="@+id/item_mymusic_duration"
            android:layout_width="wrap_content"
            android:layout_height="30dp"
            android:layout_alignParentBottom="true"
            android:layout_alignParentRight="true"
            android:gravity="bottom"
            android:layout_marginRight="5dp"
            android:layout_marginBottom="5dp"
            android:text="歌曲時間"
            android:textSize="16sp" />

    </RelativeLayout>


</LinearLayout>

然後在Listview所在的activity裏,調用工具類獲取音樂集合,構造適配器,給Listview設置適配器,即可在Listview中顯示本地所有的音樂啦,關鍵代碼就三行,如下

List<Song> list = MusicUtils.getMusicData(MainActivity.this);
MyAdapter adapter = new MyAdapter(MainActivity.this, list);
listview.setAdapter(adapter);

好了,到現在爲止,你已經實現了,顯示手機裏所有的音樂,但是還不能播放,怎麼播放,接着往下看

2.音樂的播放與控制

實現音樂播放,需要用到的類爲MediaPlayer,爲了方便,封裝一個播放音樂的方法,如下

private void musicplay(int position) {
        try {
            mplayer.reset();
            mplayer.setDataSource(list.get(position).getPath());
            mplayer.prepare();
            mplayer.start();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

傳入的position爲,播放的音樂的位置,即序號。

然後給listview設置點擊事件

listview.setOnItemClickListener(new AdapterView.OnItemClickListener() {

            @Override
            public void onItemClick(AdapterView<?> parent, View view,
                                    int position, long id) {
                musicplay(currentposition);
            }
        });

這樣我們只是實現了簡單的播放,點擊Listview對應的條目,即可播放對應的音樂

我們下一步就是實現,音樂播放的控制,即暫停,下一曲,上一曲的實現

首先是暫停,在播放按鈕的點擊時間中,我們通常的需求是這樣的,如果當前音樂正在播放,那麼點擊,暫停音樂,再點擊,即可再次接着上次的繼續播放,所以在播放按鈕的點擊事件中,需要根據不同情況處理,同時爲了直觀,需要準備兩張圖片,播放的時候一張,暫停的時候一張,播放按鈕的點擊事件如下

imageView_play.setOnClickListener(new View.OnClickListener() {

            @Override
            public void onClick(View v) {
                if (mplayer.isPlaying()) {
                    mplayer.pause();
                    imageview.clearAnimation();
                } else {
                    mplayer.start();
                    // thread = new Thread(new SeekBarThread());
                    // thread.start();
                    imageview.startAnimation(AnimationUtils.loadAnimation(
                            MainActivity.this, R.anim.imageview_rotate));
                }
            }
        });

由於爲了界面體驗良好,我這裏還設置了,當音樂播放的時候,左側圖片的旋轉效果,代碼已經在上面的點擊事件中,效果圖如下

左側imageview的動畫代碼如下

<?xml version="1.0" encoding="utf-8"?>
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="20000"
    android:fromDegrees="0"
    android:interpolator="@android:anim/linear_interpolator"
    android:pivotX="50%"
    android:pivotY="50%"
    android:repeatCount="-1"
    android:repeatMode="restart"
    android:toDegrees="360" />

扯了點其他的,下面來實現上一曲和下一曲的效果,我們也可以和播放一個,分別寫一個對應的方法

上一曲方法代碼如下

// 上一曲
    private void frontMusic() {
        currentposition--;
        if (currentposition < 0) {
            currentposition = list.size() - 1;
        }
        musicplay(currentposition);
    }

其中,currentposition是記錄的當前音樂播放序號,這裏有一點需要考慮的是,當前播放音樂的序號爲0的時候,進行--操作之後那麼會變成負數,所以,這裏根據邏輯,處理爲播放列表最後一曲,即設置序號爲list.size()-1,形成一個環形。

相信你看了上一曲的方法,那麼下一曲也很簡單了,下一曲方法代碼如下

// 下一曲
    private void nextMusic() {
        currentposition++;
        if (currentposition > list.size() - 1) {
            currentposition = 0;
        }
        musicplay(currentposition);
    }

同樣我們也需要處理播放歌曲到最後一曲的時候,設置爲播放列表第一首歌曲。

3.關聯進度條seekbar,自定義seekbar

關聯進度條的方法也很簡單,這裏將更新seekbar的方法重新開了一個線程,專門處理更新,代碼如下

// 自定義的線程,用於下方seekbar的刷新
    class SeekBarThread implements Runnable {

        @Override
        public void run() {
            while (!ischanging && mplayer.isPlaying()) {
                // 將SeekBar位置設置到當前播放位置
                seekBar.setProgress(mplayer.getCurrentPosition());

                try {
                    // 每500毫秒更新一次位置
                    Thread.sleep(500);
                    // 播放進度

                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

其中,ischanging用於判斷當前的seekbar是否處於滑動狀態,然後在音樂播放的地方,也就是剛纔封裝的musicplay方法中,更改爲如下代碼

private void musicplay(int position) {
        seekBar.setMax(list.get(position).getDuration());
        imageview.startAnimation(AnimationUtils.loadAnimation(
                MainActivity.this, R.anim.imageview_rotate));
        try {
            mplayer.reset();
            mplayer.setDataSource(list.get(position).getPath());
            mplayer.prepare();
            mplayer.start();
        } catch (Exception e) {
            e.printStackTrace();
        }
        thread = new Thread(new SeekBarThread());
        thread.start();
    }

當然,不要忘了先設置seekbar的最大刻度值,也就是上面代碼中setMax方法。

至此,你的音樂播放就已經和seekbar進度條關聯起來了,但是你可能會發現系統默認的進度條很醜,不符合你的審美,那麼我們就需要更改seekbar的樣式,也就是自定義seekbar。

自定義seekbar,需要在佈局中設置progressDrawable和thumb,分別對應進度條的背景和進度條的指示小圖標,我這裏進度條的背景採用的是drawable,代碼如下

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >

    <item android:id="@+id/background">
        <shape>
            <solid 
                android:color="#DCDCDC"/>
        </shape>
    </item>
    <item android:id="@+id/secondaryProgress">
        <clip>
            <shape>
                <solid android:color="@color/blue" />
            </shape>
        </clip>
    </item>
    <item android:id="@+id/progress">
        <clip>
            <shape>
                <solid android:color="@color/blue" />
            </shape>
        </clip>
    </item>

</layer-list>

而thumb,我這裏使用的就是一張圖片,可以在我的項目源代碼中找到,圖片長下面這個樣子

當然你也可以採用自己的圖片,來實現炫酷的效果哦!

4.單曲循環,順序播放,隨機播放的實現

實現這個效果,首先我哦們定義一個變量,用於記錄當前的播放類型是哪種,如下

// 用於判斷當前的播放順序,0->單曲循環,1->順序播放,2->隨機播放
private int play_style = 0;

然後在我們的更改播放類型的按鈕點擊事件中,更改它的值,點擊事件代碼如下

imageview_playstyle.setOnClickListener(new View.OnClickListener() {

            @Override
            public void onClick(View v) {
                play_style++;
                if (play_style > 2) {
                    play_style = 0;
                }

                switch (play_style) {
                    case 0:
                        imageview_playstyle.setImageResource(R.mipmap.cicle);
                        Toast.makeText(MainActivity.this, "單曲循環",
                                Toast.LENGTH_SHORT).show();
                        break;
                    case 1:
                        imageview_playstyle.setImageResource(R.mipmap.ordered);
                        Toast.makeText(MainActivity.this, "順序播放",
                                Toast.LENGTH_SHORT).show();
                        break;
                    case 2:
                        imageview_playstyle.setImageResource(R.mipmap.unordered);
                        Toast.makeText(MainActivity.this, "隨機播放",
                                Toast.LENGTH_SHORT).show();
                        break;

                }
            }
        });

邏輯比較簡單,應該都能看懂,然後就是怎麼根據這個變量來實現對應的效果,核心方法就是MediaPLayer的setOnCompeleteListener,代碼如下

// 監聽mediaplayer播放完畢時調用
        mplayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {

            @Override
            public void onCompletion(MediaPlayer mp) {
                // TODO Auto-generated method stub
                switch (play_style) {
                    case 0:
                        musicplay(currentposition);
                        break;
                    case 1:
                        nextMusic();
                        break;
                    case 2:
                        random_nextMusic();
                        break;
                    default:

                        break;
                }
            }
        });

下一曲的代碼上面已經給出了,下面是隨機播放下一曲的代碼,思想很簡單,就是生成一個隨機數,再設置爲currentpositon,然後調用musicplay方法即可

// 隨機播放下一曲
    private void random_nextMusic() {
        currentposition = currentposition + random.nextInt(list.size() - 1);
        currentposition %= list.size();
        musicplay(currentposition);
    }

5.設置喜愛音樂

喜愛音樂的設置,我這裏處理的比較簡單, 當長按列表項的時候,彈出對話框,用於設置喜愛音樂,效果如下

然後,用sharepreference記錄下喜愛音樂的序號值,當要播放喜愛音樂的時候,直接取到該序號值,然後調用musicplay方法播放序號值對應的音樂即可。主要就是sharepreference的使用,代碼很簡單,就不貼了

6.播放列表背景圖設置與保存

設置播放列表背景也就是調用一下,listview.setBackground即可,但是我們如果不進行保存的話,下次進入APP的時候,背景圖可能又恢復爲初始的,那麼我們就需要保存列表ode背景圖,這裏也採用sharepreference來保存,首先用Base64將圖片轉換爲String,然後保存起來,下次進入APP的時候,再取出來,用Base64將String轉爲drawable對象,在設置上去即可。相關代碼如下。

// 使用sharedPreferences保存listview背景圖片
    private void saveDrawable(Drawable drawable) {
        SharedPreferences.Editor editor = sharedPreferences.edit();
        BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable;
        Bitmap bitmap = bitmapDrawable.getBitmap();
        // Bitmap bitmap = BitmapFactory.decodeResource(getResources(), id);
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        bitmap.compress(Bitmap.CompressFormat.JPEG, 50, baos);
        String imageBase64 = new String(Base64.encodeToString(
                baos.toByteArray(), Base64.DEFAULT));
        editor.putString("listbg", imageBase64);
        editor.commit();
    }

    // 加載用sharedPreferences保存的圖片
    private Drawable loadDrawable() {
        String temp = sharedPreferences.getString("listbg", "");
        ByteArrayInputStream bais = new ByteArrayInputStream(Base64.decode(
                temp.getBytes(), Base64.DEFAULT));
        return Drawable.createFromStream(bais, "");
    }

7.實現APP主題換膚的功能

實現主題效果,有很多種方法,我這裏採用的是自定義屬性的方法,首先我們在values下新建一個文件attrs,內容如下

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <attr name="theme_color" format="color" />
    <attr name="popupwindow_bg" format="reference"/>
    <attr name="dialogactivity_bg" format="reference"/>
    <attr name="btn_submit_bg" format="reference"/>
    <attr name="seekbar_progress_bg" format="reference"/>
    <attr name="play_image" format="reference" />
    <attr name="next_image" format="reference" />
    <attr name="front_image" format="reference" />
    <attr name="thumb_image" format="reference" />
    <attr name="indicate_image" format="reference" />
</resources>

這裏每一個attr屬性代表了哪些內容需要根據主題不同而更換,比如popupwindow_bg,即彈出窗口的背景色等等,然後在styles文件文件中指定各個主題下,這些值分別對應哪個具體的值,styles中相關代碼如下

<style name="Theme_blue">
        <item name="theme_color">@color/blue</item>
        <item name="popupwindow_bg">@drawable/popupwindow_bg</item>
        <item name="dialogactivity_bg">@drawable/dialogactivity_bg</item>
        <item name="btn_submit_bg">@drawable/btn_submit_bg</item>
        <item name="seekbar_progress_bg">@drawable/seekbar_progress_bg</item>
        <item name="play_image">@mipmap/play</item>
        <item name="next_image">@mipmap/next</item>
        <item name="front_image">@mipmap/front</item>
        <item name="thumb_image">@mipmap/seekbar_thumb</item>
        <item name="indicate_image">@mipmap/play_small</item>
    </style>

    <style name="Theme_purple">
        <item name="theme_color">@color/purple</item>
        <item name="popupwindow_bg">@drawable/popupwindow_bg_purple</item>
        <item name="dialogactivity_bg">@drawable/dialogactivity_bg_purple</item>
        <item name="btn_submit_bg">@drawable/btn_submit_bg_purple</item>
        <item name="seekbar_progress_bg">@drawable/seekbar_progress_bg_purple</item>
        <item name="play_image">@mipmap/play_purple</item>
        <item name="next_image">@mipmap/next_purple</item>
        <item name="front_image">@mipmap/front_purple</item>
        <item name="thumb_image">@mipmap/seekbar_thumb_purple</item>
        <item name="indicate_image">@mipmap/play_small_purple</item>
    </style>

    <style name="Theme_green">
        <item name="theme_color">@color/green</item>
        <item name="popupwindow_bg">@drawable/popupwindow_bg_green</item>
        <item name="dialogactivity_bg">@drawable/dialogactivity_bg_green</item>
        <item name="btn_submit_bg">@drawable/btn_submit_bg_green</item>
        <item name="seekbar_progress_bg">@drawable/seekbar_progress_bg_green</item>
        <item name="play_image">@mipmap/play_green</item>
        <item name="next_image">@mipmap/next_green</item>
        <item name="front_image">@mipmap/front_green</item>
        <item name="thumb_image">@mipmap/seekbar_thumb_green</item>
        <item name="indicate_image">@mipmap/play_small_green</item>
    </style>

    <style name="Theme_red">
        <item name="theme_color">@color/red</item>
        <item name="popupwindow_bg">@drawable/popupwindow_bg_red</item>
        <item name="dialogactivity_bg">@drawable/dialogactivity_bg_red</item>
        <item name="btn_submit_bg">@drawable/btn_submit_bg_red</item>
        <item name="seekbar_progress_bg">@drawable/seekbar_progress_bg_red</item>
        <item name="play_image">@mipmap/play_red</item>
        <item name="next_image">@mipmap/next_red</item>
        <item name="front_image">@mipmap/front_red</item>
        <item name="thumb_image">@mipmap/seekbar_thumb_red</item>
        <item name="indicate_image">@mipmap/play_small_red</item>
    </style>

可以很清楚的看到,我設置了四個主題,每個主題中,我都對attrs中定義的屬性進行了具體的賦值,然後怎麼使用呢,舉個例子,比如我現在需要讓popupwindow的背景色隨主題改變而更換,那麼在popupwindow的佈局中,設置其background屬性爲如下即可

android:background="?attr/popupwindow_bg"

其他屬性的使用方法同理,然後我們如何來讓用戶設置主題呢,可以寫一個dialog,也可popupwindow,不過我這裏爲了學習一下樣式爲dialog的activity,便採用了這種方式,最後效果如下

看上去就像一個dialog,其實是一個activity,然後在這裏根據用戶的選擇,來設置不同的主題,然後拿到主題的類型之後,在代碼中根據這個值去判斷應該顯示哪個主題,相關代碼如下

// 主題設置
string_theme = sharedPreferences.getString("theme_select", "blue");
if (string_theme.equals("blue")) {
      setTheme(R.style.Theme_blue);
} else if (string_theme.equals("purple")) {
      setTheme(R.style.Theme_purple);
} else if (string_theme.equals("green")) {
      setTheme(R.style.Theme_green);
} else {
      setTheme(R.style.Theme_red);
}
setContentView(R.layout.activity_main);

記住一定要在setContentView方法之前調用,具體細節各方面由於代碼比較散,不方便貼,可以去源碼裏看我是怎麼設置的,最終四個主題下的主界面效果如下

                   

                   

當然這個APP裏,還有很多其他的細節,諸如,控制當前播放的列表項爲不同顏色,頂部顯示歌曲名字的彩色TextView等,這些可以直接去看源碼,實現的方法也不難,歡迎訪問源碼!!

源碼下載

源碼下載

由於考慮到大家可能沒有積分,我把源碼重新傳到了百度雲,這樣大家可以免費下載學習,鏈接和提取碼如下:

鏈接: https://pan.baidu.com/s/1KNxJvsE6XTIi3JkEBgCNgw 提取碼: 4xhi 

發佈了37 篇原創文章 · 獲贊 86 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章