Android仿网易云音乐播放界面

概述

网易云音乐是一款非常优秀的音乐播放器,尤其是播放界面,使用唱盘机风格,显得格外古典优雅。这里抛砖引玉,原文地址:http://www.jianshu.com/p/cb54990219d9 
首先来看一下网易的播放效果。 
 
要实现上面的功能,我们需要对界面进行一个拆分,拆分后大概包含如下结构:

  • 主界面布局设计
  • 唱盘布局设计
  • 动态布局
  • 唱盘控件DiscView对外接口及方法
  • 音乐状态控制时序图

分析及实现

主界面布局设计

主界面布局从上到下可以划分几大区域,如图: 
 
如图,由上到下主要分为:标题栏区、唱盘区域、时长显示区域、播放控制区域。 
标题栏 
使用ToolBar实现,字体可能需要自定义。 
唱盘区域 
唱盘区域包括唱盘、唱针、底盘、以及实现切换的ViewPager等控件,该布局比较复杂,本案例使用自定义控件实现唱盘区域。 
时长显示区域 
使用RelativeLayout作为根布局,进度条使用SeekBar实现。 
播放控制区域 
比较简单,使用LinearLayout作为根布局。

唱盘布局实现(难点)

唱盘区域由控件DiscView实现,以RelativeLayout为根布局,子控件包括:底盘、唱针、ViewPager等。其中,底盘和唱针均用ImageView实现,然后使用ViewPager加载ImageView实现唱片的切换。如图: 
 
唱片布局如下:

<?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>

这里面涉及到一个DiscView类,这个是一个复合类,我们来看一些主要的功能。 

唱盘控件DiscView提供一个接口IPlayInfo,代码如下:

public interface IPlayInfo {
    /*用于更新标题栏变化*/
    void onMusicInfoChanged(String musicName, String musicAuthor);
    /*用于更新背景图片*/
    void onMusicPicChanged(int musicPicRes);
    /*用于更新音乐播放状态*/
    void onMusicChanged(MusicChangedStatus musicChangedStatus);
}

这上面定义的三个函数作用: 分别用于更新标题栏(音乐名、作者名)、更新背景图片以及控制音乐播放状态(播放、暂停、上/下一首等)。 
点击主界面播放/暂停、上/下一首按钮时,调用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-3所示,点击Activity的按钮时,先调用DiscView的相关方法,并在合适的时机(如动画结束)再将状态回调到Activity,并通过广播发送指令到Service,实现音乐状态切换,最后通过广播更新UI状态。 
这个状态的切换只有你仔细观察就会明白它的流程了。项目架构介绍到这里,接下来是部分视觉效果以及设计思路的介绍和项目的一些难点介绍。

解决加载大图OOM问题

解决大图加载一般有几种方案: 
1. 设置largeHeap为true。 
2. 根据图片类型选定解码格式。 
3. 根据原始图片宽高及目标显示宽高,设置图片采样率。

根据实际经验我们一般采用后两种,第一种虽然通过增加堆内存来延缓了oom的时机,但是治标不治本。这里我们整理一个类。

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

我相信有过几年Java开发经验或Android经验的人都会知道这么一个常识:首先设置options.inJustDecodeBounds = true,这样BitmapFactory.decodeResource的时候仅仅会加载图片的一些信息,然后通过options.outWidth获取到图片的宽度,根据目标图片尺寸算出采样率。最后通过inPreferredConfig设置解码格式,才正式加载图片,这样有效的避免了图片的oom。

生成圆图最简单方式

以前我们使用圆圈一般会自定义一个View,然后实现onDraw(),不过Android在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;
}

使用LayerDrawable进行图片合成

LayerDrawable介绍 
LayerDrawable也可包含一个Drawable数组,因此系统将会按这些Drawable对象的数组顺序来绘制它们,索引最大的Drawable对象将会被绘制在最上面。 LayerDrawable有点类似PhotoShop图层的概念。 
我们在分析唱片布局的时候发现原View包含两个ImageView,估计是一个用来显示唱盘,一个用来显示专辑图片。 
 
使用LayerDrawable生成复合图片代码:

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方法调整专辑图片的四周边距,让它显示在唱盘正中。

实现背景毛玻璃效果

这个网上的资料很多,也有基于JNI实现的,这个使用JNI实现可以看一下我之前的博客JNI实现毛玻璃效果,这里为了方便大家使用,我就直接使用工具类的方式,关于模糊化的实现逻辑大家可以搜索一下“BlurUtil”,考虑到这部分代码可能会阻塞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();
    }
}

使用LayerDrawable与属性动画,实现背景切换时渐变效果

