AOSP(Android Open Source
Project,安卓开源项目)自带着一些应用,但随着时间推移,安卓系统的生态日渐成熟,社区中已经涌现了不少可替代的应用,而且这些应用往往拥有着更丰富的功能。

一、前言

澳门新葡萄京官网注册,这次给大家带来的是一款可用于音乐播放器的悬浮菜单按钮,它是基于
FloatingActionButton
上完成,能够联动音乐播放器显示歌曲的进度,设置歌曲的封面和通过封面的旋转来展示播放的状态(停止或者播放)。

AOSP
自带应用的定位是作为平台的一部分,为其他安卓应用提供功能支持。综合考虑后,谷歌从去年年底就着手精简
AOSP
的一些自带应用。目前已经完成了三个应用的清理,其中包括音乐播放器、日历和计算器。

这两天做了个小项目涉及到了远程音乐播放,因为第一次做这种音乐项目,边查资料边做,其中涉及到主要技术点有:

澳门新葡萄京官网注册 1

澳门新葡萄京官网注册 2

  • 如何播放远程网络音乐
  • 如何切换当前正在播放中的音乐资源
  • 如何监听音乐播放的各种状态(播放器状态、播放的进度、缓冲的进度,播放完成)
  • 如何手动操控播放进度
  • 如何在后台模式或者锁屏情况下正常播放音乐
  • 如何在锁屏模式下显示音乐播放信息和远程操控音乐

与音乐播放器的联动

AOSP 精简的应用,左为音乐播放器,右为日历

如果您对一块技术点有兴趣或者正在寻找相关资料,那么本篇或许能提供一些参考或启发。

除此之外,它可以设置一组按钮作为菜单展示,支持上下左右四个方位显示,更方便的是可以在代码中动态的添加按钮或者移除按钮。

精简后的音乐播放器一次只能播放一首音乐,仍具有播放、暂停、控制播放进度的功能。它被公开为
android.media.browse.MediaBrowserService 的 intent
handler,播放列表、搜索、设置和歌曲详细信息等功能已被移除。

二、 网络音乐播放的核心技术点

澳门新葡萄京官网注册 3

AOSP 的日历应用则公开为日历事件的 intent
handler,无法创建新事件,也无法编辑或删除现有事件。日历的多账户支持、提醒功能和设置也已经移除,日周月视图也不复存在,未来可能进一步简化。

根据自己的经验和查了一些音乐播放的相关资料,最简单和最易上手的的技术方案我想应该是采用ios系统自带的AVFoundation框架。

作为菜单的功能

几个应用中精简程度 “最高” 的是计算器,该应用从 AOSP
构建中完全移除,不过仍会作为一个开源项目)独立存在。

我们知道AVFoundation框架是苹果专门为多媒体打造的一个库,这个库非常强大,专门用来处理音视频等复杂的多媒体技术,而本篇要讲的所有技术点就是基于AVFoundation框架中的一个类——AVPlayer。

制作这个自定义控件初衷是想要一个小型的音乐播放器能够在应用的首页显示,它能够展示当前音乐的播放状态,也能够对音乐播放器进行一些操作,比如播放暂停,上下曲切换等。

(文/开源中国)    

那么AVPlayer是什么?

具体的代码和如何引用请移步到 Github
上的项目地址上:FloatingMusicMenu

你可以把他看成是一个已经封装好的播放器,它的作用是用来播放远程的或本地的视频和音频。因为本地的音视频的播放比较简单,这里就不做讲述,本编主要是讲远程音乐播放,因为都是基于AVPlayer同一套API,所以掌握远程音乐播放其实就是相当于掌握远程视频播放。好了废话就不多说了,下面开始上菜。

如何实现

说完作品,我们来讲讲是怎么用代码实现这些效果的吧。

1、导入AVFoundation框架,创建AVPlayer播放器

利用反射使 FloatingActionButton 填充整个按钮

FloatingActionButton
中是没有开放的方法来调整图片大小的,所以这时候就想到了使用反射的方法来调整。

 /**
 * 利用反射重新定义fab图片的大小,使其充满整个fab
 */
public void setMaxImageSize() {
    try {
        Class clazz = getClass().getSuperclass();
        Method sizeMethod = clazz.getDeclaredMethod("getSizeDimension");
        sizeMethod.setAccessible(true);
        int size = (Integer) sizeMethod.invoke(this);
        Field field = clazz.getDeclaredField("mMaxImageSize");
        field.setAccessible(true);
        field.set(this, size);
    } catch (NoSuchFieldException e) {
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    } catch (NoSuchMethodException e) {
        e.printStackTrace();
    } catch (InvocationTargetException e) {
        e.printStackTrace();
    }
    postInvalidate();
}

