前言:网易云音乐是一款非常优秀的音乐播放器,尤其是播放界面,使用唱盘机风格,显得格外古典优雅。本文是AchillesL出于学习与挑战的想法,思考播放界面背后的实现原理,并写了一个小程序。AchillesL的简书地址为:http://www.jianshu.com/u/d914bc42928c。点击最下方【阅读原文】,可看本文原文。话不多说,看下正文。

  笔者尽可能地去模仿官方的视觉、交互效果,其中包括了唱盘与唱针切换时的细节处理、背景渐变等。本文将会分享一些视觉效果实现的方法以及设计思想,但难免有错漏之处。若读者发现有错误的地方或者更好的实现方法,请留言回复,希望与大家共同进步。效果如下图所示:

1 源码地址

  需要源码的读者,可以到github中自行下载:
  https://github.com/AchillesLzg/jianshu-neteasedisc

2 本文内容

  • 项目结构介绍

  • 解决加载大图OOM问题

  • 生成圆图最简单的方法

  • 使用LayerDrawable进行图片合成

  • 实现背景毛玻璃效果

  • 结束语

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 唱盘区域布局

  唱盘布局代码如下所示:

<xml version="1.0" encoding="utf-8"?>
<com.achillesl.neteasedisc.widget.DiscViewmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="wrap_content"><!--底盘--><ImageViewandroid:id="@+id/ivDiscBlackgound"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_centerHorizontal="true"/><!--ViewPager实现唱片切换--><android.support.v4.view.ViewPagerandroid:id="@+id/vpDiscContain"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_centerHorizontal="true"/><!--唱针--><ImageViewandroid: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() {@Overridepublic void run() {final Drawable foregroundDrawable = getForegroundDrawable(musicPicRes);runOnUiThread(new Runnable() {@Overridepublic void run() {mRootLayout.setForeground(foregroundDrawable);mRootLayout.beginAnimation();}});}}).start();}
}

8 结束语

  本案例还可以进行更多的优化,比如ViewPager无限切换、显示歌词等。但边幅有限,本章的内容就先到此结束,希望能起到抛砖引玉的作用,更多的实现细节可以参考项目源码。

第一时间获得博客更新提醒,以及更多android、小程序干货,源码分析,最新开源项目推荐,欢迎关注我的微信公众号,扫一扫下方二维码或者长按识别二维码,即可关注。

一个酷炫的音乐播放界面相关推荐

  1. [开源项目]Android_炫酷的3D音乐播放器_各种特效OpenGL

    这是 我见过最炫的一个音乐播放器了.里面包含各种特效OpenGL,先上图片...看看大家的反应. 3Dmusic3.png(48.63 KB, 下载次数: 34) 炫酷的3D音乐播放器_各种特效Ope ...

  2. 运用Java制作一个属于自己的音乐播放软件

    运用Java制作一个属于自己的音乐播放软件 前言 上个寒假小编用python做了一个音乐播放软件(博客链接为:)运用tkinter.爬虫做了一个播放音乐的小程序(动态显示歌词[歌词向上翻滚]),觉得效 ...

  3. python拿什么做可视化界面好-用python打造可视化爬虫监控系统,酷炫的图形化界面...

    原标题:用python打造可视化爬虫监控系统,酷炫的图形化界面 本文并不是讲解爬虫的相关技术实现的,而是从实用性的角度,将抓取并存入 MongoDB 的数据 用 InfluxDB 进行处理,而后又通过 ...

  4. python中turtle画酷炫图案-用python打造可视化爬虫监控系统,酷炫的图形化界面

    本文并不是讲解爬虫的相关技术实现的,而是从实用性的角度,将抓取并存入 MongoDB 的数据 用 InfluxDB 进行处理,而后又通过 Grafana 将爬虫抓取数据情况通过酷炫的图形化界面展示出来 ...

  5. html制作一个酷炫的记事本(源码)

    文章目录 1.记事本风格和灵感 1.1 设计灵感 1.2 整体风格 2.代码展示 1.1 酷炫的记事本效果图 1.2 主代码 源码下载 html制作一个酷炫的记事本(源码) 使用html实现记事本的完 ...

  6. 酷炫的android手机界面作品

    一个酷炫的手机界面设计的作品 http://hi.bdimg.com/static/vphot ... amp;name=/album.swf

  7. 使用TextInputLayout分分钟构造一个酷炫登录框架

    Google在2015的IO大会上,给我们带来了更加详细的Material Design设计规范,同时,也给我们带来了全新的Android Design Support Library,Android ...

  8. 利用GitHub搭建一个酷炫免费的个人博客

    转载自公众号:python_shequ 由于公众号的文章不易后续整理阅读,于是小吴昨天上午花了半个小时使用 GitHub + Hexo 搭建了一下个人博客,打算将公众号的文章搬过去,支持关键字搜索.分 ...

  9. 超详细——手把手教你用threejs实现一个酷炫的模型发光扫描效果(一)

    前言 模型特效是大家在3d可视化项目所追求的,但很多人苦于无法实现一个好的模型效果,本次就手把手一步一步教你实现一个酷炫的模型发光扫描特效,帮让你的项目提升一个逼格.话不多说,先上效果: 本文所使用的 ...

  10. 如何开发一个酷炫的mdx

    使用mdx开发一个酷炫的ppt ​ 效果展示:mdx-deck-slide-decks MDX 是一种书写格式,允许你在 Markdown 文档中无缝地插入 JSX 代码. 你还可以导入(import ...

最新文章

  1. 2021年大数据Kafka(二):❤️Kafka特点总结和架构❤️
  2. 公共基础选择题前10t
  3. Matlab--sort排序
  4. Matlab 2015a 中 pointCloud类相关知识
  5. python 杂记(二)
  6. CSDN又力推一优秀开源项目jeecg,跨时代重构精华版发布
  7. 月老办事处月云开发微信小程序源码
  8. ActionBar(3):搜索条
  9. 为Windows Phone SDK 模拟器安装应用
  10. IPy模块测试demo,打印C段ip列表
  11. 计算机人工智能领域英文文献,人工智能 英文文献译文
  12. 2011年12月13日 timeout 与 refused windows clipbrd
  13. 苹果商城怎么调成中文_海豚加速器拳头账号中文注册下载-海豚加速器拳头账号注册下载 v2020...
  14. 计算机2.0培训心得,信息技术2.0心得体会
  15. since作为连词引导时间状语从句的用法
  16. 离线环境下的软件交付姿态
  17. 《伟大是熬出来的》冯仑与年轻人闲话人生之三
  18. MYSQL语句按中文拼音排序
  19. bim的二次开发需要什么语言_CAD二次开发语言简介
  20. 装饰模式 - Unity

热门文章

  1. mysql 1005 150_錯誤1005 errno:150與mysql
  2. 图像语义分割(2)-DeepLabV1: 使用深度卷积网络和全连接条件随机场进行图像语义分割
  3. DB2 ResultSet用法
  4. 论文阅读笔记(七)——Thin MobileNet
  5. 随笔小杂记(一)——更改整个文件夹内的命名
  6. 课后作业1:字串加密
  7. extract-text-webpack-plugin---webpack插件
  8. springmvc中@PathVariable和@RequestParam的区别(百度收集)
  9. 一个简单的空间配置器
  10. (005)CSS选择器的具体性与层叠