仔细观察网易云音乐,发现切换歌曲时,背景图也会随着变化。其实这种也很好做,可以使用LayerDrawable加属性动画来实现。 
思路如下: 
1. 给LayerDrawable设置两个图层,第一图层是前一个背景,第二图层是准备显示的背景。 
2. 先把准备显示的背景透明度设为0,因此完全透明,此时只显示前一个背景图。 
3. 通过属性动画,动态将第二图层的透明度从0调整至100,并不断更新控件的背景。

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

唱针变化逻辑

我们来看一下唱针的变化,为了真实的模拟真实的场景,唱针主要有以下状态:

  • 初始状态为暂停/停止时,点击播放按钮,此时唱针移动到底部。
  • 初始状态为播放时,点击暂停按钮,此时唱针移到顶部。
  • 初始状态为播放时,手指按住唱盘并稍微偏移,等唱针未移到顶部时,立刻松开手指,此时唱针回到顶部后立刻再回到唱盘位置。
  • 初始状态为暂停/停止时,点击播放,此时唱针往下移动,当唱针还未移到底部,手指马上按住唱盘并偏移,此时唱针立刻往顶部移动。
  • 初始状态为播放/暂停/停止时,左右滑动唱片进行音乐切换,唱针动画未结束时,立刻点击上/下一首按钮,进行音乐切换,此时唱针状态不能出现混乱。

初始状态为暂停/停止时,点击播放按钮,此时唱针移动到底部。 
 
初始状态为播放时,点击暂停按钮,此时唱针移到顶部。 
 
初始状态为播放时,手指按住唱盘并稍微偏移,等唱针未移到顶部时,立刻松开手指,此时唱针回到顶部后立刻再回到唱盘位置。 
 
初始状态为暂停/停止时,点击播放,此时唱针往下移动,当唱针还未移到底部,手指马上按住唱盘并偏移,此时唱针立刻往顶部移动。 
这里写链接内容 
初始状态为播放/暂停/停止时,左右滑动唱片进行音乐切换,唱针动画未结束时,立刻点击上/下一首按钮,进行音乐切换,此时唱针状态不能出现混乱,反复做了步骤1的动作。 
 
我们队上面的图片仔细分析,然后结合ViewPager的原理我们来看看。 
 
唱片(即ViewPager)的状态可以通过PageChangeListener得到。唱针的状态,笔者用枚举来表示,并且在动画的开始、结束时对唱针状态及时更新。那么我们很容易就想到case或者枚举。

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

每种状态都定义清楚,每个动画负责的功能都拆分这样写起来就比较清楚了。 
比如需要播放动画时,就包含两个状态:  
- 唱针动画暂停中,唱针处于远端。 
- 唱针动画播放中,唱针处于从近端往远端移动

那么我们调用代码的时候就这么用:

/*播放动画*/
private void playAnimator() {
    /*唱针处于远端时,直接播放动画*/
    if (needleAnimatorStatus == NeedleAnimatorStatus.IN_FAR_END) {
        mNeedleAnimator.start();
    }
    /*唱针处于往远端移动时,设置标记,等动画结束后再播放动画*/
    else if (needleAnimatorStatus == NeedleAnimatorStatus.TO_FAR_END) {
        mIsNeed2StartPlayAnimator = true;
    }
}

至于其他的比较跨组件的界面更新,一般会使用广播,大家也可以使用事件总线(EventBus). 
附上源码,这里可能需要大家自己编译。 
附:仿网易云音乐界面源码

时间: 2024-08-03 19:45:53

Android仿网易云音乐播放界面的相关文章

仿网易云音乐的播放进度条

仿网易云音乐的播放进度条,有三种状态:播放.暂停和拖动,只是实现了动画和主要的交互逻辑,其他细节(如暂停音乐的播放等)还需要自己完善: DKPlayerBar 是继承于UIControl的,如果想获取播放\暂停的事件建议用标准的addTarget方法: [playerBar addTarget:self action:@selector(playOrPause) forControlEvents:UIControlEventValueChanged]; 然后在DKPlayerBar里监听DKPl

网易云音乐如何设置为默认播放器

  网易云音乐怎么设置为默认播放器?网易云音乐,是一个简单实用而又功能强大的音乐播放器,深受广大群众的喜爱.音乐无界限,听见好时光.平时电脑使用网易云音乐播放器来播放音乐,那么就要将它设置为默认音乐播放器了.下面,安下小编给大家带来网易云音乐设置为默认播放器的方法. 网易云音乐设置为默认播放器的两种方法 方法一: 1.鼠标左键双击桌面上的网易云播放器,打开它. 2.打开之后,来到播放器的首页,我们点击右上角的设置按钮. 3.然后在这我们就可以将它设置为默认播放器了. 方法二: 1.点击桌面左下角

网易云音乐iOS1.3问世

