仿網易雲音樂播放介面
前言
網易雲音樂是一款非常優秀的音樂播放器,尤其是播放介面,使用唱盤機風格,顯得格外古典優雅。筆者出於學習與挑戰的想法,思考播放介面背後的實現原理,並寫了一個小程式。
筆者儘可能地去模仿官方的視覺、互動效果,其中包括了唱盤與唱針切換時的細節處理、背景漸變等。本文將會分享一些視覺效果實現的方法以及設計思想,但難免有錯漏之處。若讀者發現有錯誤的地方或者更好的實現方法,請留言回覆,希望與大家共同進步。
1 原始碼地址
需要原始碼的朋友,可以到github中自行下載。Android開發技術進階群;701740775。加群的朋友麻煩備註一下csdn
github;https://github.com/AchillesLzg/jianshu-neteasedisc
2 本文內容
- 專案結構介紹
- 解決載入大圖OOM問題
- 生成圓圖最簡單的方法
- 使用LayerDrawable進行圖片合成
- 實現背景毛玻璃效果
- 使用LayerDrawable與屬性動畫,實現背景切換時漸變效果
- 遇到複雜的場景,應該如何編寫程式碼
- 配合Service、本地廣播進行音樂播放
- 結束語
3 專案結構介紹
專案結構介紹包括以下內容:
- 主介面佈局設計
- 唱盤佈局設計
- 動態佈局
- 唱盤控制元件DiscView對外介面及方法
- 音樂狀態控制時序圖
3.1主介面佈局設計
主介面佈局從上到下可以劃分幾大區域,如圖3-1所示:
圖 3-1 主介面佈局
標題欄
使用ToolBar實現,字型可能需要自定義。
唱盤區域
唱盤區域包括唱盤、唱針、底盤、以及實現切換的ViewPager等控制元件,該佈局比較複雜,本案例使用自定義控制元件實現唱盤區域。
時長顯示區域
使用RelativeLayout作為根佈局,進度條使用SeekBar實現。
播放控制區域
比較簡單,使用LinearLayout作為根佈局。
另外,主介面使用RelativeLayout作為根佈局。
3.2 唱盤佈局設計
唱盤區域由控制元件DiscView實現,以RelativeLayout為根佈局,子控制元件包括:底盤、唱針、ViewPager等。其中,底盤和唱針均用ImageView實現,然後使用ViewPager載入ImageView實現唱片的切換。如圖3-2所示。
圖 3-2 唱盤區域佈局
唱盤佈局程式碼如下所示:
<?ml version="1.0" encoding="utf-8"?>
<com.achillesl.neteasedisc.widget.DiscView
mlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<!--底盤-->
<ImageView
android:id="@+id/ivDiscBlackgound"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
/>
<!--ViewPager實現唱片切換-->
<android.support.v4.view.ViewPager
android:id="@+id/vpDiscContain"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
/>
<!--唱針-->
<ImageView
android:id="@+id/ivNeedle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_needle"/>
</com.achillesl.neteasedisc.widget.DiscView>
3.3 動態佈局
到這裡,讀者可能有些好奇,上述佈局中並沒有指定控制元件的寬高、邊距等引數,那如何保證控制元件顯示在正確的位置?我們沒有網易雲音樂的設計圖,因此不能得知官方的佈局引數,那該怎麼辦呢?其實有個笨方法,我們可以開啟網易雲音樂的播放介面並截圖,然後手動去量需要的高度、邊距等引數。
截圖量到控制元件的寬高、邊距等數值,除以截圖的寬或高,得到控制元件引數比例。使用時,我們根據手機的螢幕寬高,乘以對應的比例,就能得到該螢幕尺寸下的控制元件寬高、邊距。
當然,這種動態佈局肯定會消耗更多效能,但不失為沒有辦法中的辦法。
相關控制元件引數比例,筆者統一放在DisplayUtil.java檔案中,程式碼如下:
public class DisplayUtil {
/*手柄起始角度*/
public static final float ROTATION_INIT_NEEDLE = -30;
/*截圖螢幕寬高*/
private static final float BASE_SCREEN_WIDTH = (float) 1080.0;
private static final float BASE_SCREEN_HEIGHT = (float) 1920.0;
/*唱針寬高、距離等比例*/
public static final float SCALE_NEEDLE_WIDTH = (float) (276.0 / BASE_SCREEN_WIDTH);
public static final float SCALE_NEEDLE_MARGIN_LEFT = (float) (500.0 / BASE_SCREEN_WIDTH);
public static final float SCALE_NEEDLE_PIVOT_ = (float) (43.0 / BASE_SCREEN_WIDTH);
public static final float SCALE_NEEDLE_PIVOT_Y = (float) (43.0 / BASE_SCREEN_WIDTH);
public static final float SCALE_NEEDLE_HEIGHT = (float) (413.0 / BASE_SCREEN_HEIGHT);
public static final float SCALE_NEEDLE_MARGIN_TOP = (float) (43.0 / BASE_SCREEN_HEIGHT);
/*唱盤比例*/
public static final float SCALE_DISC_SIZE = (float) (813.0 / BASE_SCREEN_WIDTH);
public static final float SCALE_DISC_MARGIN_TOP = (float) (190 / BASE_SCREEN_HEIGHT);
/*專輯圖片比例*/
public static final float SCALE_MUSIC_PIC_SIZE = (float) (533.0 / BASE_SCREEN_WIDTH);
/*裝置螢幕寬度*/
public static int getScreenWidth(Contet contet) {
return contet.getResources().getDisplayMetrics().widthPiels;
}
/*裝置螢幕高度*/
public static int getScreenHeight(Contet contet) {
return contet.getResources().getDisplayMetrics().heightPiels;
}
}
例如需要設定唱盤底盤的頂部外邊距,我們先獲得該比例,然後乘上當前螢幕高度,得到具體數值,最後通過LayoutParams類進行動態設定。
int marginTop = (int) (DisplayUtil.SCALE_DISC_MARGIN_TOP * mScreenHeight);
RelativeLayout.LayoutParams layoutParams = (LayoutParams) mDiscBlackground.getLayoutParams();
layoutParams.setMargins(0, marginTop, 0, 0);
3.4 DiscView對外介面及方法
唱盤控制元件DiscView提供一個介面IPlayInfo,程式碼如下:
public interface IPlayInfo {
/*用於更新標題欄變化*/
public void onMusicInfoChanged(String musicName, String musicAuthor);
/*用於更新背景圖片*/
public void onMusicPicChanged(int musicPicRes);
/*用於更新音樂播放狀態*/
public void onMusicChanged(MusicChangedStatus musicChangedStatus);
}
介面IPlayInfo中包含三個方法,分別用於更新標題欄(音樂名、作者名)、更新背景圖片以及控制音樂播放狀態(播放、暫停、上/下一首等)。
讀者可能有些疑問?
1. IPlayInfo介面的第一、二個方法屬於同一型別,為何要拆成兩個?
2. 為何通過回撥來控制音樂播放?點選主介面的控制按鈕時,直接控制音樂播放不也可以嗎?
這兩個問題,筆者也是經過多次考慮。
第一個問題,首先網易雲音樂互動上,更新標題欄和更新背景圖的時機不一樣(ViewPager偏移頁面1/2時更新標題欄,而背景圖是ViewPager是停止滑動後才更新)。若兩個介面合併為一個,一來不利於解耦,二來可能造成開發者誤解,並且造成資源浪費。
第二個問題,筆者考慮到,點選主介面的控制按鈕,並不代表立刻需要發生音樂的狀態變更(比如點選播放按鈕,需要等唱針動畫結束後才能開始播放音樂)。因此,控制音樂的時機是依賴與DiscView的狀態。因此,我們通過介面中的onMusicChanged方法在適合的時間先將音樂控制回撥到Activity,再通過Activity傳送指令,來達到切換音樂狀態的效果。
點選主介面播放/暫停、上/下一首按鈕時,呼叫DiscView暴露的方法:
@Override
public void onClick(View v) {
if (v == mIvPlayOrPause) {
mDisc.playOrPause();
} else if (v == mIvNet) {
mDisc.net();
} else if (v == mIvLast) {
mDisc.last();
}
}
當主介面收到DiscView回撥時,呼叫相關方法控制音樂播放:
public void onMusicChanged(MusicChangedStatus musicChangedStatus) {
switch (musicChangedStatus) {
case PLAY:{
play();
break;
}
case PAUSE:{
pause();
break;
}
case NET:{
net();
break;
}
case LAST:{
last();
break;
}
case STOP:{
stop();
break;
}
}
}
3.5 音樂狀態控制時序圖
圖 3-3 音樂狀態控制時序圖
音樂控制狀態時序如圖3-3所示,點選Activity的按鈕時,先呼叫DiscView的相關方法,並在合適的時機(如動畫結束)再將狀態回撥到Activity,並通過廣播傳送指令到Service,實現音樂狀態切換,最後通過廣播更新UI狀態。
專案架構介紹到這裡,接下來是部分視覺效果以及設計思路的介紹。
4 解決載入大圖OOM問題
載入大圖避免OOM(記憶體溢位),這是一個老生常談的話題,筆者以後會有 專門的文章來講述這方面的內容,這裡先放出結論。
解決大圖載入一般有幾種方案:
1. 設定largeHeap為true。
2. 根據圖片型別選定解碼格式。
3. 根據原始圖片寬高及目標顯示寬高,設定圖片取樣率。
第一種方法,可以增加了堆記憶體空間,但這種方法僅僅延後了OOM發生的時機,治標不治本,不推薦使用該方法。
第二種方法,Android對圖片進行解碼時,預設是採用ARGB_8888格式,即每個畫素佔32位,如果圖片格式是jpg,那麼用ARGB_8888來解析自然是浪費,因為jpg圖片沒有透明通道。一般我們採用RGB_565格式來對jpg圖片解碼,RGB_565即每個畫素點佔16位,因此解碼後圖片的記憶體佔用僅僅是使用ARGB_8888解碼的一半。
第三種方法,這也是網上最普遍方式,也是最有通用的,取樣率可以理解成:當取樣率為4,表示將4個點“合併”為一個點來讀出,縮小圖片尺寸的同時也減少了圖片佔用空間,這樣解碼得到出來的圖片佔用空間自然比原圖少。
以載入音樂專輯圖片的程式碼為例:
private Bitmap getMusicPicBitmap(int musicPicSize, int musicPicRes) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(getResources(),musicPicRes,options);
int imageWidth = options.outWidth;
int sample = imageWidth / musicPicSize;
int dstSample = 1;
if (sample > dstSample) {
dstSample = sample;
}
options.inJustDecodeBounds = false;
//設定圖片取樣率
options.inSampleSize = dstSample;
//設定圖片解碼格式
options.inPreferredConfig = Bitmap.Config.RGB_565;
return Bitmap.createScaledBitmap(BitmapFactory.decodeResource(getResources(),
musicPicRes, options), musicPicSize, musicPicSize, true);
}
上面程式碼中,我們先設定options.inJustDecodeBounds = true,這樣BitmapFactory.decodeResource的時候僅僅會載入圖片的一些資訊,然後通過options.outWidth獲取到圖片的寬度,根據目標圖片尺寸算出取樣率。最後通過inPreferredConfig設定解碼格式,才正式載入圖片。
5 生成圓圖最簡單的方法
我們看到,網易雲音樂唱盤背後有個底座,是個透明的圓形圖,如圖5-1所示。筆者找過所有網易雲音樂的圖片資源,只發現了一張透明的方形圖,看來我們需要自己生成圓形圖片了。
圖 5-1 唱盤底座
生成圓圖有各種各樣的方式,比如自定義控制元件複寫onDraw方法、給圖片加上圓形蒙版等,網上都有很多資料,在此不再多說。
在此給大家分享一種筆者認為最簡單的方式:
RoundedBitmapDrawable是android.support.v4.graphics.drawable 裡面的一個類,通過這個類可以很容易實現圓角和圓形圖片。
用法:
使用RoundedBitmapDrawable生成圓形圖,先要將初始圖片調整為正方形,由於網易雲音樂的這張圖片本身就是方形,因此筆者將這一步省略。
程式碼非常簡單,程式碼如下:
private Drawable getDiscBlackgroundDrawable() {
int discSize = (int) (mScreenWidth * DisplayUtil.SCALE_DISC_SIZE);
Bitmap bitmapDisc = Bitmap.createScaledBitmap(BitmapFactory.decodeResource(getResources(), R
.drawable.ic_disc_blackground), discSize, discSize, false);
RoundedBitmapDrawable roundDiscDrawable = RoundedBitmapDrawableFactory.create
(getResources(), bitmapDisc);
return roundDiscDrawable;
}
我們將圖片資原始檔轉為Bitmap物件,然後初始化RoundedBitmapDrawable物件,然後直接返回該物件就可以了。
6 使用LayerDrawable進行圖片合成
這一步,主要用於合成唱盤與專輯圖片,如圖6-1所示。筆者用UI Automation工具檢視網易雲音樂唱盤佈局時,發現裡面用了兩個ImageView,估計是一個用來顯示唱盤,一個用來顯示專輯圖片(並不確定)。但如果可以將唱盤與專輯圖片合併成一張圖,那使用一個ImageView就夠了。
圖 6-1
LayerDrawable介紹:
LayerDrawable也可包含一個Drawable陣列,因此係統將會按這些Drawable物件的陣列順序來繪製它們,索引最大的Drawable物件將會被繪製在最上面。 LayerDrawable有點類似PhotoShop圖層的概念。
思路:
1. 生成圓形的專輯圖。
2. 使用LayerDrawable載入唱盤及專輯圖片。
3. 調整專輯圖的邊距,讓它顯示在唱盤的正中間。
4. 在ImageView中顯示。
程式碼:
private Drawable getDiscDrawable(int musicPicRes) {
int discSize = (int) (mScreenWidth * DisplayUtil.SCALE_DISC_SIZE);
int musicPicSize = (int) (mScreenWidth * DisplayUtil.SCALE_MUSIC_PIC_SIZE);
Bitmap bitmapDisc = Bitmap.createScaledBitmap(BitmapFactory.decodeResource(getResources(), R
.drawable.ic_disc), discSize, discSize, false);
Bitmap bitmapMusicPic = getMusicPicBitmap(musicPicSize,musicPicRes);
BitmapDrawable discDrawable = new BitmapDrawable(bitmapDisc);
RoundedBitmapDrawable roundMusicDrawable = RoundedBitmapDrawableFactory.create
(getResources(), bitmapMusicPic);
//抗鋸齒
discDrawable.setAntiAlias(true);
roundMusicDrawable.setAntiAlias(true);
Drawable[] drawables = new Drawable[2];
drawables[0] = roundMusicDrawable;
drawables[1] = discDrawable;
LayerDrawable layerDrawable = new LayerDrawable(drawables);
int musicPicMargin = (int) ((DisplayUtil.SCALE_DISC_SIZE - DisplayUtil
.SCALE_MUSIC_PIC_SIZE) * mScreenWidth / 2);
//調整專輯圖片的四周邊距
layerDrawable.setLayerInset(0, musicPicMargin, musicPicMargin, musicPicMargin,
musicPicMargin);
return layerDrawable;
}
在上面程式碼中,我們先生成了唱盤物件BitmapDrawable,然後通過RoundedBitmapDrawable生成圓形專輯圖片,然後存放到Drawable[]陣列中,並用來初始化LayerDrawable物件。最後,我們用setLayerInset方法調整專輯圖片的四周邊距,讓它顯示在唱盤正中。
7 實現背景毛玻璃效果
顯而易見地,網易雲音樂的背景圖是由專輯圖片加上毛玻璃效果而生成的,如圖7-1所示。
圖 7-1 毛玻璃效果
毛玻璃效果,我們可以StackBlur模糊演算法來實現,這種演算法應用非常廣泛,能得到非常良好的毛玻璃效果。在這裡我們使用它的java實現。
用法如下:
public static Bitmap doBlur(Bitmap sentBitmap, int radius, boolean canReuseInBitmap)
第一個引數是需要模糊處理的Bitmap,第二個引數是模糊半徑(一般設定為8),第三個參數列示是否複用。
對圖片進行模糊化之前,我們先針對播放介面思考幾個問題:
1. 網易雲音樂專輯圖均為方形,若將專輯圖全屏載入會造成圖片變形。
2. 直接對大圖模糊化很容易出現OOM,同時效能也有所損耗。
3. 可能有部分專輯圖片顏色過亮(如純白色),會影響按鈕的視覺效果。
第一點,比較容易解決,我們可以在原圖中部,切割一個與螢幕寬高比例對應的圖片即可。
第二點,**做圖片模糊化處理前,我們一般先對大圖進行縮小處理,再用演算法進行模糊,這樣不容易出現OOM,對效能也沒影響。 **
第三點,我們可以在圖片模糊化後的基礎上,加上灰色遮罩層,這樣就算是純白背景,也不會對主介面的控制元件造成視覺影響。
程式碼如下所示:
private Drawable getForegroundDrawable(int musicPicRes) {
/*得到螢幕的寬高比,以便按比例切割圖片一部分*/
final float widthHeightSize = (float) (DisplayUtil.getScreenWidth(MainActivity.this)
*1.0 / DisplayUtil.getScreenHeight(this) * 1.0);
Bitmap bitmap = getForegroundBitmap(musicPicRes);
int cropBitmapWidth = (int) (widthHeightSize * bitmap.getHeight());
int cropBitmapWidth = (int) ((bitmap.getWidth() - cropBitmapWidth) / 2.0);
/*切割部分圖片*/
Bitmap cropBitmap = Bitmap.createBitmap(bitmap, cropBitmapWidth, 0, cropBitmapWidth,
bitmap.getHeight());
/*縮小圖片*/
Bitmap scaleBitmap = Bitmap.createScaledBitmap(cropBitmap, bitmap.getWidth() / 50, bitmap
.getHeight() / 50, false);
/*模糊化*/
final Bitmap blurBitmap = FastBlurUtil.doBlur(scaleBitmap, 8, true);
final Drawable foregroundDrawable = new BitmapDrawable(blurBitmap);
/*加入灰色遮罩層,避免圖片過亮影響其他控制元件*/
foregroundDrawable.setColorFilter(Color.GRAY, PorterDuff.Mode.MULTIPLY);
return foregroundDrawable;
}
考慮到這部分程式碼可能會阻塞UI執行緒,因此筆者將其放著單獨執行緒中執行。
private void try2UpdateMusicPicBackground(final int musicPicRes) {
if (mRootLayout.isNeed2UpdateBackground(musicPicRes)) {
new Thread(new Runnable() {
@Override
public void run() {
final Drawable foregroundDrawable = getForegroundDrawable(musicPicRes);
runOnUiThread(new Runnable() {
@Override
public void run() {
mRootLayout.setForeground(foregroundDrawable);
mRootLayout.beginAnimation();
}
});
}
}).start();
}
}
8 使用LayerDrawable與屬性動畫,實現背景切換時漸變效果
仔細觀察網易雲音樂,發現切換歌曲時,背景圖也會隨著變化,如圖8-1所示,變化時還帶有一個漸變的效果。筆者曾經也是為這個效果想了很長時間,這效果究竟是怎麼實現的?後來筆者想到了一個很簡單的方法,可以用前面介紹的LayerDrawable加屬性動畫來實現。
圖 8-1 背景圖漸變效果
思路如下:
1. 給LayerDrawable設定兩個圖層,第一圖層是前一個背景,第二圖層是準備顯示的背景。
2. 先把準備顯示的背景透明度設為0,因此完全透明,此時只顯示前一個背景圖。
3. 通過屬性動畫,動態將第二圖層的透明度從0調整至100,並不斷更新控制元件的背景。
有了思路,寫程式碼就簡單了。我們通過RelativeLayout來顯示背景,考慮到需要對程式碼進行封裝,我們自定義一個類BackgourndAnimationRelativeLayout繼承RelativeLayout,並在該類中實現上述的思路,關鍵程式碼如下:
/**
* 自定義一個控制元件,繼承RelativeLayout
**/
public class BackgourndAnimationRelativeLayout etends RelativeLayout
//初始化LayerDrawable物件
private void initLayerDrawable() {
Drawable backgroundDrawable = getContet().getDrawable(R.drawable.ic_blackground);
Drawable[] drawables = new Drawable[2];
/*初始化時先將前景與背景顏色設為一致*/
drawables[INDE_BACKGROUND] = backgroundDrawable;
drawables[INDE_FOREGROUND] = backgroundDrawable;
layerDrawable = new LayerDrawable(drawables);
}
private void initObjectAnimator() {
objectAnimator = ObjectAnimator.ofFloat(this, "number", 0f, 1.0f);
objectAnimator.setDuration(DURATION_ANIMATION);
objectAnimator.setInterpolator(new AccelerateInterpolator());
objectAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int foregroundAlpha = (int) ((float) animation.getAnimatedValue() * 255);
/*動態設定Drawable的透明度,讓前景圖逐漸顯示*/
layerDrawable.getDrawable(INDE_FOREGROUND).setAlpha(foregroundAlpha);
BackgourndAnimationRelativeLayout.this.setBackground(layerDrawable);
}
});
objectAnimator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
/*動畫結束後,記得將原來的背景圖及時更新*/
layerDrawable.setDrawable(INDE_BACKGROUND, layerDrawable.getDrawable(
INDE_FOREGROUND));
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
}
//對外提供方法,用於播放漸變動畫
public void beginAnimation() {
objectAnimator.start();
}
9 遇到複雜的場景,應該如何編寫程式碼
這是個很有趣的問題,我們平時寫程式碼也會遇到很多複雜的場景。就拿網易雲音樂來說,我們可以仔細觀察唱針動畫的細節,你會發現網易雲音樂是多麼優秀的應用。
唱針動畫細節:
- 初始狀態為暫停/停止時,點選播放按鈕,此時唱針移動到底部。 圖9-1所示。
圖 9-1
- 初始狀態為播放時,點選暫停按鈕,此時唱針移到頂部。 如圖9-2所示。
圖 9-2
- 初始狀態為播放時,手指按住唱盤並稍微偏移,等唱針未移到頂部時,立刻鬆開手指,此時唱針回到頂部後立刻再回到唱盤位置。如圖9-3所示。
圖 9-3
- 初始狀態為暫停/停止時,點選播放,此時唱針往下移動,當唱針還未移到底部,手指馬上按住唱盤並偏移,此時唱針立刻往頂部移動。如圖9-4所示 。
圖 9-4
- 初始狀態為播放/暫停/停止時,左右滑動唱片進行音樂切換,唱針動畫未結束時,立刻點選上/下一首按鈕,進行音樂切換,此時唱針狀態不能出現混亂。如圖9-5所示。
圖 9-5
第1、2個效果很容易實現,只要監聽ViewPager的狀態配合屬性動畫就可以了,但第3、4、5個效果(本質上5和3、4一致)實現起來就有難度了。當然我們可以簡單地加boolean標記暴力解決,標記雖然可以用,但不能隨意用,否則變數多起來程式碼可讀性句變得非常差了。
筆者在這談一點自己的心得體會,遇到這種問題,原則如下:
冷靜分析,簡化並找到不同場景的被觸發的狀態。
我們仔細分析上述的幾種場景,無非和兩個因素有關:唱片是否偏離以及動作觸發時,唱針所處的位置。因此,我們可以把狀態分為兩類,六種狀態:
1. 唱片狀態(兩種):包括偏移中、偏移結束。 如圖9-6所示。
2. 唱針的狀態(四種):處於遠端(遠離唱片)、處於近端(貼近唱片)、正在從遠端往近端移動、正在從近端往遠端移動。如圖9-7所示。
圖 9-6 ViewPager的狀態
圖 9-7 唱針的狀態
其中,唱片(即ViewPager)的狀態可以通過PageChangeListener得到。唱針的狀態,筆者用列舉來表示,並且在動畫的開始、結束時對唱針狀態及時更新。
唱針狀態列舉:
private enum NeedleAnimatorStatus {
/*移動時:從唱盤往遠處移動*/
TO_FAR_END,
/*移動時:從遠處往唱盤移動*/
TO_NEAR_END,
/*靜止時:離開唱盤*/
IN_FAR_END,
/*靜止時:貼近唱盤*/
IN_NEAR_END
}
動畫開始時,更新唱針狀態:
@Override
public void onAnimationStart(Animator animator) {
/**
*根據動畫開始前NeedleAnimatorStatus的狀態,
*即可得出動畫進行時NeedleAnimatorStatus的狀態
**/
if (needleAnimatorStatus == NeedleAnimatorStatus.IN_FAR_END) {
needleAnimatorStatus = NeedleAnimatorStatus.TO_NEAR_END;
} else if (needleAnimatorStatus == NeedleAnimatorStatus.IN_NEAR_END) {
needleAnimatorStatus = NeedleAnimatorStatus.TO_FAR_END;
}
}
動畫結束時,更新唱針狀態:
@Override
public void onAnimationEnd(Animator animator) {
if (needleAnimatorStatus == NeedleAnimatorStatus.TO_NEAR_END) {
needleAnimatorStatus = NeedleAnimatorStatus.IN_NEAR_END;
int inde = mVpContain.getCurrentItem();
playDiscAnimator(inde);
} else if (needleAnimatorStatus == NeedleAnimatorStatus.TO_FAR_END) {
needleAnimatorStatus = NeedleAnimatorStatus.IN_FAR_END;
}
}
每種狀態都定義清楚,這樣程式碼寫起來就相當清晰了。
比如需要播放動畫時,就包含兩個狀態:
1. 唱針動畫暫停中,唱針處於遠端。
2. 唱針動畫播放中,唱針處於從近端往遠端移動(上述場景3的問題)
/*播放動畫*/
private void playAnimator() {
/*唱針處於遠端時,直接播放動畫*/
if (needleAnimatorStatus == NeedleAnimatorStatus.IN_FAR_END) {
mNeedleAnimator.start();
}
/*唱針處於往遠端移動時,設定標記,等動畫結束後再播放動畫*/
else if (needleAnimatorStatus == NeedleAnimatorStatus.TO_FAR_END) {
mIsNeed2StartPlayAnimator = true;
}
}
再比如需要暫停動畫時,也包含兩種狀態:
1. 唱針動畫暫停中,唱針處於近端時。
2. 唱針動畫播放中,唱針往近端移動時(解決上述場景第4個細節問題)
/*暫停動畫*/
private void pauseAnimator() {
/*播放時暫停動畫*/
if (needleAnimatorStatus == NeedleAnimatorStatus.IN_NEAR_END) {
int index = mVpContain.getCurrentItem();
pauseDiscAnimatior(index);
}
/*唱針往唱盤移動時暫停動畫*/
else if (needleAnimatorStatus == NeedleAnimatorStatus.TO_NEAR_END) {
mNeedleAnimator.reverse();
/**
* 若動畫在沒結束時執行reverse方法,則不會執行監聽器的onStart方法,此時需要手動設定
* */
needleAnimatorStatus = NeedleAnimatorStatus.TO_FAR_END;
}
/**
* 動畫可能執行多次,只有音樂處於停止 / 暫停狀態時,才執行暫停命令
* */
if (musicStatus == MusicStatus.STOP) {
notifyMusicStatusChanged(MusicChangedStatus.STOP);
} else if (musicStatus == MusicStatus.PAUSE) {
notifyMusicStatusChanged(MusicChangedStatus.PAUSE);
}
}
10 配合Service、本地廣播進行音樂播放
10.1 為什麼選擇Service?
面試的時候,可能面試官會問:什麼場景下,使用Service會比Activity更有優勢?很多人回答,執行在不需要顯示介面的場景時,但沒有沒有說出具體場景。
播放音訊,需要用到MediaPlayer類。筆者認為,對MediaPlayer的控制,放在Service處理再好不過了:Service不需要介面,但更重要的是,不需要處理螢幕旋轉的邏輯。如果把MediaPlayer放在Activity,螢幕旋轉時,因為Activity會重建,估計還要儲存ediaPlayer的各種狀態,使用Service就沒有這個顧慮了。
筆者的案例不涉及到螢幕旋轉處理,為了演示流程,還是使用Service處理音樂播放。
10.2 為什麼選擇本地廣播?
Android的廣播時全域性的,一旦發出,會在系統內傳播,如果有其他APP得知你的Action資訊和許可權,可能造成資訊洩露,甚至可以傳送廣播來控制你的APP。而本地廣播(LocalBroadcast)只在應用內部傳遞,則不會有這個顧慮。
本地廣播有以下幾個特點:
- **廣播在應用內部傳播,不必擔心資訊洩露。 **
- 別的應用無法傳送廣播來控制你的APP,更加安全。
- 傳送的廣播不需要系統中轉,效率更高。
本地廣播的用法很簡單,如下:
註冊:
LocalBroadcastManager.getInstance(this).registerReceiver(receiver, intentFilter);
取消註冊:
LocalBroadcastManager.getInstance(this).unregisterReceiver(receiver);
廣播傳送:
LocalBroadcastManager.getInstance(contet).sendBroadcast(new Intent(ACTION));
10.3 進度條的處理
本案例中,我們使用Handler+SeekBar實現歌曲進度動態更新,通過Handler的sendEmptyMessageDelayed方法,每隔1秒傳送一個事件。當接收到事件時,更新SeekBar的進度然後再次呼叫sendEmptyMessageDelayed方法,這樣就可以實現進度的動態更新。
關鍵程式碼如下所示:
private Handler mMusicHandler = new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
mSeekBar.setProgress(mSeekBar.getProgress() + 1000);
mTvMusicDuration.setTet(duration2Time(mSeekBar.getProgress()));
startUpdateSeekBarProgress();
}
};
private void startUpdateSeekBarProgress() {
/*避免重複傳送Message*/
stopUpdateSeekBarProgree();
mMusicHandler.sendEmptyMessageDelayed(0,1000);
}
當SeekBar滑動時,使用removeMessages方法移除Handler中的延時訊息,暫停Handler對SeekBar的更新。當SeekBar滑動結束後,根據當前的進度值來更新音樂播放的位置。
關鍵程式碼如下所示:
mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
mTvMusicDuration.setTet(duration2Time(progress));
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
stopUpdateSeekBarProgree();
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
seekTo(seekBar.getProgress());
startUpdateSeekBarProgress();
}
});
11 結束語
本案例還可以進行更多的優化,比如ViewPager無限切換、顯示歌詞等。但邊幅有限,本章的內容就先到此結束,希望能起到拋磚引玉的作用。
能這麼多看完的都是真愛啊 給你們分享一波Android高階進階資料。加Android技術群免費領取;701740775。備註一下csdn
相關文章
- Flutter仿網易雲音樂:播放介面Flutter
- iOS仿網易雲音樂iOS
- 仿網易雲音樂webAppWebAPP
- Taro小程式仿網易雲音樂
- 高仿網易雲音樂 Vue前端專案Vue前端
- Android專案實戰之高仿網易雲音樂啟動介面Android
- 全新版本仿網易雲音樂來啦
- 使用jQuery仿製網易雲音樂移動端jQuery
- HTML+CSS+JAVASCRIPT 高仿低配網頁版網易雲音樂播放器HTMLCSSJavaScript網頁播放器
- Electron+Vue實現仿網易雲音樂實戰Vue
- Android開源音樂播放器之高仿雲音樂黑膠唱片Android播放器
- HTML網頁呼叫 網易雲 音樂播放器程式碼HTML網頁播放器
- 音樂播放器WordPress外掛 WP-Player (支援網易雲音樂, 蝦米音樂, QQ音樂, 百度音播放器
- AI音樂,騰訊音樂、網易雲音樂的新版圖?AI
- 在自己網站中插入網易雲音樂的外鏈,播放音樂網站
- Vue全家桶高仿網易雲音樂mac客戶端版(可能是西湖區最精美的web pc音樂播放器)VueMac客戶端Web播放器
- SimpleMusic-網易雲音樂高仿-安卓安卓
- LRC歌詞原理和實現高仿Android網易雲音樂Android
- 微信小程式-網易雲音樂微信小程式
- 【專案實戰:跟小K來擼一個菜鳥音樂APP】—高仿網易雲音樂APP
- Android專案實戰之高仿網易雲音樂專案介紹Android
- YesPlayMusic for Mac(高顏值網易雲音樂第三方播放器)支援vip播放Mac播放器
- win10系統怎麼把網易雲音樂設定成預設音樂播放器Win10播放器
- 網易雲音樂介面+vue全家桶開發一款移動端音樂webAppVueWebAPP
- Flutter實戰 | 從 0 搭建「網易雲音樂」APP(五、播放功能邏輯)FlutterAPP
- 卡拉OK歌詞原理和實現高仿Android網易雲音樂Android
- Android專案實戰之高仿網易雲音樂建立專案和配置Android
- 網易雲音樂財報:2022年網易雲音樂營收90億元 同比增長28.5%營收
- Vue 實現網易雲音樂 WebAppVueWebAPP
- 基於 React + TypeScript 的網易雲音樂ReactTypeScript
- 網易雲音樂機器學習平臺實踐機器學習
- RocketMQ 在網易雲音樂的實踐MQ
- 網易雲音樂解鎖灰色歌曲教程
- 網易雲音樂的一個評論
- 奇妙音樂屋!一個基於Vue3高仿網易雲PC端的音樂流媒體網站Vue網站
- Flutter實戰 | 從 0 搭建「網易雲音樂」APP(四、排行榜、播放頁面)FlutterAPP
- Python爬蟲實踐-網易雲音樂Python爬蟲
- 基於網易雲音樂的 SPA 應用