其中通过 getSizeDimension 可以获得 FloatingActionButton
的实际大小,而 mMaxImageSize 则是图片的指定大小,所以可以重新赋值
mMaxImageSize 来调整图片的大小。

具体代码则是在 FloatingMusicButton 中。

-(AVPlayer *)player
{
  if (_player == nil) {
     // AVPlayerItem是一个包装音乐资源的类,初始化时可以传入一个音乐的url
    AVPlayerItem *item = [[AVPlayerItem alloc] initWithURL:[NSURL URLWithString:@"http://xxxxxxxx"]];
    //通过AVPlayerItem初始化player
    _player = [[AVPlayer alloc] initWithPlayerItem:item];
  }

  return _player;
}

一个可以旋转的 Drawable 图片

这里要实现的效果有点多,首先要将图片的形状变成圆形,其次要在图片的周围留出一定的边距以便显示进度条,最后让图片转起来。

圆形图片有多种方式可以实现,这里我们通过 BitmapShader 来进行变换。

/**
 * 圆形
 */
private void circleBitmap() {
    BitmapShader bitmapShader = new BitmapShader(mBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
    mPaint = new Paint();
    mPaint.setAntiAlias(true);
    mPaint.setShader(bitmapShader);
}

BitmapShader
通过设置给mPaint,然后用这个mPaint绘图时,就会根据你设置的TileMode,对绘制区域进行着色。
mBitmapShader = new BitmapShader(bitmap, TileMode.CLAMP, TileMode.CLAMP);

绘制进度条可以通过 canvas.drawArc()
来实现,记得要先将之前的图片缩小,防止进度条覆盖在图片之上。

@Override
public void draw(Canvas canvas) {
    float progressWidth = mWidth * progressPercent / 100f;
    float halfWidth = progressWidth / 2;
    // 画背景图
    canvas.save();
    canvas.rotate(mRotation, getBounds().centerX(), getBounds().centerY());
    float scale = 1 - progressWidth * 2.0f / mWidth;
    canvas.scale(scale, scale, mWidth / 2.0f, mWidth / 2.0f);
    canvas.drawCircle(mWidth / 2, mWidth / 2, mWidth / 2, mPaint);
    canvas.restore();
    // 画进度条
    rectF.set(halfWidth, halfWidth, mWidth - halfWidth, mWidth - halfWidth);
    canvas.drawArc(rectF, -90, progress, false, progressPaint);
}

让图片旋转则是很简单的旋转 canvas 画布,利用 Handler
不断循环发送消息来更新 mRotation 保持旋转的动画。

canvas.rotate(mRotation, getBounds().centerX(), getBounds().centerY());

具体代码在 RotatingProgressDrawable 中。

此处懒加载创建,让播放器成为控制器的全局属性,注意需要强引用,否则回收释放掉了就无法播放。

可展开和收缩的 ViewGroup

自定义 ViewGroup
首先要做的两件事是测量控件的宽高和子控件的摆放,由于控件可以四个方向自由摆放,所以需要针对不同的方向进行不同的计算和摆放。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    measureChildren(widthMeasureSpec, heightMeasureSpec);
    switch (floatingDirection) {
        case FLOATING_DIRECTION_UP:
        case FLOATING_DIRECTION_DOWN:
            onMeasureVerticalDirection();
            break;
        case FLOATING_DIRECTION_LEFT:
        case FLOATING_DIRECTION_RIGHT:
            onMeasureHorizontalDirection();
            break;
    }
}

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    switch (floatingDirection) {
        case FLOATING_DIRECTION_UP:
            onUpDirectionLayout(l, t, r, b);
            break;
        case FLOATING_DIRECTION_DOWN:
            onDownDirectionLayout(l, t, r, b);
            break;
        case FLOATING_DIRECTION_LEFT:
            onLeftDirectionLayout(l, t, r, b);
            break;
        case FLOATING_DIRECTION_RIGHT:
            onRightDirectionLayout(l, t, r, b);
            break;
    }
}

控件的大小和位置确定好后就需要为每个子视图添加动画,比较简单的做法是将动画相关的变量放入
LayoutParams 中,方便管理。

