仿網(wǎng)易云音樂播放界面

原創(chuàng)作者:AchillesL
若轉(zhuǎn)載文章谅阿,請?jiān)诿黠@的位置標(biāo)明文章出處

0 前言

??網(wǎng)易云音樂是一款非常優(yōu)秀的音樂播放器,尤其是播放界面绘证,使用唱盤機(jī)風(fēng)格缩抡,顯得格外古典優(yōu)雅辛燥。筆者出于學(xué)習(xí)與挑戰(zhàn)的想法,思考播放界面背后的實(shí)現(xiàn)原理,并寫了一個(gè)小程序挎塌。

??筆者盡可能地去模仿官方的視覺徘六、交互效果,其中包括了唱盤與唱針切換時(shí)的細(xì)節(jié)處理榴都、背景漸變等待锈。本文將會分享一些視覺效果實(shí)現(xiàn)的方法以及設(shè)計(jì)思想,但難免有錯(cuò)漏之處嘴高。若讀者發(fā)現(xiàn)有錯(cuò)誤的地方或者更好的實(shí)現(xiàn)方法竿音,請留言回復(fù),希望與大家共同進(jìn)步拴驮。效果如下圖所示:

效果圖 使用系統(tǒng)瀏覽器查看效果更佳

1 源碼地址

??需要源碼的讀者春瞬,可以到github中自行下載:
??https://github.com/AchillesLzg/jianshu-neteasedisc

2 本文內(nèi)容

  • 項(xiàng)目結(jié)構(gòu)介紹
  • 解決加載大圖OOM問題
  • 生成圓圖最簡單的方法
  • 使用LayerDrawable進(jìn)行圖片合成
  • 實(shí)現(xiàn)背景毛玻璃效果
  • 使用LayerDrawable與屬性動(dòng)畫,實(shí)現(xiàn)背景切換時(shí)漸變效果
  • 遇到復(fù)雜的場景套啤,應(yīng)該如何編寫代碼
  • 配合Service宽气、本地廣播進(jìn)行音樂播放
  • 結(jié)束語

3 項(xiàng)目結(jié)構(gòu)介紹

項(xiàng)目結(jié)構(gòu)介紹包括以下內(nèi)容:

  • 主界面布局設(shè)計(jì)
  • 唱盤布局設(shè)計(jì)
  • 動(dòng)態(tài)布局
  • 唱盤控件DiscView對外接口及方法
  • 音樂狀態(tài)控制時(shí)序圖

3.1主界面布局設(shè)計(jì)

??主界面布局從上到下可以劃分幾大區(qū)域,如圖3-1所示:

圖 3-1 主界面布局
  • 標(biāo)題欄
    使用ToolBar實(shí)現(xiàn)潜沦,字體可能需要自定義萄涯。

  • 唱盤區(qū)域
    唱盤區(qū)域包括唱盤、唱針唆鸡、底盤涝影、以及實(shí)現(xiàn)切換的ViewPager等控件,該布局比較復(fù)雜争占,本案例使用自定義控件實(shí)現(xiàn)唱盤區(qū)域燃逻。

  • 時(shí)長顯示區(qū)域
    使用RelativeLayout作為根布局,進(jìn)度條使用SeekBar實(shí)現(xiàn)臂痕。

  • 播放控制區(qū)域
    使用LinearLayout作為根布局伯襟。

    另外,主界面使用RelativeLayout作為根布局刻蟹。

3.2 唱盤布局設(shè)計(jì)

??唱盤區(qū)域由控件DiscView實(shí)現(xiàn)逗旁,以RelativeLayout為根布局嘿辟,子控件包括:底盤舆瘪、唱針、ViewPager等红伦。其中英古,底盤和唱針均用ImageView實(shí)現(xiàn),然后使用ViewPager加載ImageView實(shí)現(xiàn)唱片的切換昙读。如圖3-2所示召调。

圖 3-2 唱盤區(qū)域布局

??唱盤布局代碼如下所示:

<?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實(shí)現(xiàn)唱片切換-->
    <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 動(dòng)態(tài)布局

??到這里,讀者可能有些好奇,上述布局中并沒有指定控件的寬高唠叛、邊距等參數(shù)只嚣,那如何保證控件顯示在正確的位置?我們沒有網(wǎng)易云音樂的設(shè)計(jì)圖艺沼,因此不能得知官方的布局參數(shù)册舞,那該怎么辦呢?其實(shí)有個(gè)笨方法障般,我們可以打開網(wǎng)易云音樂的播放界面并截圖调鲸,然后手動(dòng)去量需要的高度、邊距等參數(shù)挽荡。