近日,网易在北京举行了隆重的发布会,重磅发布旗下战略级新产品----网易云音乐,该产品的发布,可谓是空前的,从丁磊现身演讲,到五大唱片老板及众多艺人站台,足以看出网易对这款产品不容有失的举措,在中国数字音乐多方鏖战的红海中,也让我们对网易云音乐充满了期待. 如今,相亲已经成为了一种潮流,当对面坐这你的相亲对象的时候,你会用什么去标准去衡量他?一般可以归纳为:外貌.能力.性格三个要素.而这三个要素恰好和音乐产品的三个要素吻合,外貌相对于界面设计和操作,能力相对于曲库.音质等核心功能,性格相对于产品

网易云音乐下载的歌曲在哪里

  网易云音乐自从发布以来一直都备受欢迎,其社交式的音乐模式与非常高质量的音质让很多网友都非常满意. 不过网易云音乐播放器功能上有些用户还不是很清楚,经常有网友问网易云音乐下载的歌曲在哪里,这里就给大家介绍下. 网易云音乐下载的歌曲在哪: 安卓版网易云音乐歌曲下载目录: 网易云音乐下载的歌曲存放在:我的文件/sdcard/netease/cloudmusic/music,不过下载下来的歌曲没有名字,是一些乱码,各位只能一首一首的试听哦. pc版网易云音乐歌曲下载目录: 打开网易云音乐,点击左侧功

网易云音乐发布2014年度中国移动音乐用户行为报告

日前,网易云音乐对全国音乐软件用户进行了详尽调查,通过海量数据分析得出<2014中国人移动音乐用户行为报告>.该报告主要对中国移动音乐用户行为进行分析,研究各种因素对移动音乐用户行为产生的影响,移动音乐软件内容特点及行业发展.期望为市场各方面提供参考.报告显示,移动音乐用户与收入有关,不同收入人群对音乐软件选择有较大差异:2014年,移动音乐用户最多的行为除听歌外,分享单曲.歌词以及音乐评论等社交行为较前一年有数倍增长.提升显著.第一部分:用户年龄.性别.收入等基本属性85后占移动音乐听众主流

网易云音乐for Android 1.5版引入了个性化推荐机制

摘要: 从1月25日上线至今,网易云音乐已经迭代发展了八个月时间,在今天下午的媒体沟通会上,网易音乐高级总监王磊首次披露了云音乐的各项数据:截至9月初,注册用户已达600万:歌单总 从1月25日上线至今,网易云音乐已经迭代发展了八个月时间,在今天下午的媒体沟通会上,网易音乐高级总监王磊首次披露了云音乐的各项数据:截至9月初,注册用户已达600万:歌单总量超千万,其运营数据表显示,在歌单.单曲.专辑和歌手TOP50等多种音乐形式中,80%的用户选择收听歌单. 而新上线的网易云音乐 for Andr

网易云音乐设置定时播放的教程

我们在手机中设置音乐定时功能只要在个人设置中就可以设置了,具体的设置我们来看下文吧. 设置方法 1.在手机中打开网易云音乐app然后点击界面中的[个人设置]按钮 2.f进入到个人设置我们点击[定时停止播放]功能了,我们点击 3.我们在点击[定时停止播放]菜单后,就能看到设置定时播放时间了,不过该播放时间暂时不支持自定义定时,不过用户可根据自己的需求来选择现有的定时播放时间 好了这样设置好之后只要手机有电在指定的时间就会自动播放音乐了,希望文章能够对各位同学带来帮助的哦.

网易云音乐APP播放记录查看方法分享

给各位网易云音乐软件的使用者们来详细的解析分享一下播放记录的查看方法. 方法分享: 1.首先打开网易云音乐APP,点击"我的音乐"选项:     2.然后在"我的音乐"页面点击"最近播放"选项:     3.你就会在"最近播放"的页面看到你曾经听过的歌曲,接着你就可以找到那首好听的歌了.   好了,以上的信息就是小编给各位网易云音乐的这一款软件的使用者们带来的详细的播放记录的查看方法解析分享的全部内容了,各位看到这里的软件使用

网易云音乐IPAD V1.0设计总结

  网易云音乐作为网易旗下的重磅产品,无论做工还是体验都是同类软件的翘楚,从Android到iPhone,都是好评如潮.现在iPad端也上线了,产品团队分享了这款产品从调研到落地的全部过程,希望同学们阅读完之后可以了解一下专业的设计流程,对今后加入大公司参与工作,非常有帮助呦. 一切从心开始 当网易云音乐iphone平台上线的那一刻开始,项目组就不断地收到了很多用户希望iPad平台早日上线的呼声.这种期待给了我们一种无形的力量,也让我们更加坚信我们不能随随便便地把iPhone的所有设计照抄照搬,