private class MenuLayoutParams extends LayoutParams {

        private ObjectAnimator expandDirAnim = new ObjectAnimator();
        private ObjectAnimator expandAlphaAnim = new ObjectAnimator();
        private ObjectAnimator collapseDirAnim = new ObjectAnimator();
        private ObjectAnimator collapseAlphaAnim = new ObjectAnimator();
        ...
}

同时要重写 generateLayoutParams() 方法,使其在 addView() 时能够将
MenuLayoutParams 赋给每个子视图。

@Override
protected LayoutParams generateDefaultLayoutParams() {
    return new MenuLayoutParams(super.generateDefaultLayoutParams());
}

@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
    return new MenuLayoutParams(super.generateLayoutParams(attrs));
}

@Override
protected LayoutParams generateLayoutParams(LayoutParams p) {
    return new MenuLayoutParams(super.generateLayoutParams(p));
}

这样当在 onLayout()
的方法期间就可以为每个子视图确定动画的距离,方向等属性。

具体的细节可以查看类 —— FloatingMusicMenu

2、播放或停止音乐

总结

这个控件是我在写 MoeMusic
项目时就已经存在的,但只有一个向上展开的功能,没有现在这么完整。想到自己好久没有写开源项目了,就把这个控件拎出来完善了一下开源给大家,希望能够喜欢。如果使用过程中有任何问题或者建议可以直接联系!

  //开始播放
  [self.player play];
   //停止播放
  [self.player pause];

这个没什么好讲的,只要调用AVPlayer的两个实例方法

3、切换当前正在播放中的音乐资源

//创建需要播放的AVPlayerItem
 AVPlayerItem *item = [[AVPlayerItem alloc] initWithURL:[NSURL URLWithString:model.url]];
 //替换当前音乐资源 
 [self.player replaceCurrentItemWithPlayerItem:item];

这个可以用于歌曲的切换,如上一首、下一首。

4、通过KVO监听播放器的状态

 [self.player.currentItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];

拿到播放器的currentItem,注册当前对象为观察者,监听它的status属性。status属性是AVPlayerItemStatus类型,它是一个枚举类型,如下:

typedef NS_ENUM(NSInteger, AVPlayerItemStatus) {
  AVPlayerItemStatusUnknown,//未知状态
  AVPlayerItemStatusReadyToPlay,//准备播放
  AVPlayerItemStatusFailed//加载失败
};

当status属性值发生改变时,就会触发观察者方法的回调,如下:

//观察者回调
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
  //注意这里查看的是self.player.status属性
  if ([keyPath isEqualToString:@"status"]) {
    switch (self.player.status) {
      case AVPlayerStatusUnknown:
      {
        NSLog(@"未知转态");
      }
        break;
      case AVPlayerStatusReadyToPlay:
      {
        NSLog(@"准备播放");
      }
        break;
      case AVPlayerStatusFailed:
      {
        NSLog(@"加载失败");
      }
        break;
         default:
        break;
    }
  }
}


self.player.status ==  AVPlayerStatusReadyToPlay时,音乐就会开始正常播放,另外两种状态音乐是无法播放的,可以在上面方法相应状态里给出提示。这里需要特别强调一点的是观察者监听的对象是self.player.currentItem,而不是self.player,而当监听的属性发生改变时,观察者回调的方法里需要查看的是self.player.status。当然,你也可以不这么干,但是我尝试过好几次,不这么干的后果是无法监听到self.player.status属性的改变。

当音乐播放完成,或者切换下一首歌曲时,请务必记得移除观察者,否则会crash。操作如下:

//移除观察者
 [self.player.currentItem removeObserver:self forKeyPath:@"status"];

5、监听音乐的缓冲进度

这个也是通过KVO监听播放器当前播放的音乐资源AVPlayerItem的loadedTimeRanges属性。我们先看监听,如下:

//KVO监听音乐缓冲状态
[self.player.currentItem addObserver:self forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionNew context:nil];

loadedTimeRanges属性发生改变时,回调如下:

-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
  if ([keyPath isEqualToString:@"loadedTimeRanges"]) {

    NSArray * timeRanges = self.player.currentItem.loadedTimeRanges;
    //本次缓冲的时间范围
    CMTimeRange timeRange = [timeRanges.firstObject CMTimeRangeValue];
    //缓冲总长度
    NSTimeInterval totalLoadTime = CMTimeGetSeconds(timeRange.start) + CMTimeGetSeconds(timeRange.duration);
    //音乐的总时间
    NSTimeInterval duration = CMTimeGetSeconds(self.player.currentItem.duration);
    //计算缓冲百分比例
    NSTimeInterval scale = totalLoadTime/duration;
    //更新缓冲进度条
    self.loadTimeProgress.progress = scale;
  }

}