??截圖量到控件的寬高藐石、邊距等數(shù)值,除以截圖的寬或高定拟,得到控件參數(shù)比例于微。使用時(shí),我們根據(jù)手機(jī)的屏幕寬高办素,乘以對應(yīng)的比例角雷,就能得到該屏幕尺寸下的控件寬高、邊距性穿。

??當(dāng)然勺三,這種動(dòng)態(tài)布局肯定會消耗更多性能,但不失為沒有辦法中的辦法需曾。

??相關(guān)控件參數(shù)比例吗坚,筆者統(tǒng)一放在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);

    /*設(shè)備屏幕寬度*/
    public static int getScreenWidth(Contet contet) {
        return contet.getResources().getDisplayMetrics().widthPiels;
    }

    /*設(shè)備屏幕高度*/
    public static int getScreenHeight(Contet contet) {
        return contet.getResources().getDisplayMetrics().heightPiels;
    }
}

??例如需要設(shè)置唱盤底盤的頂部外邊距商源,我們先獲得該比例,然后乘上當(dāng)前屏幕高度谋减,得到具體數(shù)值牡彻,最后通過LayoutParams類進(jìn)行動(dòng)態(tài)設(shè)置。

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提供一個(gè)接口IPlayInfo出爹,代碼如下:

public interface IPlayInfo {
    /*用于更新標(biāo)題欄變化*/
    public void onMusicInfoChanged(String musicName, String musicAuthor);
    /*用于更新背景圖片*/
    public void onMusicPicChanged(int musicPicRes);
    /*用于更新音樂播放狀態(tài)*/
    public void onMusicChanged(MusicChangedStatus musicChangedStatus);
}

??接口IPlayInfo中包含三個(gè)方法庄吼,分別用于更新標(biāo)題欄(音樂名、作者名)严就、更新背景圖片以及控制音樂播放狀態(tài)(播放总寻、暫停、上/下一首等)梢为。

??讀者可能有些疑問渐行?
  1. IPlayInfo接口的第一轰坊、二個(gè)方法屬于同一類型,為何要拆成兩個(gè)祟印?
  2. 為何通過回調(diào)來控制音樂播放肴沫?點(diǎn)擊主界面的控制按鈕時(shí),直接控制音樂播放不也可以嗎?

??這兩個(gè)問題,筆者也是經(jīng)過多次考慮刃鳄。

??第一個(gè)問題,首先網(wǎng)易云音樂交互上驻襟,更新標(biāo)題欄和更新背景圖的時(shí)機(jī)不一樣(ViewPager偏移頁面1/2時(shí)更新標(biāo)題欄,而背景圖是ViewPager是停止滑動(dòng)后才更新)芋哭。若兩個(gè)接口合并為一個(gè)沉衣,一來不利于解耦,二來可能造成開發(fā)者誤解减牺,并且造成資源浪費(fèi)豌习。

??第二個(gè)問題,筆者考慮到拔疚,點(diǎn)擊主界面的控制按鈕肥隆,并不代表立刻需要發(fā)生音樂的狀態(tài)變更(比如點(diǎn)擊播放按鈕,需要等唱針動(dòng)畫結(jié)束后才能開始播放音樂)稚失。因此栋艳,控制音樂的時(shí)機(jī)是依賴與DiscView的狀態(tài)。因此句各,我們通過接口中的onMusicChanged方法在適合的時(shí)間先將音樂控制回調(diào)到Activity吸占,再通過Activity發(fā)送指令,來達(dá)到切換音樂狀態(tài)的效果凿宾。

??點(diǎn)擊主界面播放/暫停矾屯、上/下一首按鈕時(shí),調(diào)用DiscView提供的方法:

@Override
public void onClick(View v) {
    if (v == mIvPlayOrPause) {
        mDisc.playOrPause();
    } else if (v == mIvNet) {
        mDisc.net();
    } else if (v == mIvLast) {
        mDisc.last();
    }
}

??當(dāng)主界面收到DiscView回調(diào)時(shí)初厚,調(diào)用相關(guān)方法控制音樂播放:

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 音樂狀態(tài)控制時(shí)序圖

圖 3-3 音樂狀態(tài)控制時(shí)序圖

