Android MAD HW3 仿网易云播放器

Android MAD HW3 仿网易云播放器

一、项目说明

本项目为2021学年移动应用开发课程第三次作业,要求是实现一个仿网易云音乐播放器,本想提前做的,还是拖到检查前一天才开始写,就写一下文档记录一下一个通宵肝完的音乐播放器吧
项目仓库:仿网易云播放器demo

二、作业要求&实现

  • 实现具有以上布局样式的音乐播放器

    • 在这里插入图片描述
  • 播放器具有正常的播放、暂停、停止、继续、退出功能,按停止键会重置封面转角,进度条和播放按钮;按退出键将停止播放并退出程序

  • 后台播放功能,按手机的返回键和 home 键都不会停止播放,而是转入后台进行播放

  • 进度条能正确显示播放进度、拖动进度条改变进度功能

  • 显示当前播放时间功能,播放时图片旋转(圆形图片的实现使用的是一个开源控件 CircleImageView)

  • 可以从本地存储中读取音乐资源进行播放

  • 加分项:在保持上述原有的功能的情况下,使用 rxJava 代替 Handler 进行 UI 的更新

具体实现效果:

  1. 实现作业要求中所有功能
  2. 使用RxJava替代Handler实现异步操作进行UI更新
  3. 使用BroadCast进行Activity与Service之间通信
  4. 实现背景显示歌曲封面,同时背景模糊(参照网易云效果实现
  5. 实现点击 …… 后弹出自定义Dialog,显示未被添加的本地音乐,同时添加支持单选多选全选模式,长按进入多选模式,同时页面图片使用Glide缓存加载
  6. 实现完全沉浸模式,不显示导航栏和底部虚拟按键栏(同游玩全屏游戏王者、吃鸡类似,滑动底部屏幕显示虚拟按钮,其他情况下应用全屏显示
  7. 实现滑动切歌功能,通过重写viewpager的onpagechangelistener实现

三、实现思路

  • UI界面
    • 大体依照网易云界面,由于项目要求有本地文件播放功能,和退出按钮,就考虑在原有播放键两端或者原有评论位置替换
    • 大体部件包括:旋转的CD,播放、暂停按钮,文件选择,退出按钮,上方toolbar
  • toolBar:显示歌曲名称和歌手,右侧分享按钮
    • 歌曲名称和歌手通过读取音频文件获取
  • CD旋转:
    • 用助教要求的CircleImageView实现,旋转用Animation来做吧(应该
    • 切换用重写一个PageView来实现,通过重写OnPageChangeListener 对滑动进行判断来进行页面切换
  • SeekBar:
    • 通过Activity和service进行broadcast通信通知seekbar改变,同时在设定handler作为定时器进行seekbar的更新操作
  • 播放暂停:
    • 调用MediaPlayer的播放功能
  • 歌曲切换:
    • 模仿网易云功能实现
  • 文件读取:
    • 读取本地音乐文件播放
  • 退出
    • 没啥好说的,直接退出activity即可
  • 后端运行:
    • service
  • Activity与Service通信:
    • 用BroadCast实现
  • 所有异步数据更新操作:
    • RxJava实现
  • 弹出自定义dialog实现添加音乐:
    • 重写BaseAdapter,以及为dialog添加监听
  • 拓展:
    • 背景模糊、沉浸模式、点击弹出列表选择音乐加入(仿网易云)、实现单选多选全选加入音乐

四、成果展示

基础页面1基础页面2滑动下方屏幕显示虚拟按键
在这里插入图片描述
歌曲单选添加歌曲全选歌曲多选添加
在这里插入图片描述

五、目录结构

在这里插入图片描述

六、具体实现

获取权限

因为实现音乐播放器要用到存储的读取权限,所以我们要改写Manifest文件

  1. 加入所需权限

    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"
        tools:ignore="ProtectedPermissions" />
    
  2. 安卓10.0之后需要在application中加入

    android:requestLegacyExternalStorage="true"
    

    才能访问外部存储,否则只能播放系统自带音频,不能播放网易云/qq音乐下载的音乐文件(划重点!!!!

初始化BroadCastReceiver

初始化广播接受器,过滤接受对应的广播类型,用于activity和Service之间数据传输

    private void initBroadcastReceiver(){
        IntentFilter filter = new IntentFilter();
        filter.addAction(BroadType.SERVICE2ACTIVITY_MUSIC_LIST_EMPTY);
        filter.addAction(BroadType.SERVICE2ACTIVITY_MUSIC_PROCESS);
        filter.addAction(BroadType.SERVICE2ACTIVITY_MUSIC_DURATION);
        filter.addAction(BroadType.SERVICE2ACTIVITY_MUSIC_PLAY_OVER);
        filter.addAction(BroadType.SERVICE2ACTIVITY_MUSIC_PAUSE);
        filter.addAction(BroadType.SERVICE2ACTIVITY_MUSIC_LAST);
        filter.addAction(BroadType.SERVICE2ACTIVITY_MUSIC_PLAY);
        filter.addAction(BroadType.SERVICE2ACTIVITY_MUSIC_NEXT);
        LocalBroadcastManager.getInstance(this).registerReceiver(mainBroadcastReceiver,filter);
    }

初始化View

  • 首先初始化播放其页面所要用的view视图,并为seekbar添加监听:

    • 其中当停止拖拽时,调用handler为seekbar进行每秒的定时更新
    seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
    
                @Override
                public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
                    curr_time.setText(duration2Time(progress));
                }
    
                @Override
                public void onStartTrackingTouch(SeekBar seekBar) {
                    timeHandler.removeMessages(0x1);
                }
    
                @Override
                public void onStopTrackingTouch(SeekBar seekBar) {
                    sendBroadCast(BroadType.MUSIC_SEEKBAR_CHANGE,seekBar.getProgress());
                    timeHandler.sendEmptyMessageDelayed(0x1,1000);
                }
            });
    
  • 之后为viewpager添加自定义PageChangeListener

    • 监听viewpager的滑动状态以及设定页面切换后的逻辑动作,如:更新toolbar、背景、歌曲等
    • 主要通过onPageScoll中对滑动Offset 进行判断左滑or右滑
    • 以及在onPageSelected中进行判断页面切换
    viewPager.addOnPageChangeListener(MyPageChangeListener);
    
            @Override
            public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
                //左滑
                if (lastPositionOffsetPixels > positionOffsetPixels) {
                    if (positionOffset < 0.5) {
                        changeViewPagerPage(position);
                    } else {
                        changeViewPagerPage(viewPager.getCurrentItem());
                    }
                }
                //右滑
                else if (lastPositionOffsetPixels < positionOffsetPixels) {
                    if (positionOffset > 0.5) {
                        changeViewPagerPage(position + 1);
                    } else {
                        changeViewPagerPage(position);
                    }
                }
                lastPositionOffsetPixels = positionOffsetPixels;
            }
            
                    @Override
            public void onPageSelected(int position) {
                //更新背景图 实现渐变
                setBackGroundView(position);
                //重置动画-----------------------------------------------------------------------
                animationList.get(position).start();
                //更新上方标题作者信息
                updateBarInfo(position);
                if (position > currentItem){
                    sendBroadCast(BroadType.MUSIC_NEXT,null);
                }else {
                    sendBroadCast(BroadType.MUSIC_LAST,null);
                }
                currentItem = position;
            }
    

    初始化音乐数据

    这里因为我设定默认一开始拉取本地三首音乐进播放器,所以一开始要搜索本地音乐并加入Service

    这里通过调用Util中的LocalMusicUitls来获取本地存储中的音乐,在获取List完成之后,使用RxJava 进行异步处理,对每一个Song调用addPageViewPage函数进行加载新的view以及将Song数据存储到列表中,之后使用列表数据更新viewpager实现数据加入

      //歌曲数据存储列表
    private List<Song> musicDataList = new ArrayList<>();
    //viewpager 视图列表
    private List<View> viewList = new ArrayList<>();
    
  private void initMusicData(){
            List<Song> tem_SongList = LocalMusicUtils.getmusic(this);
            showLog("总音乐数:" +tem_SongList.size() + "");
            while (tem_SongList.size() > 3){
                tem_SongList.remove(0);
            }
            sendRxJavaSongList(tem_SongList);
        }
        
   private void sendRxJavaSongList(List<Song> songs){
        Observable<Song> observable = Observable.fromIterable(songs);
        observable.subscribe(songObserver);
    }
    
    

