Android 自定義音樂播放器實現
Android自定義音樂播放器
一:首先介紹用了哪些Android的知識點:
1 MediaPlayer工具來播放音樂
2 Handle。因為存在定時任務(歌詞切換,動畫,歌詞進度條變換等)需要由Handle來處理Ui相關內容
3 動態許可權申請(該應用程式讀取本地歌曲,並且設定音質相關屬性)且這兩個許可權在Android6.0後都需要動態申請
4 手勢控制 (左劃和右劃需要滿足一定條件後可以進行切歌)
5 Service服務 (啟動Service 繫結Service 前臺Service)
6 BroadcastReceiver 廣播,Service與Activity,Activity與Activity存在動態互動,需要廣播實現
7 基本的重寫View能力和Intent互動資料的能力
8 Animation動畫 圖片旋轉 歌詞更新
二:實現過程(簡要步驟,下面會詳細講解)
1 先編寫MusicInfo工具類。因為我們是從手機記憶體中去讀取音樂的相關資訊,那麼讀出的資料該儲存到MusicInfo工具類集合中。
2 先申請許可權,然後再去手機記憶體將音樂及其相關的資訊讀出來 ,用一個ListView容器去裝載所有的本地音樂
PS:到了這裡基本的音樂資訊列表已經有了 這也是我們的主介面(音樂列表介面) 即: 展示音樂/歌曲列表;
3 這時候先不方去實現播放這類的功能,我們先去處理歌詞
這裡說明一下。一個歌詞檔案(.Lrc)裡面內容格式如下
(張衛健--真英雄)
可以發現他由時間戳和歌詞內容兩部分組成。有了這個資訊後。編寫LrcContent工具類,用於記錄歌詞內容和歌詞時間。然後去手機裡面尋找歌曲對應的歌詞檔案,將其編碼,讀出,裝載為LrcContent集合。
4 編寫Service類,Service主要用來處理:音樂播放,前臺服務。在播放狀態改變的時候與播放音樂的Activity進行通訊。該Service由主介面啟動,後面的Activity繫結即可
5 編寫播放音樂的Activity類(MusicPlay)。當我們從主介面(音樂列表介面)點選了一首具體的音樂時,就會調轉到該Activity,所有首先,主介面Activity需要傳遞一些資訊給該Activity。
(1) MusicInfo工具類集合。即手機中所有的音樂資訊
(2)當前點選的歌曲,傳遞位置(position)即可
好,現在我們播放音樂的Activity有了所有的音樂資訊,還有當前需要播放的歌曲位置。因為需要前臺服務,所以我們把音樂播放的控制權交給Service,我們去繫結服務,然後把所有的音樂資訊,還有當前需要播放的歌曲位置都傳遞給Serivce,Service來控制播放音樂。
6 好了,現在我們的程式可以播放音樂了,我們再來一步步完善細節,歌詞同步,該功能自定義View實現,最後顯現在播放音樂的Activity中。注意Mediaplayer有一個重要屬性:
mediaPlayer.getCurrentPosition()。該方法會返回當前播放時間,不過返回的時候時毫秒(重)。
自定義View(LrcView 顯示歌詞),該View中除了傳統的自定義View需要的OnDraw之類的方法外,還需要獲取第三步中的LrcContent集合,有了這個我們就有了所有的歌詞內容和歌詞相應的時間,那麼同步如何實現呢?音樂最終該View要顯示在播放音樂的Activity(MusicPlay)中,我們去MusicPlay的佈局檔案申明該View,然後在MusicPlay中編寫一個定時器,可以設定每一秒啟動一次,定時器傳送訊息,在Handle中接受訊息,處理訊息。Handle中我們需要:獲取歌曲當前播放時間,根據當前播放時間去LrcContent集合中尋求匹配的歌詞。用invalidate()方法,通知自定義View重繪,來同步更新歌詞
7 我們的音樂播放器還差一個重要的東西,音樂控制器部分。這部分需要來控制播放上一首,下一首,播放/暫停,音樂進度拖動,音量設定。
該部分不難,所以再這裡不詳細講。
三:效果圖 因為完整錄製的GIF太大,傳不上來所以分批處理
四:程式碼精講 程式碼量也不很大,但是全貼出來挨著講又影響閱讀。所以部分節選和重要知識點一併講解。
PS:原始碼中含有大量System.out.println("XXXX");語句。個人比較偏愛的一種測試方式。。應該不干擾閱讀,忘見諒
1 許可權獲取,我們要做的第一件事就是去記憶體讀取音樂相關資訊,那麼我們就需要獲得相關的許可權,從Android6.0開始部分許可權不僅需要在AndroidManifest.xml檔案中宣告,還需要在執行程式的時候動態獲取.這裡以讀取儲存許可權為例:
首先在AndroidManifest.xml中定義
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
在Java業務程式碼中先判斷是否已經有許可權,有許可權就不再申請,沒有就申請許可權
//首先檢查自身是否已經擁有相關許可權,擁有則不再重複申請
int check = checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) ;
//沒有相關許可權
if (check != PackageManager.PERMISSION_GRANTED)
{
//申請許可權 STORGE_REQUEST = 1ActivityCompat.requestPermissions(this , new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE} ,STORGE_REQUEST);
}else {
//已有許可權的情況下可以直接初始化程式
init();
}
當我們申請許可權後,去判斷使用者是否給與了相關許可權,如果賦予了就可以做我們的事情了
/*
申請許可權處理結果
*/
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
switch (requestCode)
{
case STORGE_REQUEST :
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED)
{
//完成程式的初始化
init();
System.out.println("程式申請許可權成功,完成初始化") ;
}
else {
System.out.println("程式沒有獲得相關許可權,請處理");
}
break ;
}
}
關於許可權這部分:有興趣的可以看下參考下這篇部落格
2 有了許可權,我們就可以從手機中去讀取音樂相關資訊了,這裡講解下如何去讀取資訊
(1) 我們需要先建立一個儲存音樂資訊MusicInfo工具類
注意:程式碼沒貼完,還有屬性的get和set方法沒列出
public class MusicInfo implements Serializable {
private int _id = - 1; //音樂標識碼
private int duration = -1 ; //音樂時常
private String artist = null ; //音樂作者
private String musicName= null ; //音樂名字
private String album = null ; //音樂檔案專輯
private String title = null ; //音樂檔案標題
private int size ; //音樂檔案的大小 返回byte大小
private String data ; //獲取檔案的完整路徑
private String album_id ; //實際儲存為音樂專輯團片
}
(2)讀取音樂資訊。
Android中的音樂資訊儲存再手機資料庫中,那麼我們就需要去訪問資料庫。這裡介紹下如何訪問資料庫
首先要知道基本資料庫查詢操作,返回Cursor物件
public final Cursor query(Uri uri , //查詢路徑
String[] projection , //查詢指定的列
String selection, //查詢條件
String[] selectionArgs, //查詢引數
String sortOrder } //查詢結果的排序方式
那麼就有了我們查詢音樂的操作。
//申明ContentResolver物件,用於訪問系統資料庫 private ContentResolver contentResolver ; //用於裝載MusicInfo物件 private List<MusicInfo> musicInfos ;
//獲取系統的ContentResolver contentResolver = getContentResolver() ; //從資料庫中獲取指定列的資訊 mCursor = contentResolver.query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI , new String[] {MediaStore.Audio.Media._ID , MediaStore.Audio.Media.TITLE , MediaStore.Audio.Media.ALBUM , MediaStore.Audio.Media.ARTIST , MediaStore.Audio.Media.DURATION , MediaStore.Audio.Media.DISPLAY_NAME , MediaStore.Audio.Media.SIZE , MediaStore.Audio.Media.DATA , MediaStore.Audio.Media.ALBUM_ID } , null ,null ,null) ;
我們已經查詢出了所有的音樂資訊,並且儲存在mCursor物件中,現在我們需要把音樂資訊裝載到musicInfos工具類集合中。
但是注意,音樂專輯圖片需要特殊處理:我們這是隻查詢出了音樂專輯id MediaStore.Audio.Media.ALBUM_ID 我們需要根據音樂專輯id再去查詢音樂專輯圖片,用該圖片來作為音樂圖片。
/* 獲取本地音樂專輯的圖片 */ private String getAlbumArt(int album_id) { String UriAlbum = "content://media/external/audio/albums" ; String projecttion[] = new String[] {"album_art"} ; Cursor cursor = contentResolver.query(Uri.parse(UriAlbum + File.separator +Integer.toString(album_id)) , projecttion , null , null , null); String album = null ; if (cursor.getCount() > 0 && cursor.getColumnCount() > 0) { cursor.moveToNext() ; album = cursor.getString(0) ; } //關閉資源資料 cursor.close(); return album ; }
這時還可能出現一個問題,可能存在本地專輯音樂圖片不存在,所以我們要進行判斷,沒有專輯圖片就使用預設的圖片。好了,現在我們就可以裝載音樂資訊了
musicInfos = new ArrayList<>() ; for (int i = 0 ; i < mCursor.getCount() ; i++) { Map<String , String> map = new HashMap<>() ; MusicInfo musicInfo = new MusicInfo() ; //列表移動 mCursor.moveToNext() ; //將資料裝載到List<MusicInfo>中 musicInfo.set_id(mCursor.getInt(0)); musicInfo.setTitle(mCursor.getString(1)); musicInfo.setAlbum(mCursor.getString(2)); musicInfo.setArtist(mCursor.getString(3)); musicInfo.setDuration(mCursor.getInt(4)); musicInfo.setMusicName(mCursor.getString(5)); musicInfo.setSize(mCursor.getInt(6)); musicInfo.setData(mCursor.getString(7)); //將資料裝載到List<Map<String ,String>>中 //獲取本地音樂專輯圖片 String MusicImage = getAlbumArt(mCursor.getInt(8)) ; //判斷本地專輯的圖片是否為空 if (MusicImage == null) { //為空,用預設圖片 map.put("image" , String.valueOf(R.mipmap.timg)) ; musicInfo.setAlbum_id(String.valueOf(R.mipmap.timg)); }else { //不為空,設定專輯圖片為音樂顯示的圖片 map.put("image" , MusicImage) ; musicInfo.setAlbum_id(MusicImage); } // musicInfo.setAlbum_id(mCursor.getInt(8)); musicInfos.add(musicInfo) ;
3 MediaPlayer重要方法:MediaPlayer用來指定音樂檔案和播放相關動作,所以我們需要掌握與音樂播放相關的方法。
方法: getCurrentPosition()
解釋:返回 Int, 得到當前播放位置 毫秒單位時間
方法: getDuration()
解釋:返回 Int,得到檔案的時間
方法:isLooping()
解釋:返回 boolean ,是否迴圈播放
方法:isPlaying()
解釋:返回 boolean,是否正在播放
方法:pause()
解釋:無返回值 ,暫停
方法:release()
解釋:無返回值,釋放 MediaPlayer 物件
方法:reset()
解釋:無返回值,重置 MediaPlayer 物件
方法:seekTo(int msec)
解釋:無返回值,指定播放的位置(以毫秒為單位的時間)
方法:setDataSource(String path)
解釋:無返回值,設定多媒體資料來源【根據 路徑】
方法:setLooping(boolean looping)
解釋:無返回值,設定是否迴圈播放
方法:start()
解釋:無返回值,開始播放
方法:stop()
解釋:無返回值,停止播放
方法:setVolume(float leftVolume, float rightVolume)
解釋:無返回值,設定音量
事件:setOnCompletionListener(MediaPlayer.OnCompletionListener listener)
解釋:監聽事件,網路流媒體播放結束監聽
事件:setOnBufferingUpdateListener(MediaPlayer.OnBufferingUpdateListener listener)
解釋:監聽事件,網路流媒體的緩衝監聽
事件:setOnErrorListener(MediaPlayer.OnErrorListener listener)
解釋:監聽事件,設定錯誤資訊監聽
4 歌詞處理 說下思路把:首先根據歌曲去記憶體尋找對應的歌詞檔案,找到後解碼為歌詞內容和歌詞時間,並且把歌詞內容和個時間裝載到歌詞資訊類集合中。然後提供一個外部訪問的方法用於向外部輸出歌詞資訊
// 注:LrcContent是歌詞處理類 其中包含兩個引數,歌詞時間和歌詞內容
public class LrcProcess {
//所有需要處理的歌詞物件
private List<LrcContent> lrcList;
//一個歌詞物件
private LrcContent mLrcContent;
/*
建構函式完成物件的初始化
*/
public LrcProcess() {
lrcList = new ArrayList<>();
}
/*
*從記憶體中讀取歌詞檔案,並轉換為String物件輸出
*/
public String readLrc(String path)
{
//用StringBuild來儲存歌詞內容
StringBuilder sb = new StringBuilder() ;
//獲取歌詞檔案 因為傳入的檔案為MusicInfo類中的Data內容,其檔案為mp3,需要更換為lrc檔案
File f = new File(path.replace(".mp3" , ".lrc")) ;
try{
//通過檔案流物件來獲取檔案內容並且匯入歌詞內容物件集合中(lrcList)
FileInputStream inputStream = new FileInputStream(f) ;
InputStreamReader streamReader = new InputStreamReader(inputStream , "utf-8") ;
BufferedReader bufferedReader = new BufferedReader(streamReader) ;
String tempStr = "" ;
while((tempStr = bufferedReader.readLine()) != null)
{
//實現字元替換
tempStr = tempStr.replace("[" , "") ;
tempStr = tempStr.replace("]" , "@") ;
//根據@分號對檔案分離
String[] splitData = tempStr.split("@") ;
// System.out.println("THE TEMP STR IS " + splitData[0]) ;
if (splitData.length > 1)
{
//新建歌詞內容物件
mLrcContent = new LrcContent();
//設定歌詞文字內容
mLrcContent.setLrcStr(splitData[1]);
//設定歌詞時間
int lrcTime = timeToStr(splitData[0]) ;
mLrcContent.setLrcTime(lrcTime);
//新增到列表
lrcList.add(mLrcContent);
// System.out.println("錄入歌詞成功") ;
}else {
// System.out.println("錄入歌詞失敗") ;
}
}
}catch (FileNotFoundException e)
{
sb.append("木有歌詞檔案,趕緊去下載!...");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
sb.append("木有讀取到歌詞哦!");
} catch (IOException e) {
e.printStackTrace();
sb.append("木有讀取到歌詞哦!");
}
return sb.toString() ;
}
/*
* 對歌詞檔案lrc中的時間內容進行轉碼
* [00:02.32]陳奕迅 時間分別代表分,秒,毫秒
* [00:03.43]好久不見
*/
public int timeToStr(String timeStr)
{
timeStr = timeStr.replace(":" , ".") ;
timeStr = timeStr.replace("." , "@") ;
String []splitTime = timeStr.split("@") ;
//分離出分, 秒, 毫秒
int minute = Integer.parseInt(splitTime[0]) ;
int second = Integer.parseInt(splitTime[1]) ;
int millisSecond = Integer.parseInt(splitTime[2]) ;
int time = (minute * 60 + second) * 1000 + millisSecond ;
return time ;
}
/*
提供一個外界方法歌詞物件集合的方法
*/
public List<LrcContent> getLrcList()
{
return lrcList ;
}
}
//例項化歌詞處理物件 Activity中呼叫方法
//musicInfosList為封裝好的所有音樂資訊類,get(position)可以獲取當前音樂資訊,getData獲取音樂路徑
LrcProcess mLrcProcess = new LrcProcess() ;
mLrcProcess.readLrc(musicInfosList.get(position).getData()) ;
這裡再補一個內容:通常經常下載的歌曲都是沒有歌詞檔案的,所以我們需要自己去下載歌詞檔案,注意該程式中你需要把歌詞檔案和音樂檔案放在一個地方,並且歌詞檔案和音樂檔案字首相同只是將.mp3改為了.lrc。我用的是網易雲音樂,這裡順便推薦一個網易雲下載歌詞的網址,大神們各種推薦供你選擇
5 歌曲播放介面
看圖分析:
(1)標題動畫
這兩個TextView的佈局程式碼就不再列出了。動畫效果主要是繼承了Animation類自定實現。
public class TextAnimation extends Animation { private float currentX ; //指定X private float currentY ; //指定Y //定義持續時間 private int duration ; //設定Camera private Camera camera = new Camera() ; public TextAnimation(float x , float y ,int duration) { currentX = x ; currentY = y ; this.duration = duration ; System.out.println("THE X IS " + currentX + "\nTHE Y IS " + currentY) ; } @Override public void initialize(int width, int height, int parentWidth, int parentHeight) { super.initialize(width, height, parentWidth, parentHeight); //設定持續時間 setDuration(duration); //設定動畫結束後保留 setFillAfter(true); //設定變換速度 setInterpolator(new AccelerateDecelerateInterpolator()); //減速 //setInterpolator(new AccelerateInterpolator()); //加速 預設情況 //setInterpolator(new LinearInterpolator()); //勻速 } @Override protected void applyTransformation(float interpolatedTime, Transformation t) { super.applyTransformation(interpolatedTime, t); /* 儲存 */ camera.save(); //根據interpolatedTime來控制X Y Z上的偏移 camera.translate(10.0f - 10.0f * interpolatedTime , 30.0f - 30.0f * interpolatedTime , 80.0f - 80.0f * interpolatedTime); //根據interpolatedTime在X軸做角度變換 camera.rotateX(360 * interpolatedTime); //根據interpolatedTime在Y軸做角度變換 camera.rotateY(360 * interpolatedTime); //獲取Transformation引數封裝的matrix物件 camera.getMatrix(t.getMatrix()); t.getMatrix().preTranslate(-currentX / 4 , -currentY / 4) ; t.getMatrix().postTranslate(currentX , currentY) ; /* 如果存在儲存的狀態,就恢復 */ camera.restore(); } }initialize(int width, int height, int parentWidth, int parentHeight)中,width和height代表指定播放動畫的View空間寬高,parentWidth和parentHeight代表該View控制元件所在的父控制元件寬高。可以在該方法中獲取View的寬和高,但是我在這裡面主要是完成的初始化
applyTransformation()方法是動畫具體的實現方法,在系統繪製動畫時會反覆呼叫這個方法,每呼叫一次applyTransformation()方法,其中的interpolatedTime引數都會改變一次,值從0到1遞增,當interpolatedTime的值為1時則動畫結束。Transformatio類是一個變換的矩陣,通過改變該矩陣就可以實現各種複雜的效果。PS:更多的相關的動畫知識可以閱讀官方文件,或者其它博文,書籍,網路視訊獲取。
Activity呼叫該動畫方法:
/* 獲取並且設定音樂的標題和歌手。並新增動畫來顯示 */
MusicArtist = (TextView) findViewById(R.id.MusicArtist) ; MusicName = (TextView) findViewById(R.id.MusicName) ; MusicName.setText(musicInfosList.get(position).getTitle()); MusicArtist.setText(musicInfosList.get(position).getArtist()); //動畫效果 MusicArtist.setAnimation(new TextAnimation(0 , 0 , 2000)); MusicName.setAnimation(new TextAnimation(0 , 0 , 2000));(2)音樂專輯圖片處理
1,圓形圖片
需要用到Shader
Shader的使用步驟:1. 構建Shader物件
2. 通過Paint的setShader方法設定渲染物件
3.設定渲染物件
4.繪製時使用這個Paint物件
public class XCRoundImageView extends android.support.v7.widget.AppCompatImageView{ private Paint mPaintBitmap = new Paint(Paint.ANTI_ALIAS_FLAG); private Bitmap mRawBitmap; private BitmapShader mShader; private Matrix mMatrix = new Matrix(); public XCRoundImageView(Context context, AttributeSet attrs) { super(context, attrs); } @Override protected void onDraw(Canvas canvas) { //獲取原生Bitmap點陣圖 Bitmap rawBitmap = getBitmap(getDrawable()); if (rawBitmap != null){ //獲取圖片寬和高 int viewWidth = getWidth(); int viewHeight = getHeight(); //由於要變換為圓形,在變換過程中取邊長相對小的為基準 int viewMinSize = Math.min(viewWidth, viewHeight); float dstWidth = viewMinSize; float dstHeight = viewMinSize; //如果是第一次繪製 if (mShader == null || !rawBitmap.equals(mRawBitmap)){ mRawBitmap = rawBitmap; /* BitmapShader是Shader的子類,可以通過Paint.setShader(Shader shader)進行設定、 這裡我們只關注BitmapShader,構造方法: mBitmapShader = new BitmapShader(bitmap, TileMode.CLAMP, TileMode.CLAMP); 引數1:bitmap 引數2,引數3:TileMode; TileMode的取值有三種: CLAMP 拉伸 REPEAT 重複 MIRROR 映象 如果大家給電腦螢幕設定屏保的時候,如果圖片太小,可以選擇重複、拉伸、映象; 重複:就是橫向、縱向不斷重複這個bitmap 映象:橫向不斷翻轉重複,縱向不斷翻轉重複; 拉伸:這個和電腦屏保的模式應該有些不同,這個拉伸的是圖片最後的那一個畫素;橫向的最後一個橫行畫素,不斷的重複,縱項的那一列畫素,不斷的重複; */ mShader = new BitmapShader(mRawBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); } if (mShader != null){ /* void setLocalMatrix(Matrix localM); 設定shader的本地矩陣,如果localM為空將重置shader的本地矩陣。 */ mMatrix.setScale(dstWidth / rawBitmap.getWidth(), dstHeight / rawBitmap.getHeight()); mShader.setLocalMatrix(mMatrix); } mPaintBitmap.setShader(mShader); float radius = viewMinSize / 2.0f; canvas.drawCircle(radius, radius, radius, mPaintBitmap); } else { super.onDraw(canvas); } } private Bitmap getBitmap(Drawable drawable){ if (drawable instanceof BitmapDrawable){ return ((BitmapDrawable)drawable).getBitmap(); } else if (drawable instanceof ColorDrawable){ Rect rect = drawable.getBounds(); int width = rect.right - rect.left; int height = rect.bottom - rect.top; int color = ((ColorDrawable)drawable).getColor(); Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(bitmap); canvas.drawARGB(Color.alpha(color), Color.red(color), Color.green(color), Color.blue(color)); return bitmap; } else { return null; } } }
PS:作者當時也是搬運的,只是補充了部分註釋。內容可能有點複雜,需要多實踐,建議參考資料文件
自定義控制元件三部曲之繪圖篇(十八)——BitmapShader與望遠鏡效果2,圖片旋轉首先定義旋轉動畫 :image_rotate.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@android:anim/linear_interpolator">
<rotate
android:fromDegrees="0"
android:toDegrees="359"
android:pivotX="50%"
android:pivotY="50%"
android:duration="8000"
/>
</set>
然後在Java業務程式碼中設定定時器來啟動動畫
/* 該Timer用於實現:音樂播放介面圖片旋轉動畫 */ MyHandle2 handle2 = new MyHandle2() ; new Timer().schedule(new TimerTask() { @Override public void run() { handle2.sendEmptyMessage(0x112) ; } }, 0 ,8000);
最後再Handle中接收訊息,並且啟動動畫
public class MyHandle2 extends Handler { @Override public void handleMessage(Message msg) { if ((msg.what == 0x112)) { //設定圖片旋轉 MusicImage.setAnimation(AnimationUtils.loadAnimation(MusicPlay.this , R.anim.image_rotate)); } } }
五:下載地址github
六:參考資料
七:總結
第一次寫這麼長文的部落格,也沒想過邀功。不過真心希望您再閱讀了這篇博文,並且有所建議和收穫後留下您寶貴的評論。謝謝。
PS:轉載請註明 https://blog.csdn.net/qq_29989087/article/details/80206290