??音樂控制狀態(tài)時(shí)序如圖3-3所示件蚕,點(diǎn)擊Activity的按鈕時(shí),先調(diào)用DiscView的相關(guān)方法产禾,并在合適的時(shí)機(jī)(如動(dòng)畫結(jié)束)再將狀態(tài)回調(diào)到Activity排作,并通過廣播發(fā)送指令到Service,實(shí)現(xiàn)音樂狀態(tài)切換下愈,最后通過廣播更新UI狀態(tài)纽绍。

??項(xiàng)目架構(gòu)介紹到這里蕾久,接下來是部分視覺效果以及設(shè)計(jì)思路的介紹势似。

4 解決加載大圖OOM問題

??加載大圖避免OOM(內(nèi)存溢出)拌夏,這是一個(gè)老生常談的話題,筆者以后會有 專門的文章來講述這方面的內(nèi)容履因,這里先放出結(jié)論障簿。

??解決大圖加載一般有幾種方案:
  1. 設(shè)置largeHeap為true。
  2. 根據(jù)圖片類型選定解碼格式栅迄。
  3. 根據(jù)原始圖片寬高及目標(biāo)顯示寬高站故,設(shè)置圖片采樣率。

??第一種方法毅舆,可以增加了堆內(nèi)存空間西篓,但這種方法僅僅延后了OOM發(fā)生的時(shí)機(jī),治標(biāo)不治本憋活,不推薦使用該方法岂津。

??第二種方法,Android對圖片進(jìn)行解碼時(shí)悦即,默認(rèn)是采用ARGB_8888格式吮成,即每個(gè)像素占32位,如果圖片格式是jpg辜梳,那么用ARGB_8888來解析自然是浪費(fèi)粱甫,因?yàn)閖pg圖片沒有透明通道。一般我們采用RGB_565格式來對jpg圖片解碼作瞄,RGB_565即每個(gè)像素點(diǎn)占16位茶宵,因此解碼后圖片的內(nèi)存占用僅僅是使用ARGB_8888解碼的一半

??第三種方法宗挥,這也是網(wǎng)上最普遍方式节预,也是最有通用的,采樣率可以理解成:當(dāng)采樣率為4属韧,表示將4個(gè)點(diǎn)“合并”為一個(gè)點(diǎn)來讀出安拟,縮小圖片尺寸的同時(shí)也減少了圖片占用空間,這樣解碼得到出來的圖片占用空間自然比原圖少宵喂。

??以加載音樂專輯圖片的代碼為例:

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;
    //設(shè)置圖片采樣率
    options.inSampleSize = dstSample;
    //設(shè)置圖片解碼格式
    options.inPreferredConfig = Bitmap.Config.RGB_565;

    return Bitmap.createScaledBitmap(BitmapFactory.decodeResource(getResources(),
            musicPicRes, options), musicPicSize, musicPicSize, true);
}

??上面代碼中糠赦,我們先設(shè)置options.inJustDecodeBounds = true,這樣BitmapFactory.decodeResource的時(shí)候僅僅會加載圖片的一些信息锅棕,然后通過options.outWidth獲取到圖片的寬度拙泽,根據(jù)目標(biāo)圖片尺寸算出采樣率。最后通過inPreferredConfig設(shè)置解碼格式裸燎,才正式加載圖片顾瞻。

5 生成圓圖最簡單的方法

??我們看到,網(wǎng)易云音樂唱盤背后有個(gè)底座德绿,是個(gè)透明的圓形圖荷荤,如圖5-1所示退渗。筆者找過所有網(wǎng)易云音樂的圖片資源,只發(fā)現(xiàn)了一張透明的方形圖蕴纳,看來我們需要自己生成圓形圖片了会油。

圖 5-1 唱盤底座

??生成圓圖有各種各樣的方式,比如自定義控件復(fù)寫onDraw方法古毛、給圖片加上圓形蒙版等翻翩,網(wǎng)上都有很多資料,在此不再多說稻薇。

??在此給大家分享一種筆者認(rèn)為最簡單的方式:

RoundedBitmapDrawable是android.support.v4.graphics.drawable 里面的一個(gè)類嫂冻,通過這個(gè)類可以很容易實(shí)現(xiàn)圓角和圓形圖片。

更多介紹請見:
  http://www.cnblogs.com/liunanjava/p/5827919.html

??使用RoundedBitmapDrawable生成圓形圖塞椎,先要將初始圖片調(diào)整為正方形絮吵,由于網(wǎng)易云音樂的這張圖片本身就是方形,因此筆者將這一步省略忱屑。