根据音乐数据更新viewpager

在初始化音乐数据完成后,RxJava 的观察者调用onNext来通过addPageViewPage实现页面view、animation、Drawable的更新,在onComplete中刷新adapter实现viewpager的更新,在首次初始化音乐数据后,调用songObserver,在oncomplete中进行Service的初始化

  • 通过song数据读取对应的歌曲封面等

        private void addPageViewPage(Song song){
            musicDataList.add(song);
            View add_view = LayoutInflater.from(this).inflate(R.layout.page_layout,viewPager,false);
            CircleImageView circleImageView = add_view.findViewById(R.id.cd_image_view);
            Bitmap album_bitmap = LocalMusicUtils.getArtwork(this,song.id,song.albumId,true,false);
            Drawable drawable = new BitmapDrawable(getResources(),album_bitmap);
            circleImageView.setImageDrawable(drawable);
            animationList.add(getDiscObjectAnimator(circleImageView)) ;
            viewList.add(add_view);
        }
    

在createActivity的最后,因此虚拟按键,并设置全屏

    /**
     * 隐藏虚拟按键,并且全屏
     */
    protected void hideBottomUIMenu() {
        //隐藏虚拟按键,并且全屏
        if (Build.VERSION.SDK_INT > 11 && Build.VERSION.SDK_INT < 19) { // lower api
            View v = this.getWindow().getDecorView();
            v.setSystemUiVisibility(View.GONE);
        } else if (Build.VERSION.SDK_INT >= 19) {
            //for new api versions.
            View decorView = getWindow().getDecorView();
            int uiOptions = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
            decorView.setSystemUiVisibility(uiOptions);
        }
    }

