概述

网易云音乐是一款非常优秀的音乐播放器,尤其是播放界面,使用唱盘机风格,显得格外古典优雅。这里抛砖引玉,原文地址:
http://www.jianshu.com/p/cb54990219d9 <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实现毛玻璃效果
<http://www.jianshu.com/p/f8463b3bbffb>
,这里为了方便大家使用,我就直接使用工具类的方式,关于模糊化的实现逻辑大家可以搜索一下“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(); }
<>唱针变化逻辑

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

* 初始状态为暂停/停止时,点击播放按钮,此时唱针移动到底部。
* 初始状态为播放时,点击暂停按钮,此时唱针移到顶部。
* 初始状态为播放时,手指按住唱盘并稍微偏移,等唱针未移到顶部时,立刻松开手指,此时唱针回到顶部后立刻再回到唱盘位置。
* 初始状态为暂停/停止时,点击播放,此时唱针往下移动,当唱针还未移到底部,手指马上按住唱盘并偏移,此时唱针立刻往顶部移动。
* 初始状态为播放/暂停/停止时,左右滑动唱片进行音乐切换,唱针动画未结束时,立刻点击上/下一首按钮,进行音乐切换,此时唱针状态不能出现混乱。
初始状态为暂停/停止时,点击播放按钮,此时唱针移动到底部。 
 
初始状态为播放时,点击暂停按钮,此时唱针移到顶部。 
 
初始状态为播放时,手指按住唱盘并稍微偏移,等唱针未移到顶部时,立刻松开手指,此时唱针回到顶部后立刻再回到唱盘位置。 
 
初始状态为暂停/停止时,点击播放,此时唱针往下移动,当唱针还未移到底部,手指马上按住唱盘并偏移,此时唱针立刻往顶部移动。 
这里写链接内容
<http://upload-images.jianshu.io/upload_images/3240261-bc47abb2298a5c36.gif?imageMogr2/auto-orient/strip>
 

初始状态为播放/暂停/停止时,左右滑动唱片进行音乐切换,唱针动画未结束时,立刻点击上/下一首按钮,进行音乐切换,此时唱针状态不能出现混乱,反复做了步骤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). 
附上源码,这里可能需要大家自己编译。 


附:仿网易云音乐界面源码 <https://github.com/AchillesLzg/jianshu-neteasedisc>

转载:https://blog.csdn.net/xiangzhihong8/article/details/54353956



友情链接
KaDraw流程图
API参考文档
OK工具箱
云服务器优惠
阿里云优惠券
腾讯云优惠券
华为云优惠券
站点信息
问题反馈
邮箱:[email protected]
QQ群:637538335
关注微信