??代碼非常簡單蹬敲,代碼如下:

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

??我們將圖片資源文件轉(zhuǎn)為Bitmap對象,然后初始化RoundedBitmapDrawable對象莺戒,然后直接返回該對象就可以了伴嗡。

6 使用LayerDrawable進(jìn)行圖片合成

??這一步,主要用于合成唱盤與專輯圖片从铲,如圖6-1所示瘪校。筆者用UI Automation工具查看網(wǎng)易云音樂唱盤布局時(shí),發(fā)現(xiàn)里面用了兩個(gè)ImageView名段,估計(jì)是一個(gè)用來顯示唱盤阱扬,一個(gè)用來顯示專輯圖片(并不確定)。但如果可以將唱盤與專輯圖片合并成一張圖伸辟,那使用一個(gè)ImageView就夠了麻惶。

圖 6-1

LayerDrawable介紹:

LayerDrawable也可包含一個(gè)Drawable數(shù)組,因此系統(tǒng)將會按這些Drawable對象的數(shù)組順序來繪制它們信夫,索引最大的Drawable對象將會被繪制在最上面窃蹋。 LayerDrawable有點(diǎn)類似PhotoShop圖層的概念。

??思路:
  1. 生成圓形的專輯圖静稻。
  2. 使用LayerDrawable加載唱盤及專輯圖片警没。
  3. 調(diào)整專輯圖的邊距,讓它顯示在唱盤的正中間振湾。
  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);
    //調(diào)整專輯圖片的四周邊距
    layerDrawable.setLayerInset(0, musicPicMargin, musicPicMargin, musicPicMargin,
            musicPicMargin);

    return layerDrawable;
}

??在上面代碼中,我們先生成了唱盤對象BitmapDrawable押搪,然后通過RoundedBitmapDrawable生成圓形專輯圖片树酪,然后存放到Drawable[]數(shù)組中浅碾,并用來初始化LayerDrawable對象。最后嗅回,我們用setLayerInset方法調(diào)整專輯圖片的四周邊距,讓它顯示在唱盤正中摧茴。

7 實(shí)現(xiàn)背景毛玻璃效果

??顯而易見地绵载,網(wǎng)易云音樂的背景圖是由專輯圖片加上毛玻璃效果而生成的,如圖7-1所示苛白。

圖 7-1 毛玻璃效果

??毛玻璃效果娃豹,我們可以StackBlur模糊算法來實(shí)現(xiàn),這種算法應(yīng)用非常廣泛购裙,能得到非常良好的毛玻璃效果懂版。在這里我們使用它的java實(shí)現(xiàn)

??用法如下:

public static Bitmap doBlur(Bitmap sentBitmap, int radius, boolean canReuseInBitmap)

??第一個(gè)參數(shù)是需要模糊處理的Bitmap躏率,第二個(gè)參數(shù)是模糊半徑(一般設(shè)置為8)躯畴,第三個(gè)參數(shù)表示是否復(fù)用。

??對圖片進(jìn)行模糊化之前薇芝,我們先針對播放界面思考幾個(gè)問題:

??1. 網(wǎng)易云音樂專輯圖均為方形蓬抄,若將專輯圖全屏加載會造成圖片變形。
  2. 直接對大圖模糊化很容易出現(xiàn)OOM夯到,同時(shí)性能也有所損耗嚷缭。
  3. 可能有部分專輯圖片顏色過亮(如純白色),會影響按鈕的視覺效果耍贾。

??第一點(diǎn)阅爽,比較容易解決,我們可以在原圖中部荐开,切割一個(gè)與屏幕寬高比例對應(yīng)的圖片即可付翁。
??第二點(diǎn),**做圖片模糊化處理前晃听,我們一般先對大圖進(jìn)行縮小處理胆敞,再用算法進(jìn)行模糊,這樣不容易出現(xiàn)OOM杂伟,對性能也沒影響移层。 **
??第三點(diǎn),我們可以在圖片模糊化后的基礎(chǔ)上赫粥,加上灰色遮罩層观话,這樣就算是純白背景,也不會對主界面的控件造成視覺影響越平。

代碼如下所示:

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線程,因此筆者將其放著單獨(dú)線程中執(zhí)行晦溪。

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與屬性動(dòng)畫瀑粥,實(shí)現(xiàn)背景切換時(shí)漸變效果