Service的初始化

首先在Activity中初始化完3首歌曲信息之后,在RxJava的Observer Oncomplete回调中调用initService

使用Intent方法,将歌曲信息List传递给Service

//start service
        Intent intent = new Intent(this, MusicService.class);
        intent.putExtra(FlagMessage.SERVICE_SET_PARAM, (Serializable)musicDataList);
        startService(intent);

在Service中,首先我们了解Service并不是在startService 的intent传递时创建,而是在startservice时调用onstartCommand,开始工作,所以我们首先在onCreate中初始化BroadCast广播通信,在onStartCommand中提取对应的歌曲信息,之后调用封装好的MediaPlay的play函数开始播放

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        if (intent != null){
            List<Song> songList1 = (ArrayList<Song>) intent.getSerializableExtra(FlagMessage.SERVICE_SET_PARAM);
            for (Song song: songList1){
                showLog("song: "+ song.name);
            }
            songList = songList1;
            for (Song song: songList){
                showLog("Songlist item : " + song.name);
            }
            showLog("songlist size : "+ songList.size());
        }
        showLog("start Command");
        play(0);
        return super.onStartCommand(intent, flags, startId);
    }

歌曲播放等逻辑

为了实现基础歌曲播放,暂停下一首,上一首功能,我们要在Activity和Sercive中分别实现对应的play、stop、next、last、seekbarchange函数