loadedTimeRanges这个属性是一个数组,里面装的是本次缓冲的时间范围,这个范围是用一个结构体CMTimeRange表示,当然在oc中结构体是不能直接存放数组的,所以它被包装成了oc对象NSValue

我们来看下这个结构体:

typedef struct
{
  CMTime      start;    
  CMTime      duration;  
} CMTimeRange;

start表示本次缓冲时间的起点,duratin表示本次缓冲持续的时间范围,具体详细的计算方法可以看上面方法的实现。

当音乐播放完成,或者切换下一首歌曲时,请务必记得移除观察者,否则会crash。操作如下:

[self.player.currentItem addObserver:self forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionNew context:nil];

6、监听音乐播放的进度

这个不是通过KVO了,AVPlayer专门提供了下面这个api用来监听播放的进度:

/**
 监听音乐播放进度

 @param interval 监听的时间间隔,用来设置多长时间回调一次
 @param queue  队列,一般传主队列
 @param block  回调的block,会把当前的播放时间传递过来

 @return 监听的对象
 */
- (id)addPeriodicTimeObserverForInterval:(CMTime)interval queue:(nullable dispatch_queue_t)queue usingBlock:(void (^)(CMTime time))block;

操作如下:

 __weak typeof(self) weakSelf = self;
  self.timeObserver = [self.player addPeriodicTimeObserverForInterval:CMTimeMake(1.0, 1.0) queue:dispatch_get_main_queue() usingBlock:^(CMTime time) {
    //当前播放的时间
    float current = CMTimeGetSeconds(time);
    //总时间
    float total = CMTimeGetSeconds(item.duration);
    if (current) {
      float progress = current / total;
      //更新播放进度条
      weakSelf.playSlider.value = progress;
      weakSelf.currentTime.text = [weakSelf timeFormatted:current];
    }
  }];

我们可以这个block里面拿到当前播放时间,根据总时间计算出当前播放所占的时间比例,最后更新播放进度条。这里又涉及到了一个数据类类型CMTime,它也是一个结构体,用来作为时间的格式,定义如下:

  typedef struct
   CMTimeValue  value;    
   CMTimeScale  timescale;  
   CMTimeFlags  flags;    
   CMTimeEpoch  epoch;    
  } CMTime;

CMTime是以分数的形式表示时间,value表示分子,timescale表示分母,flags是位掩码,表示时间的指定状态。所以我们要获得时间的秒数需要分子除以分母。当然你还可以用下面这个函数来获取时间的秒数:

Float64 CMTimeGetSeconds(CMTime time)

最后,当音乐播放完成或者切换音乐时,依然需要移除监听:

if (self.timeObserver) {
    [self.player removeTimeObserver:self.timeObserver];
    self.timeObserver = nil;
  }

7、手动超控(移动滑块)播放进度

这是一个播放音视频很常见的功能,所以强大的AVPlayer理所当然的提供了几个api,下面只讲述其中最简单的一个:

/**
 定位播放时间

 @param time 指定的播放时间
 */
- (void)seekToTime:(CMTime)time;
具体使用如下:

//移动滑块调整播放进度
- (IBAction)playSliderValueChange:(UISlider *)sender
{
  //根据值计算时间
  float time = sender.value * CMTimeGetSeconds(self.player.currentItem.duration);
  //跳转到当前指定时间
  [self.player seekToTime:CMTimeMake(time, 1)];
}

8、监听音乐播放完成

一般音视频播放完成时我们或多或少的都要处理一些业务,比如循环播放,播完退出界面等等。下面看下如何监听AVPlayer的播放完成。

//给AVPlayerItem添加播放完成通知
  [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playFinished:) name:AVPlayerItemDidPlayToEndTimeNotification object:_player.currentItem];

这里是采用注册监听AVPlayerItemDidPlayToEndTimeNotification通知,当AVPlayer一播放完成时,便会发出这个通知,我们收到通知后进行处理即可

9、设置音乐后台播放