??仔細(xì)觀察網(wǎng)易云音樂,發(fā)現(xiàn)切換歌曲時(shí)三圆,背景圖也會隨著變化狞换,如圖8-1所示,變化時(shí)還帶有一個(gè)漸變的效果舟肉。筆者曾經(jīng)也是為這個(gè)效果想了很長時(shí)間修噪,這效果究竟是怎么實(shí)現(xiàn)的?后來筆者想到了一個(gè)很簡單的方法路媚,可以用前面介紹的LayerDrawable加屬性動(dòng)畫來實(shí)現(xiàn)黄琼。

圖 8-1 背景圖漸變效果

??思路如下:
  1. 給LayerDrawable設(shè)置兩個(gè)圖層,第一圖層是前一個(gè)背景整慎,第二圖層是準(zhǔn)備顯示的背景脏款。
  2. 先把準(zhǔn)備顯示的背景透明度設(shè)為0,因此完全透明裤园,此時(shí)只顯示前一個(gè)背景圖弛矛。
  3. 通過屬性動(dòng)畫,動(dòng)態(tài)將第二圖層的透明度從0調(diào)整至100比然,并不斷更新控件的背景丈氓。

??有了思路,寫代碼就簡單了强法。我們通過RelativeLayout來顯示背景万俗,考慮到需要對代碼進(jìn)行封裝,我們自定義一個(gè)類BackgourndAnimationRelativeLayout繼承RelativeLayout饮怯,并在該類中實(shí)現(xiàn)上述的思路闰歪,關(guān)鍵代碼如下:

/**
 * 自定義一個(gè)控件,繼承RelativeLayout
 **/
public class BackgourndAnimationRelativeLayout etends RelativeLayout

//初始化LayerDrawable對象
private void initLayerDrawable() {
    Drawable backgroundDrawable = getContet().getDrawable(R.drawable.ic_blackground);
    Drawable[] drawables = new Drawable[2];

    /*初始化時(shí)先將前景與背景顏色設(shè)為一致*/
    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);
            /*動(dòng)態(tài)設(shè)置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) {
            /*動(dòng)畫結(jié)束后库倘,記得將原來的背景圖及時(shí)更新*/
            layerDrawable.setDrawable(INDE_BACKGROUND, layerDrawable.getDrawable(
                    INDE_FOREGROUND));
        }

        @Override
        public void onAnimationCancel(Animator animation) {

        }

        @Override
        public void onAnimationRepeat(Animator animation) {

        }
    });
}

//對外提供方法,用于播放漸變動(dòng)畫
public void beginAnimation() {
    objectAnimator.start();
}

9 遇到復(fù)雜的場景论矾,應(yīng)該如何編寫代碼

??這是個(gè)很有趣的問題教翩,我們平時(shí)寫代碼也會遇到很多復(fù)雜的場景。就拿網(wǎng)易云音樂來說贪壳,我們可以仔細(xì)觀察唱針動(dòng)畫的細(xì)節(jié)饱亿,你會發(fā)現(xiàn)網(wǎng)易云音樂是多么優(yōu)秀的應(yīng)用。

??唱針動(dòng)畫細(xì)節(jié):

  • 初始狀態(tài)為暫停/停止時(shí),點(diǎn)擊播放按鈕彪笼,此時(shí)唱針移動(dòng)到底部钻注。 圖9-1所示。
圖 9-1
  • 初始狀態(tài)為播放時(shí)配猫,點(diǎn)擊暫停按鈕幅恋,此時(shí)唱針移到頂部。 如圖9-2所示泵肄。
圖 9-2
  • 初始狀態(tài)為播放時(shí)捆交,手指按住唱盤并稍微偏移,等唱針未移到頂部時(shí)凡伊,立刻松開手指零渐,此時(shí)唱針回到頂部后立刻再回到唱盤位置窒舟。如圖9-3所示系忙。
圖 9-3
  • 初始狀態(tài)為暫停/停止時(shí),點(diǎn)擊播放惠豺,此時(shí)唱針往下移動(dòng)银还,當(dāng)唱針還未移到底部,手指馬上按住唱盤并偏移洁墙,此時(shí)唱針立刻往頂部移動(dòng)蛹疯。如圖9-4所示 。
圖 9-4
  • 初始狀態(tài)為播放/暫停/停止時(shí)热监,左右滑動(dòng)唱片進(jìn)行音樂切換捺弦,唱針動(dòng)畫未結(jié)束時(shí),立刻點(diǎn)擊上/下一首按鈕孝扛,進(jìn)行音樂切換列吼,此時(shí)唱針狀態(tài)不能出現(xiàn)混亂。如圖9-5所示苦始。