Servie中,通过BroadcastReceiver接受对应信号来调用对应逻辑函数:

    private BroadcastReceiver musicBroadcastReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            showLog("Service receive BoardCast : "+ intent.getAction());
            switch (intent.getAction()){
                case BroadType.MUSIC_LAST:
                    last();
                    break;
                case BroadType.MUSIC_NEXT:
                    next();
                    break;
                case BroadType.MUSIC_PLAY:
                    play(currentMusicPosition);
                    break;
                case BroadType.MUSIC_SEEKBAR_CHANGE:
                    seekBarChange(intent);
                    break;
                case BroadType.MUSIC_ADD_SONGS:
                    List<Song> songList1 = (ArrayList<Song>) intent.getSerializableExtra(BroadType.MUSIC_ADD_SONGS);
                    songList.addAll(songList1);
                    showLog("歌曲数量增加,现在的数量为" + songList.size());
                    break;
            }
        }
    };
  • Service:

    • play:

      • 根据position是否为当前页面和是否时播放状态,决定发出播放、暂停、切歌等对应的广播消息,并执行相关函数

            private void play(final int position) {
                showLog("Service play: "+ position );
                if(position >= songList.size()){
                    sendBroadcastMessage(BroadType.SERVICE2ACTIVITY_MUSIC_LIST_EMPTY);
                    return;
                }
        
                if(currentMusicPosition == position){
                    if (mMediaPlayer == null){
                        mMediaPlayer = new MediaPlayer();
        
                        String tem = songList.get(position).path.replaceAll(" ","\\ ");
        //                File musicFile = new File(songList.get(position).path);
                        File musicFile = new File(tem);
                        showLog("songList.size() :" + songList.size()+ "get position:" + songList.get(position).singer + " getPath: " + tem);
                        mMediaPlayer = MediaPlayer.create(getApplicationContext(), Uri.fromFile(musicFile));
                        mMediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
                            @Override
                            public void onPrepared(MediaPlayer mp) {
                                mMediaPlayer.start();
                                mMediaPlayer.setOnCompletionListener(MusicService.this);
                                currentMusicPosition = position;
                                currentSongPosition = 0;
                                sendBroadcastMessage(BroadType.SERVICE2ACTIVITY_MUSIC_DURATION);
                                sendBroadcastMessage(BroadType.SERVICE2ACTIVITY_MUSIC_PLAY);
                            }
                        });
        
                    }
                    else if (mMediaPlayer.isPlaying()){
                        //记录暂停时位置
                        currentSongPosition = mMediaPlayer.getCurrentPosition();
                        mMediaPlayer.pause();
                        sendBroadcastMessage(BroadType.SERVICE2ACTIVITY_MUSIC_PAUSE);
                    }else {
                        if (currentSongPosition!= 0){
                            //从暂停位置开始播放
                            mMediaPlayer.seekTo(currentSongPosition);
                            mMediaPlayer.setOnSeekCompleteListener(new MediaPlayer.OnSeekCompleteListener() {
                                @Override
                                public void onSeekComplete(MediaPlayer mp) {
                                    mMediaPlayer.start();
                                }
                            });
                        }else {
                            mMediaPlayer.start();
                        }
                        sendBroadcastMessage(BroadType.SERVICE2ACTIVITY_MUSIC_PLAY);
                    }
                }else {
                    //歌曲切换情况
                    showLog("play another music");
                    if (mMediaPlayer != null){
                        mMediaPlayer.stop();
                    }
                    mMediaPlayer = null;
                    File musicFile = new File(songList.get(position).path);
                    showLog("歌曲path: " + songList.get(position).path);
                    mMediaPlayer = MediaPlayer.create(getApplicationContext(), Uri.fromFile(musicFile));
                    mMediaPlayer.start();
                    mMediaPlayer.setOnCompletionListener(MusicService.this);
                    currentMusicPosition = position;
                    currentSongPosition = 0;
                    sendBroadcastMessage(BroadType.SERVICE2ACTIVITY_MUSIC_DURATION);
                    sendBroadcastMessage(BroadType.SERVICE2ACTIVITY_MUSIC_PLAY);
        
                }
            }
        
      • next:调用play

      • last:调用play

      • seekBarchange:

        • 向activity发送seekbarchange的信号,并且设置MediaPlayer播放对应progress位置音频
            private void seekBarChange(Intent intent){
                if (mMediaPlayer.isPlaying()){
                    int position = intent.getIntExtra(BroadType.MUSIC_SEEKBAR_CHANGE,0);
                    mMediaPlayer.seekTo(position);
                }else {
                    Intent i = new Intent(BroadType.SERVICE2ACTIVITY_MUSIC_PROCESS);
                    i.putExtra(BroadType.SERVICE2ACTIVITY_MUSIC_PROCESS,mMediaPlayer.getCurrentPosition());
                    LocalBroadcastManager.getInstance(this).sendBroadcast(i);
                }
            }
        
  • Activity

    • 逻辑函数通过点击button以及收到Service的歌曲播放暂停广播调用

    • play:播放页面旋转动画,设置图片转动,开始更新seekbar

      private void play(){
              animationList.get(viewPager.getCurrentItem()).resume();
              button_play.setImageResource(R.drawable.ic_pause);
              startUpdateSeekBar();
          }
      
    • pause:与play相反,暂停

    • next/last:通过对应button点击触发,通过viewpager的setcurrentItem实现页面切换

      •     private void last(){
                stopUpdateSeekBarProgree();
                int currentItem = viewPager.getCurrentItem();
                if (currentItem == 0){
                    Toast.makeText(this,"已经是第一个了",Toast.LENGTH_SHORT).show();
                }else {
                    viewPager.setCurrentItem(currentItem - 1,false);
                }
            }
        
  • Activity页面切换:

    • 当调用next、last函数时,通过viewpager的setcurrentItem触发listener的onPageSelected回调,进行上方标题栏、歌曲、背景图等的更新

      @Override
              public void onPageSelected(int position) {
                  //更新背景图 实现渐变
                  setBackGroundView(position);
                  //重置动画-----------------------------------------------------------------------
                  animationList.get(position).start();
                  //更新上方标题作者信息
                  updateBarInfo(position);
                  if (position > currentItem){
                      sendBroadCast(BroadType.MUSIC_NEXT,null);
                  }else {
                      sendBroadCast(BroadType.MUSIC_LAST,null);
                  }
                  currentItem = position;
              }
      

背景更新