我们知道运行在ios系统下的程序一旦进入后台就会处于休眠状态,程序停止运行了,也就播放不了什么音乐了。但是有一些特定功能的app还是处于可以后台运行的,比如音乐类型的app正处于这个范畴。但是,并不是说你在应用中播放音乐就能后台高枕无忧的运行了,你依然需要做如下几步操作:

(1)开启后台模式

target ->capabilities-> Background modes ->打开开关
->勾选第一个选项澳门新葡萄京官网注册 4

(2)程序启动时设置音频会话

  //一般在方法:application: didFinishLaunchingWithOptions:设置
  //获取音频会话
  AVAudioSession *session = [AVAudioSession sharedInstance];
  //设置类型是播放。
  [session setCategory:AVAudioSessionCategoryPlayback error:nil];
  //激活音频会话。
  [session setActive:YES error:nil];

以上两步设置无误,程序进入后台模式,便可以进行音乐播放

10、如何设置音乐锁频信息

我们看百度音乐锁频时,也依然能在屏幕上展示歌曲的信息,以及切换歌曲等。下面看看这个功能是如何实现的:

//音乐锁屏信息展示
- (void)setupLockScreenInfo
{
  // 1.获取锁屏中心
  MPNowPlayingInfoCenter *playingInfoCenter = [MPNowPlayingInfoCenter defaultCenter];

  //初始化一个存放音乐信息的字典
  NSMutableDictionary *playingInfoDict = [NSMutableDictionary dictionary];
  // 2、设置歌曲名
  if (self.currentModel.name) {
    [playingInfoDict setObject:self.currentModel.name forKey:MPMediaItemPropertyAlbumTitle];
  }
  // 设置歌手名
  if (self.currentModel.artist) {
    [playingInfoDict setObject:self.currentModel.artist forKey:MPMediaItemPropertyArtist];
  }
  // 3设置封面的图片
  UIImage *image = [self getMusicImageWithMusicId:self.currentModel];
  if (image) {
    MPMediaItemArtwork *artwork = [[MPMediaItemArtwork alloc] initWithImage:image];
    [playingInfoDict setObject:artwork forKey:MPMediaItemPropertyArtwork];
  }

  // 4设置歌曲的总时长
  [playingInfoDict setObject:self.currentModel.detailDuration forKey:MPMediaItemPropertyPlaybackDuration];

  //音乐信息赋值给获取锁屏中心的nowPlayingInfo属性
  playingInfoCenter.nowPlayingInfo = playingInfoDict;

  // 5.开启远程交互,只有开启这个才能进行远程操控
  [[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
}

这里设置图片时需要注意下,异步加载网络图片后再设置是无效的,所以图片信息最好是先请求下来后再进行设置。

远程超控的回调如下:

//监听远程交互方法
- (void)remoteControlReceivedWithEvent:(UIEvent *)event
{

  switch (event.subtype) {
    //播放
    case UIEventSubtypeRemoteControlPlay:{
      [self.player play];
          }
      break;
    //停止
    case UIEventSubtypeRemoteControlPause:{
      [self.player pause];
          }
      break;
    //下一首
    case UIEventSubtypeRemoteControlNextTrack:
      [self nextBtnAction:nil];
      break;
    //上一首
    case UIEventSubtypeRemoteControlPreviousTrack:
      [self lastBtnAction:nil];
      break;

    default:
      break;
  }
}

三、总结

最后,画了一张图总结下播放远程网络音乐的流程:澳门新葡萄京官网注册 5

根据QQ音乐的界面做了个小demo,下面是demo的真机前台和后台播放的运行效果:澳门新葡萄京官网注册 6

四、结束语

播放远程网络音乐的核心技术点基本上已经写完,当然AVPlayer还有很多强大的功能没有写出来,有兴趣的可以进一步挖掘。写到这里已经疲倦至极,后续会持续更新一些精彩的技术点,也希望大家多多支持脚本之家。

您可能感兴趣的文章:

  • iOS如何获取手机的Mac地址
  • iOS、Mac OS
    X系统中编程实现汉字转拼音的方法(超级简单)
  • WMI获取硬件信息封装函数方法(联想台式机出厂编号 CPUID BIOS序列号
    硬盘信息 显卡信息 MAC地址)
  • Nagios远程监控安装与配置详解图文
  • iOS10
    适配远程推送功能实现代码
  • iOS实现远程推送原理及过程
  • iOS10最新实现远程通知的开发教程详解
  • iOS开发之运动事件和远程控制
  • iOS Remote
    Notification远程消息推送处理
  • iOS-Mac远程连接控制Window