圖 9-5

??第1寞钥、2個(gè)效果很容易實(shí)現(xiàn),只要監(jiān)聽ViewPager的狀態(tài)配合屬性動(dòng)畫就可以了陌选,但第3理郑、4、5個(gè)效果(本質(zhì)上5和3咨油、4一致)實(shí)現(xiàn)起來就有難度了您炉。當(dāng)然我們可以簡單地加boolean標(biāo)記暴力解決,標(biāo)記雖然可以用役电,但不能隨意用邻吭,否則變量多起來代碼可讀性句變得非常差了

??筆者在這談一點(diǎn)自己的心得體會,遇到這種問題囱晴,原則如下:

冷靜分析膏蚓,簡化并找到不同場景的被觸發(fā)的狀態(tài)。

??我們仔細(xì)分析上述的幾種場景畸写,無非和兩個(gè)因素有關(guān):唱片是否偏離以及動(dòng)作觸發(fā)時(shí)驮瞧,唱針?biāo)幍奈恢?/strong>。因此枯芬,我們可以把狀態(tài)分為兩類论笔,六種狀態(tài):

??1. 唱片狀態(tài)(兩種):包括偏移中、偏移結(jié)束千所。 如圖9-6所示狂魔。
  2. 唱針的狀態(tài)(四種):處于遠(yuǎn)端(遠(yuǎn)離唱片)、處于近端(貼近唱片)淫痰、正在從遠(yuǎn)端往近端移動(dòng)最楷、正在從近端往遠(yuǎn)端移動(dòng)。如圖9-7所示待错。

圖 9-6 ViewPager的狀態(tài)
圖 9-7 唱針的狀態(tài)

??其中籽孙,唱片(即ViewPager)的狀態(tài)可以通過PageChangeListener得到。唱針的狀態(tài)火俄,筆者用枚舉來表示犯建,并且在動(dòng)畫的開始、結(jié)束時(shí)對唱針狀態(tài)及時(shí)更新瓜客。

??唱針狀態(tài)枚舉:

    private enum NeedleAnimatorStatus {
        /*移動(dòng)時(shí):從唱盤往遠(yuǎn)處移動(dòng)*/
        TO_FAR_END,
        /*移動(dòng)時(shí):從遠(yuǎn)處往唱盤移動(dòng)*/
        TO_NEAR_END,
        /*靜止時(shí):離開唱盤*/
        IN_FAR_END,
        /*靜止時(shí):貼近唱盤*/
        IN_NEAR_END
    }

動(dòng)畫開始時(shí)适瓦,更新唱針狀態(tài):

@Override
public void onAnimationStart(Animator animator) {
    /**
     *根據(jù)動(dòng)畫開始前NeedleAnimatorStatus的狀態(tài),
     *即可得出動(dòng)畫進(jìn)行時(shí)NeedleAnimatorStatus的狀態(tài)
     **/
    if (needleAnimatorStatus == NeedleAnimatorStatus.IN_FAR_END) {
        needleAnimatorStatus = NeedleAnimatorStatus.TO_NEAR_END;
    } else if (needleAnimatorStatus == NeedleAnimatorStatus.IN_NEAR_END) {
        needleAnimatorStatus = NeedleAnimatorStatus.TO_FAR_END;
    }
}

動(dòng)畫結(jié)束時(shí)谱仪,更新唱針狀態(tài):

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

??每種狀態(tài)都定義清楚玻熙,這樣代碼寫起來就相當(dāng)清晰了。

??比如需要播放動(dòng)畫時(shí)芽卿,就包含兩個(gè)狀態(tài)
  1. 唱針動(dòng)畫暫停中揭芍,唱針處于遠(yuǎn)端。
  2. 唱針動(dòng)畫播放中卸例,唱針處于從近端往遠(yuǎn)端移動(dòng)(上述場景3的問題)

/*播放動(dòng)畫*/
private void playAnimator() {
    /*唱針處于遠(yuǎn)端時(shí)称杨,直接播放動(dòng)畫*/
    if (needleAnimatorStatus == NeedleAnimatorStatus.IN_FAR_END) {
        mNeedleAnimator.start();
    } 
    /*唱針處于往遠(yuǎn)端移動(dòng)時(shí),設(shè)置標(biāo)記筷转,等動(dòng)畫結(jié)束后再播放動(dòng)畫*/
    else if (needleAnimatorStatus == NeedleAnimatorStatus.TO_FAR_END) {
        mIsNeed2StartPlayAnimator = true;
    }
}