更新背景,并设置高斯模糊,实现毛玻璃效果

  • 首先根据position获取歌曲列表中对应歌曲信息:并用于获取图片

        private void setBackGroundView(int position){
            View viewAtPosition = viewList.get(position);
            Bitmap album_bitmap = LocalMusicUtils.getArtwork(this,musicDataList.get(position).id,musicDataList.get(position).albumId,true,false);
            Bitmap deal_bitmap = blurBitmap(album_bitmap,25);
            Drawable drawable = new BitmapDrawable(getResources(),deal_bitmap);
            root_layout.setBackground(drawable);
            root_layout.invalidate();
        }
    
  • 调用blurBitmap进行高斯模糊,实现毛玻璃效果

        public Bitmap blurBitmap(Bitmap bitmap, int radius) {
            //创建一个空bitmap,其大小与我们想要模糊的bitmap大小相同
            Bitmap outBitmap = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888);
            //实例化一个新的Renderscript
            RenderScript rs = RenderScript.create(getApplicationContext());
            //创建Allocation对象
            Allocation allIn = Allocation.createFromBitmap(rs, bitmap);
            Allocation allOut = Allocation.createFromBitmap(rs, outBitmap);
            //创建ScriptIntrinsicBlur对象,该对象实现了高斯模糊算法
            ScriptIntrinsicBlur blurScript = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs));
    
            //设置模糊半径,0 <radius <= 25
            blurScript.setRadius(radius);
    
            //执行Renderscript
            blurScript.setInput(allIn);
            blurScript.forEach(allOut);
            //将allOut创建的Bitmap复制到outBitmap
            allOut.copyTo(outBitmap);
            //释放内存占用
            bitmap.recycle();
    
            //销毁Renderscript。
            rs.destroy();
            return outBitmap;
        }
    

Dialog的歌曲添加

使用自定义视图包含Listview实现歌曲显示,通过重写BaseAdapter实现歌曲的数据存储和选中状态,通过listview的监听实现长按进入多选状态和全选等,

  • 创建dialog:

    • 使用Util的辅助函数,获取所有歌曲信息,去除掉已经加入的歌曲,将其他歌曲显示在dialog的listview中,为listview实现点击和长按监听,进行选择确认和长按模式改变
  • 使用自定义Adapter

    • MyAdapter中,通过HashMap来存储选中状态,在getView中设定Item的数据信息,使用Glide进行图片和歌曲加载

          @Override
          public View getView(int position, View convertView, ViewGroup parent) {
              if (convertView == null){
                  holder = new ViewHolder();
                  convertView = inflater.inflate(R.layout.listitem,null);
                  holder.cb = (CheckBox) convertView.findViewById(R.id.X_checkbox);
                  holder.tv = (TextView) convertView.findViewById(R.id.X_item_text);
                  holder.iv = (ImageView) convertView.findViewById(R.id.list_item_image);
                  holder.tp = (TextView) convertView.findViewById(R.id.X_type);
                  convertView.setTag(holder);
              }else {
                  holder = (ViewHolder) convertView.getTag();
              }
              //set content
              Song model = dataSource.get(position);
              holder.tp.setText(model.name);
              holder.tv.setText(model.singer);
              holder.iv.setImageBitmap(LocalMusicUtils.getArtwork(context,dataSource.get(position).id,dataSource.get(position).albumId,true,false));
              showOrHideCheckBox();
              holder.cb.setChecked(getIsSelected().get(position));
              return convertView;
          }
      
  • 在选择完毕后进行数据加入,并更新viewpager和Service中的歌曲数据:

    • 数据加入通过RxJava进行异步处理,UI更新

          private void sendRxJavaSongAdd(List<Song> songs){
              Observable<Song> observable = Observable.fromIterable(songs);
              observable.subscribe(observer);
              updateService(songs);
          }
      

      observer中onnext 调用addpageviewpager函数,见上方之前叙述,onComplete调用myAdapter的notifydatachange,刷新viewpager

    • updateService中更新歌曲信息,通过广播传递数据

          private void updateService(List<Song> songs){
              Intent intent = new Intent(BroadType.MUSIC_ADD_SONGS);
              intent.putExtra(BroadType.MUSIC_ADD_SONGS,(Serializable) songs);
              LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
          }
      

旋转动画

一个ObjectAnimation动画,跟随view创建而创建,并在list中对应存储,在调用时更新restart状态,在play、pause中实现stop、resume

    /**     * 设置唱片旋转动画     * @param disc     * @return     */    private ObjectAnimator getDiscObjectAnimator(ImageView disc) {        ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(disc, View.ROTATION, 0, 360);        objectAnimator.setRepeatCount(ValueAnimator.INFINITE);        objectAnimator.setDuration(20 * 1000);        objectAnimator.setInterpolator(new LinearInterpolator());        return objectAnimator;    }

思路参考

  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值