??再比如需要暫停動(dòng)畫時(shí)姑原,也包含兩種狀態(tài)

1. 唱針動(dòng)畫暫停中,唱針處于近端時(shí)呜舒。
  2. 唱針動(dòng)畫播放中锭汛,唱針往近端移動(dòng)時(shí)(解決上述場景第4個(gè)細(xì)節(jié)問題)

/*暫停動(dòng)畫*/
private void pauseAnimator() {
    /*播放時(shí)暫停動(dòng)畫*/
    if (needleAnimatorStatus == NeedleAnimatorStatus.IN_NEAR_END) {
        int index = mVpContain.getCurrentItem();
        pauseDiscAnimatior(index);
    }
    /*唱針往唱盤移動(dòng)時(shí)暫停動(dòng)畫*/
    else if (needleAnimatorStatus == NeedleAnimatorStatus.TO_NEAR_END) {
        mNeedleAnimator.reverse();
        /**
         * 若動(dòng)畫在沒結(jié)束時(shí)執(zhí)行reverse方法,則不會執(zhí)行監(jiān)聽器的onStart方法,此時(shí)需要手動(dòng)設(shè)置
         * */
        needleAnimatorStatus = NeedleAnimatorStatus.TO_FAR_END;
    }
    /**
     * 動(dòng)畫可能執(zhí)行多次唤殴,只有音樂處于停止 / 暫停狀態(tài)時(shí)般婆,才執(zhí)行暫停命令
     * */
    if (musicStatus == MusicStatus.STOP) {
        notifyMusicStatusChanged(MusicChangedStatus.STOP);
    } else if (musicStatus == MusicStatus.PAUSE) {
        notifyMusicStatusChanged(MusicChangedStatus.PAUSE);
    }
}

10 配合Service、本地廣播進(jìn)行音樂播放

10.1 為什么選擇Service朵逝?

??面試的時(shí)候蔚袍,可能面試官會問:什么場景下,使用Service會比Activity更有優(yōu)勢配名?很多人回答啤咽,運(yùn)行在不需要顯示界面的場景時(shí),但沒有沒有說出具體場景渠脉。

??播放音頻宇整,需要用到MediaPlayer類。筆者認(rèn)為芋膘,對MediaPlayer的控制鳞青,放在Service處理再好不過了:Service不需要界面,但更重要的是索赏,不需要處理屏幕旋轉(zhuǎn)的邏輯盼玄。如果把MediaPlayer放在Activity贴彼,屏幕旋轉(zhuǎn)時(shí)潜腻,因?yàn)锳ctivity會重建,估計(jì)還要保存ediaPlayer的各種狀態(tài)器仗,使用Service就沒有這個(gè)顧慮了融涣。

??筆者的案例不涉及到屏幕旋轉(zhuǎn)處理,為了演示流程精钮,還是使用Service處理音樂播放威鹿。

10.2 為什么選擇本地廣播?

??Android的廣播時(shí)全局的轨香,一旦發(fā)出忽你,會在系統(tǒng)內(nèi)傳播,如果有其他APP得知你的Action信息和權(quán)限臂容,可能造成信息泄露科雳,甚至可以發(fā)送廣播來控制你的APP。而本地廣播(LocalBroadcast)只在應(yīng)用內(nèi)部傳遞脓杉,則不會有這個(gè)顧慮糟秘。

??本地廣播有以下幾個(gè)特點(diǎn):

  • 廣播在應(yīng)用內(nèi)部傳播,不必?fù)?dān)心信息泄露球散。
  • 別的應(yīng)用無法發(fā)送廣播來控制你的APP尿赚,更加安全。
  • 發(fā)送的廣播不需要系統(tǒng)中轉(zhuǎn),效率更高凌净。

??本地廣播的用法很簡單悲龟,如下:

??注冊:

LocalBroadcastManager.getInstance(this).registerReceiver(receiver, intentFilter);

??取消注冊:

LocalBroadcastManager.getInstance(this).unregisterReceiver(receiver);

??廣播發(fā)送:

LocalBroadcastManager.getInstance(contet).sendBroadcast(new Intent(ACTION));

10.3 進(jìn)度條的處理

??本案例中,我們使用Handler+SeekBar實(shí)現(xiàn)歌曲進(jìn)度動(dòng)態(tài)更新冰寻,通過Handler的sendEmptyMessageDelayed方法躲舌,每隔1秒發(fā)送一個(gè)事件。當(dāng)接收到事件時(shí)性雄,更新SeekBar的進(jìn)度然后再次調(diào)用sendEmptyMessageDelayed方法没卸,這樣就可以實(shí)現(xiàn)進(jìn)度的動(dòng)態(tài)更新。

??關(guān)鍵代碼如下所示:

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() {
    /*避免重復(fù)發(fā)送Message*/
    stopUpdateSeekBarProgree();
    mMusicHandler.sendEmptyMessageDelayed(0,1000);
}

??當(dāng)SeekBar滑動(dòng)時(shí)秒旋,使用removeMessages方法移除Handler中的延時(shí)消息约计,暫停Handler對SeekBar的更新。當(dāng)SeekBar滑動(dòng)結(jié)束后迁筛,根據(jù)當(dāng)前的進(jìn)度值來更新音樂播放的位置煤蚌。

??關(guān)鍵代碼如下所示:

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 結(jié)束語

??本案例還可以進(jìn)行更多的優(yōu)化,比如ViewPager無限切換细卧、顯示歌詞等尉桩。但邊幅有限,本章的內(nèi)容就先到此結(jié)束贪庙,希望能起到拋磚引玉的作用蜘犁,更多的實(shí)現(xiàn)細(xì)節(jié)可以參考項(xiàng)目源碼。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末止邮,一起剝皮案震驚了整個(gè)濱河市这橙,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌导披,老刑警劉巖屈扎,帶你破解...
    沈念sama閱讀 216,470評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異撩匕,居然都是意外死亡鹰晨,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,393評論 3 392
  • 文/潘曉璐 我一進(jìn)店門止毕,熙熙樓的掌柜王于貴愁眉苦臉地迎上來模蜡,“玉大人,你說我怎么就攤上這事滓技×梗” “怎么了?”我有些...
    開封第一講書人閱讀 162,577評論 0 353
  • 文/不壞的土叔 我叫張陵令漂,是天一觀的道長膝昆。 經(jīng)常有香客問我丸边,道長,這世上最難降的妖魔是什么荚孵? 我笑而不...
    開封第一講書人閱讀 58,176評論 1 292
  • 正文 為了忘掉前任妹窖,我火速辦了婚禮,結(jié)果婚禮上收叶,老公的妹妹穿的比我還像新娘骄呼。我一直安慰自己,他們只是感情好判没,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,189評論 6 388
  • 文/花漫 我一把揭開白布蜓萄。 她就那樣靜靜地躺著,像睡著了一般澄峰。 火紅的嫁衣襯著肌膚如雪嫉沽。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,155評論 1 299
  • 那天俏竞,我揣著相機(jī)與錄音绸硕,去河邊找鬼。 笑死魂毁,一個(gè)胖子當(dāng)著我的面吹牛玻佩,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播席楚,決...
    沈念sama閱讀 40,041評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼咬崔,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了酣胀?” 一聲冷哼從身側(cè)響起刁赦,我...
    開封第一講書人閱讀 38,903評論 0 274
  • 序言:老撾萬榮一對情侶失蹤娶聘,失蹤者是張志新(化名)和其女友劉穎闻镶,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體丸升,經(jīng)...
    沈念sama閱讀 45,319評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡铆农,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,539評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了狡耻。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片墩剖。...
    茶點(diǎn)故事閱讀 39,703評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖夷狰,靈堂內(nèi)的尸體忽然破棺而出岭皂,到底是詐尸還是另有隱情,我是刑警寧澤沼头,帶...
    沈念sama閱讀 35,417評論 5 343
  • 正文 年R本政府宣布爷绘,位于F島的核電站书劝,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏土至。R本人自食惡果不足惜购对,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,013評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望陶因。 院中可真熱鬧骡苞,春花似錦、人聲如沸楷扬。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,664評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽烘苹。三九已至亚铁,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間螟加,已是汗流浹背徘溢。 一陣腳步聲響...
    開封第一講書人閱讀 32,818評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留捆探,地道東北人然爆。 一個(gè)月前我還...
    沈念sama閱讀 47,711評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像黍图,于是被迫代替她去往敵國和親曾雕。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,601評論 2 353

推薦閱讀更多精彩內(nèi)容