高仿Android网易云音乐OkHttp+Retrofit+RxJava+Glide+MVC+MVVM

高仿Android网易云音乐OkHttp+Retrofit+RxJava+Glide+MVC+MVVM

简介

这是一个使用Java(以后还会推出Kotlin版本)语言,从0开发一个Android平台,接近企业级的项目(我的云音乐),包含了基础内容,高级内容,项目封装,项目重构等知识;主要是使用系统功能,流行的第三方框架,第三方服务,完成接近企业级商业级项目。

功能点

隐私协议对话框
启动界面和动态处理权限
引导界面和广告
轮播图和侧滑菜单
首页复杂列表和列表排序
音乐播放和音乐列表管理
全局音乐控制条
桌面歌词和自定义样式
全局媒体控制中心
评论和回复评论
评论富文本点击
评论提醒人和话题
朋友圈动态列表和发布
高德地图定位和路径规划
阿里云OSS上传
视频播放和控制
QQ/微信登录和分享
商城/购物车微信支付宝支付
文本和图片聊天
消息离线推送
自动和手动检查更新
内存泄漏和优化
...

开发环境概述

2022年5月开发完成的,所以全部都是最新的,平均每3年会重新制作,现在已经是第三版了。

JDK17 Android 12/13 最低兼容版本:Android 6.0 Android Studio 2021.1 

编译和运行

用最新AS打开MyCloudMusicAndroidJava目录,然后等待完全编译成功,因为是企业级项目,所以第三方依赖很多,同时代码量也很多,所以必须要确认完全编译成功,才能运行。

项目目录结构

├── MyCloudMusicAndroidJava │   ├── LRecyclerview //第三方Recyclerview框架 │   ├── LetterIndexView //类似微信通讯录字母索引 │   ├── app //云音乐项目 │   ├── build.gradle │   ├── common.gradle //通用项目配置文件 │   ├── config //配置目录,例如签名 │   ├── glidepalette //Glide画板,用来从网络图片提取颜色 │   ├── gradle │   ├── gradle.properties │   ├── gradlew │   ├── gradlew.bat │   ├── keystore.properties │   ├── local.properties │   ├── settings.gradle │   ├── super-j //公用Java语言扩展 │   ├── super-player-tencent //腾讯开源的超级播放器 │   ├── super-speech-baidu //百度语音识别 

依赖框架

内容太多,只列出部分。

//分页组件版本 //这里可以查看最新版本:https://developer.android.google.cn/jetpack/androidx/releases/paging def paging_version = "3.1.1"  //添加所有libs目录里面的jar,aar implementation fileTree(dir: 'libs', include: ['*.jar','*.aar'])  //官方兼容组件,像AppCompatActivity就是该依赖里面的 implementation 'androidx.appcompat:appcompat:1.4.1'  //Material Design组件,像FloatingActionButton就是该依赖里面的 implementation 'com.google.android.material:material:1.4.0'  //官方提供的约束布局,像ConstraintLayout就是该依赖里面的 implementation 'androidx.constraintlayout:constraintlayout:2.1.0'  //UI框架,主要是用他的工具类,也可以单独拷贝出来 //https://qmuiteam.com/android/get-started implementation 'com.qmuiteam:qmui:2.0.1'  //动态处理权限 //https://github.com/permissions-dispatcher/PermissionsDispatcher implementation "com.github.permissions-dispatcher:permissionsdispatcher:4.8.0" annotationProcessor "com.github.permissions-dispatcher:permissionsdispatcher-processor:4.8.0"  //api:依赖会传递到其他应用本模块的项目 implementation project(path: ':super-j') ...  //使用gson解析json //https://github.com/google/gson implementation 'com.google.code.gson:gson:2.9.0'  //自动释放RxJava相关资源 //https://github.com/uber/AutoDispose implementation "com.uber.autodispose2:autodispose-androidx-lifecycle:2.1.1"  //banner轮播图框架 //https://github.com/youth5201314/banner implementation 'io.github.youth5201314:banner:2.2.2'  //图片加载框架,还引用他目的是,coil有些功能不好实现 //https://github.com/bumptech/glide implementation 'com.github.bumptech.glide:glide:+' annotationProcessor 'com.github.bumptech.glide:compiler:+'  implementation 'androidx.recyclerview:recyclerview:1.2.1'  //给控件添加未读消息数红点 //https://github.com/bingoogolapple/BGABadgeView-Android implementation 'com.github.bingoogolapple.BGABadgeView-Android:api:1.2.0' annotationProcessor 'com.github.bingoogolapple.BGABadgeView-Android:compiler:1.2.0'  //webview进度条 //https://github.com/youlookwhat/WebProgress implementation 'com.github.youlookwhat:WebProgress:1.2.0'  //日志框架 //https://github.com/JakeWharton/timber implementation 'com.jakewharton.timber:timber:5.0.1'  implementation "androidx.media:media:+"  //和Glide配合处理图片 //可以实现很多效果 //模糊;圆角;圆 //我们这里是用它实现模糊效果 //https://github.com/wasabeef/glide-transformations implementation 'jp.wasabeef:glide-transformations:+'  //圆形图片控件 //https://github.com/hdodenhof/CircleImageView implementation 'de.hdodenhof:circleimageview:+'  //下载框架 //https://github.com/ixuea/android-downloader implementation 'com.ixuea:android-downloader:3.0.0'  //阿里云oss //官方文档:https://help.aliyun.com/document_detail/32043.html //sdk地址:https://github.com/aliyun/aliyun-oss-android-sdk implementation 'com.aliyun.dpa:oss-android-sdk:+'  //高德地图,这里引用的是3d //https://lbs.amap.com/api/android-sdk/guide/create-project/android-studio-create-project#gradle_sdk implementation 'com.amap.api:3dmap:+'  //定位功能 implementation 'com.amap.api:location:+'  //百度语音相关技术,目前主要用在收货地址编辑界面,语音输入收货地址 //https://ai.baidu.com/ai-doc/SPEECH/Pkgt4wwdx#%E9%9B%86%E6%88%90%E6%8C%87%E5%8D%97 implementation project(path: ':super-speech-baidu')  //TextView显示富文本,目前主要用在商品详情界面,显示富文本商品描述 //https://github.com/wangchenyan/html-text implementation 'com.github.wangchenyan:html-text:+'  //Hutool是一个小而全的Java工具类库 // 通过静态方法封装,降低相关API的学习成本 // 提高工作效率,使Java拥有函数式语言般的优雅 //https://github.com/looly/hutool implementation 'cn.hutool:hutool-all:5.7.14'  //支付宝支付 //https://opendocs.alipay.com/open/204/105296 implementation 'com.alipay.sdk:alipaysdk-android:+@aar'  //融云IM //https://docs.rongcloud.cn/v4/5X/views/im/ui/guide/quick/include/android.html implementation 'cn.rongcloud.sdk:im_lib:+'  //微信支付 //官方sdk下载文档:https://developers.weixin.qq.com/doc/oplatform/Downloads/Android_Resource.html //官方集成文档:https://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=8_5 implementation 'com.tencent.mm.opensdk:wechat-sdk-android:+'  //内存泄漏检测工具 //https://github.com/square/leakcanary //只有调试模式下才添加该依赖 debugImplementation 'com.squareup.leakcanary:leakcanary-android:+'  testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' 

用户协议对话框

高仿Android网易云音乐OkHttp+Retrofit+RxJava+Glide+MVC+MVVM

使用自定义DialogFragment实现,内容是放到字符串文件中的,其中的链接是HTML标签,设置后就可以点击了,然后修改默认对话框宽度,因为默认的有点窄。

public class TermServiceDialogFragment extends BaseViewModelDialogFragment<FragmentDialogTermServiceBinding> {      ...      @Override     protected void initViews() {         super.initViews();         //点击弹窗外边不能关闭         setCancelable(false);          SuperTextUtil.setLinkColor(binding.content, getActivity().getColor(R.color.link));     }      @Override     protected void initListeners() {         super.initListeners();         binding.primary.setOnClickListener(view -> {             dismiss();             onAgreementClickListener.onClick(view);         });          binding.disagree.setOnClickListener(view -> {             dismiss();             SuperProcessUtil.killApp();         });     }      @Override     public void onResume() {         super.onResume();         //修改宽度,默认比AlertDialog.Builder显示对话框宽度窄,看着不好看         //参考:https://stackoverflow.com/questions/12478520/how-to-set-dialogfragments-width-and-height         ViewGroup.LayoutParams params = getDialog().getWindow().getAttributes();          params.width = (int) (ScreenUtil.getScreenWith(getContext()) * 0.9);         params.height = ViewGroup.LayoutParams.WRAP_CONTENT;         getDialog().getWindow().setAttributes((android.view.WindowManager.LayoutParams) params);     } } 

动态权限

高仿Android网易云音乐OkHttp+Retrofit+RxJava+Glide+MVC+MVVM

高版本必须要动态处理权限,这里在启动界面请求了一些权限,但推荐在用到的时候才获取,写法差不多,这里使用第三方框架实现,当然也可以直接使用系统API实现。

/**  * 权限授权了就会调用该方法  * 请求相机权限目的是扫描二维码,拍照  */ @NeedsPermission({         Manifest.permission.CAMERA,         Manifest.permission.READ_EXTERNAL_STORAGE,         Manifest.permission.WRITE_EXTERNAL_STORAGE,         Manifest.permission.ACCESS_COARSE_LOCATION,         Manifest.permission.ACCESS_FINE_LOCATION }) void onPermissionGranted() {     //如果有权限就进入下一步     prepareNext(); }  /**  * 显示权限授权对话框  * 目的是提示用户  */ @OnShowRationale({         Manifest.permission.CAMERA,         Manifest.permission.READ_EXTERNAL_STORAGE,         Manifest.permission.WRITE_EXTERNAL_STORAGE,         Manifest.permission.ACCESS_COARSE_LOCATION,         Manifest.permission.ACCESS_FINE_LOCATION }) void showRequestPermission(PermissionRequest request) {     new AlertDialog.Builder(getHostActivity())             .setMessage(R.string.permission_hint)             .setPositiveButton(R.string.allow, (dialog, which) -> request.proceed())             .setNegativeButton(R.string.deny, (dialog, which) -> request.cancel()).show(); }  /**  * 拒绝了权限调用  */ @OnPermissionDenied({         Manifest.permission.CAMERA,         Manifest.permission.READ_EXTERNAL_STORAGE,         Manifest.permission.WRITE_EXTERNAL_STORAGE,         Manifest.permission.ACCESS_COARSE_LOCATION,         Manifest.permission.ACCESS_FINE_LOCATION }) void showDenied() {     //退出应用     finish(); }  /**  * 再次获取权限的提示  */ @OnNeverAskAgain({         Manifest.permission.CAMERA,         Manifest.permission.READ_EXTERNAL_STORAGE,         Manifest.permission.WRITE_EXTERNAL_STORAGE,         Manifest.permission.ACCESS_COARSE_LOCATION,         Manifest.permission.ACCESS_FINE_LOCATION }) void showNeverAsk() {     //继续请求权限     checkPermission(); }   /**  * 授权后回调  *  * @param requestCode  * @param permissions  * @param grantResults  */ @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {     super.onRequestPermissionsResult(requestCode, permissions, grantResults);     //将授权结果传递到框架     SplashActivityPermissionsDispatcher.onRequestPermissionsResult(this, requestCode, grantResults); } 

引导界面

高仿Android网易云音乐OkHttp+Retrofit+RxJava+Glide+MVC+MVVM
引导界面比较简单,就是多个图片可以左右滚动,整体使用ViewPager+Fragment实现,也可以使用ViewPager2,后面有讲解。

/**  * 引导界面适配器  */ public class GuideAdapter extends BaseFragmentStatePagerAdapter<Integer> {      /***      *  @param context 上下文      * @param fm Fragment管理器      */     public GuideAdapter(Context context, @NonNull FragmentManager fm) {         super(context, fm);     }      /**      * 返回当前位置Fragment      *      * @param position      * @return      */     @NonNull     @Override     public Fragment getItem(int position) {         return GuideFragment.newInstance(getData(position));     } } 
/**  * 引导界面Fragment  */ public class GuideFragment extends BaseViewModelFragment<FragmentGuideBinding> {     ...      @Override     protected void initDatum() {         super.initDatum();         int data = getArguments().getInt(Constant.ID);         binding.icon.setImageResource(data);     } } 

广告界面

高仿Android网易云音乐OkHttp+Retrofit+RxJava+Glide+MVC+MVVM高仿Android网易云音乐OkHttp+Retrofit+RxJava+Glide+MVC+MVVM

实现图片广告和视频广告,广告数据是在首页是缓存到本地,目的是在启动界面加载更快,因为真实项目中,大部分项目启动页面广告时间一共就5秒,如果太长了用户体验不好,如果是从网络请求,那么网络可能就耗时2秒左右,所以导致就美哟多少时间显示广告了。

下载广告

private void downloadAd(Ad data) {     if (SuperNetworkUtil.isWifiConnected(getHostActivity())) {         //wifi才下载         sp.setSplashAd(data);          //判断文件是否存在,如果存在就不下载         File targetFile = FileUtil.adFile(getHostActivity(), data.getIcon());         if (targetFile.exists()) {             return;         }          new Thread(                 new Runnable() {                     @Override                     public void run() {                          try {                             //FutureTarget会阻塞                             //所以需要在子线程调用                             FutureTarget<File> target = Glide.with(getHostActivity().getApplicationContext())                                     .asFile()                                     .load(ResourceUtil.resourceUri(data.getIcon()))                                     .submit();                              //获取下载的文件                             File file = target.get();                              //将文件拷贝到我们需要的位置                             FileUtils.moveFile(file, targetFile);                          } catch (Exception e) {                             e.printStackTrace();                         }                     }                 }         ).start();     } } 

显示广告

/**  * 显示视频广告  *  * @param data  */ private void showVideoAd(File data) {     SuperViewUtil.show(binding.video);     SuperViewUtil.show(binding.preload);      //在要用到的时候在初始化,更节省资源,当然播放器控件也可以在这里动态创建     //设置播放监听器      //创建 player 对象     player = new TXVodPlayer(getHostActivity());      //静音,当然也可以在界面上添加静音切换按钮     player.setMute(true);      //关键 player 对象与界面 view     player.setPlayerView(binding.video);      //设置播放监听器     player.setVodListener(this);      //铺满     binding.video.setRenderMode(TXLiveConstants.RENDER_MODE_FULL_FILL_SCREEN);      //开启硬件加速     player.enableHardwareDecode(true);      player.startPlay(data.getAbsolutePath()); } 

显示图片就是显示本地图片了,没什么难点,就不贴代码了。

首页/歌单详情/黑胶唱片界面

高仿Android网易云音乐OkHttp+Retrofit+RxJava+Glide+MVC+MVVM

首页没有顶部是轮播图,然后是可以左右的菜单,接下来是热门歌单,推荐单曲,最后是首页排序模块;整体上使用RecycerView实现,轮播图:

Banner bannerView = holder.getView(R.id.banner);  BannerImageAdapter<Ad> bannerImageAdapter = new BannerImageAdapter<Ad>(data.getData()) {      @Override     public void onBindView(BannerImageHolder holder, Ad data, int position, int size) {         ImageUtil.show(getContext(), (ImageView) holder.itemView, data.getIcon());     } };  bannerView.setAdapter(bannerImageAdapter);  bannerView.setOnBannerListener(onBannerListener);  bannerView.setBannerRound(DensityUtil.dip2px(getContext(), 10));  //添加生命周期观察者 bannerView.addBannerLifecycleObserver(fragment);  bannerView.setIndicator(new CircleIndicator(getContext())); 

推荐歌单

//设置标题,将标题放到每个具体的item上,好处是方便整体排序 holder.setText(R.id.title, R.string.recommend_sheet);  //显示更多容器 holder.setVisible(R.id.more, true); holder.getView(R.id.more).setOnClickListener(v -> {  });  RecyclerView listView = holder.getView(R.id.list); if (listView.getAdapter() == null) {     //设置显示3列     GridLayoutManager layoutManager = new GridLayoutManager(listView.getContext(), 3);     listView.setLayoutManager(layoutManager);      sheetAdapter = new SheetAdapter(R.layout.item_sheet);      //item点击     sheetAdapter.setOnItemClickListener(new OnItemClickListener() {         @Override         public void onItemClick(@NonNull BaseQuickAdapter<?, ?> adapter, @NonNull View view, int position) {             if (discoveryAdapterListener != null) {                 discoveryAdapterListener.onSheetClick((Sheet) adapter.getItem(position));             }         }     });     listView.setAdapter(sheetAdapter);      GridDividerItemDecoration itemDecoration = new GridDividerItemDecoration(getContext(), (int) DensityUtil.dip2px(getContext(), 5F));     listView.addItemDecoration(itemDecoration); }  sheetAdapter.setNewInstance(data.getData()); 

歌单详情

顶部是歌单信息,通过header实现,底部是列表,显示歌单内容的音乐,点击音乐进入黑胶唱片播放界面。

//添加头部 adapter.addHeaderView(createHeaderView()); 
/**  * 显示数据的方法  *  * @param holder  * @param data  */ @Override protected void convert(@NonNull BaseViewHolder holder, Song data) {     //显示位置     holder.setText(R.id.index, String.valueOf(holder.getLayoutPosition() + offset));      //显示标题     holder.setText(R.id.title, data.getTitle());      //显示信息     holder.setText(R.id.info, data.getSinger().getNickname());      if (offset != 0) {         holder.setImageResource(R.id.more, R.drawable.close);          holder.getView(R.id.more)                 .setOnClickListener(new View.OnClickListener() {                     @Override                     public void onClick(View v) {                         SuperDialog.newInstance(fragmentManager)                                 .setTitleRes(R.string.confirm_delete)                                 .setOnClickListener(new View.OnClickListener() {                                     @Override                                     public void onClick(View v) {                                         //查询下载任务                                         DownloadInfo downloadInfo = AppContext.getInstance().getDownloadManager().getDownloadById(data.getId());                                          if (downloadInfo != null) {                                             //从下载框架删除                                             AppContext.getInstance().getDownloadManager().remove(downloadInfo);                                         } else {                                             AppContext.getInstance().getOrm().deleteSong(data);                                         }                                          //从适配器中删除                                         removeAt(holder.getAdapterPosition());                                      }                                 }).show();                     }                 });     } else {         //是否下载         DownloadInfo downloadInfo = AppContext.getInstance().getDownloadManager().getDownloadById(data.getId());         if (downloadInfo != null && downloadInfo.getStatus() == DownloadInfo.STATUS_COMPLETED) {             //下载完成了              //显示下载完成了图标             holder.setGone(R.id.download, false);         } else {             holder.setGone(R.id.download, true);         }     }      //处理编辑状态     if (isEditing()) {         holder.setVisible(R.id.index, false);         holder.setVisible(R.id.check, true);         holder.setVisible(R.id.more, false);          if (isSelected(holder.getLayoutPosition())) {             holder.setImageResource(R.id.check, R.drawable.ic_checkbox_selected);         } else {             holder.setImageResource(R.id.check, R.drawable.ic_checkbox);         }     } else {         holder.setVisible(R.id.index, true);         holder.setVisible(R.id.check, false);         holder.setVisible(R.id.more, true);     }  } 

黑胶唱片

上面是黑胶唱片,和网易云音乐差不多,随着音乐滚动或暂停,顶部是控制相关,音乐播放逻辑是封装到MusicPlayerManager中:

/**  * 播放管理器默认实现  */ public class MusicPlayerManagerImpl implements MusicPlayerManager, MediaPlayer.OnCompletionListener, AudioManager.OnAudioFocusChangeListener {     ...          /**      * 获取播放管理器      * getInstance:方法名可以随便取      * 只是在Java这边大部分项目都取这个名字      *      * @return      */     public synchronized static MusicPlayerManager getInstance(Context context) {         if (instance == null) {             instance = new MusicPlayerManagerImpl(context);         }         return instance;     }      @Override     public void play(String uri, Song data) {         //保存信息         this.uri = uri;         this.data = data;          //释放播放器         player.reset();          //获取音频焦点         if (!requestAudioFocus()) {             return;         }          playNow();     }      private void playNow() {         isPrepare = true;          try {             if (uri.startsWith("content://")) {                 //内容提供者格式                  //本地音乐                 //uri示例:content://media/external/audio/media/23                 player.setDataSource(context, Uri.parse(uri));             } else {                 //设置数据源                 player.setDataSource(uri);             }              //同步准备             //真实项目中可能会使用异步             //因为如果网络不好             //同步可能会卡住             player.prepare(); //            player.prepareAsync();              //开始播放器             player.start();              //回调监听器             publishPlayingStatus();              //启动播放进度通知             startPublishProgress();              prepareLyric(data);         } catch (IOException e) {             //TODO 播放错误处理         }      }       @Override     public void pause() {         if (isPlaying()) {             //如果在播放就暂停             player.pause();              ListUtil.eachListener(listeners, musicPlayerListener -> musicPlayerListener.onPaused(data));              stopPublishProgress();         }     }      @Override     public void resume() {         if (!isPlaying()) {             //获取音频焦点             if (!requestAudioFocus()) {                 return;             }              resumeNow();         }     }      private void resumeNow() {         //如果没有播放就播放         player.start();          //回调监听器         publishPlayingStatus();          //启动进度通知         startPublishProgress();     }      @Override     public void addMusicPlayerListener(MusicPlayerListener listener) {         if (!listeners.contains(listener)) {             listeners.add(listener);         }          //启动进度通知         startPublishProgress();     }      @Override     public void removeMusicPlayerListener(MusicPlayerListener listener) {         listeners.remove(listener);     }      @Override     public void seekTo(int progress) {         player.seekTo(progress);     }      /**      * 发布播放中状态      */     private void publishPlayingStatus() { //        for (MusicPlayerListener listener : listeners) { //            listener.onPlaying(data); //        }          //使用重构后的方法         ListUtil.eachListener(listeners, musicPlayerListener -> musicPlayerListener.onPlaying(data));     }      /**      * 播放完毕了回调      *      * @param mp      */     @Override     public void onCompletion(MediaPlayer mp) {         isPrepare = false;          //回调监听器         ListUtil.eachListener(listeners, listener -> listener.onCompletion(mp));     }      @Override     public void setLooping(boolean looping) {         player.setLooping(looping);     }      /**      * 音频焦点改变了回调      *      * @param focusChange      */     @Override     public void onAudioFocusChange(int focusChange) {         Timber.d("onAudioFocusChange %s", focusChange);          switch (focusChange) {             case AudioManager.AUDIOFOCUS_GAIN:                 //获取到焦点了                 if (resumeOnFocusGain) {                     if (isPrepare) {                         resumeNow();                     } else {                         playNow();                     }                      resumeOnFocusGain = false;                 }                 break;             case AudioManager.AUDIOFOCUS_LOSS:                 //永久失去焦点,例如:其他应用请求时,也是播放音乐                 if (isPlaying()) {                     pause();                 }                 break;             case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:             case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:                 //暂时性失去焦点,例如:通话了,或者呼叫了语音助手等请求                 if (isPlaying()) {                     resumeOnFocusGain = true;                     pause();                 }                 break;         }     } } 

音乐列表逻辑封装到MusicListManager:

public class MusicListManagerImpl implements MusicListManager, MusicPlayerListener {      @Override     public void setDatum(List<Song> datum) {         //将原来数据playList标志设置为false         DataUtil.changePlayListFlag(this.datum, false);          //保存到数据库         saveAll();          //清空原来的数据         this.datum.clear();          //添加新的数据         this.datum.addAll(datum);          //更改播放列表标志         DataUtil.changePlayListFlag(this.datum, true);          //保存到数据库         saveAll();          sendPlayListChangedEvent(0);     }      /**      * 保存播放列表      */     private void saveAll() {         getOrm().saveAll(datum);     }      private LiteORMUtil getOrm() {         return LiteORMUtil.getInstance(this.context);     }      @Override     public void play(Song data) {         //当前音乐黑胶唱片滚动         data.setRotate(true);          //标记已经播放了         isPlay = true;          //保存数据         this.data = data;          if (StringUtils.isNotBlank(data.getPath())) {             //本地音乐             //不拼接地址             musicPlayerManager.play(data.getPath(), data);         } else {             //判断是否有下载对象             DownloadInfo downloadInfo = AppContext.getInstance().getDownloadManager().getDownloadById(data.getId());             if (downloadInfo != null && downloadInfo.getStatus() == DownloadInfo.STATUS_COMPLETED) {                 //下载完成了                  //播放本地音乐                 musicPlayerManager.play(downloadInfo.getPath(), data);                 Timber.d("play offline %s %s %s", data.getTitle(), downloadInfo.getPath(), data.getUri());             } else {                 //播放在线音乐                 String path = ResourceUtil.resourceUri(data.getUri());                  musicPlayerManager.play(path, data);                  Timber.d("play online %s %s", data.getTitle(), path);             }         }          //设置最后播放音乐的Id         sp.setLastPlaySongId(data.getId());     }      @Override     public void pause() {         musicPlayerManager.pause();     }      @Override     public Song next() {         if (datum.size() == 0) {             //如果没有音乐了             //直接返回null             return null;         }          //音乐索引         int index = 0;          //判断循环模式         switch (model) {             case MODEL_LOOP_RANDOM:                 //随机循环                  //在0~datum.size()中                 //不包含datum.size()                 index = new Random().nextInt(datum.size());                 break;             default:                 //找到当前音乐索引                 index = datum.indexOf(data);                  if (index != -1) {                     //找到了                      //如果当前播放是列表最后一个                     if (index == datum.size() - 1) {                         //最后一首音乐                          //那就从0开始播放                         index = 0;                     } else {                         index++;                     }                 } else {                     //抛出异常                     //因为正常情况下是能找到的                     throw new IllegalArgumentException("Cant'found current song");                 }                 break;         }          return datum.get(index);     }      @Override     public void delete(int position) {         //获取要删除的音乐         Song song = datum.get(position);          if (song.getId().equals(data.getId())) {             //删除的音乐就是当前播放的音乐              //应该停止当前播放             pause();              //并播放下一首音乐             Song next = next();              if (next.getId().equals(data.getId())) {                 //找到了自己                 //没有歌曲可以播放了                 data = null;                 //TODO Bug 随机循环的情况下有可能获取到自己             } else {                 play(next);             }         }          //直接删除         datum.remove(song);          //从数据库中删除         getOrm().deleteSong(song);          sendPlayListChangedEvent(position);     }      private void sendPlayListChangedEvent(int position) {         EventBus.getDefault().post(new MusicPlayListChangedEvent(position));     }      /**      * 播放完毕了回调      *      * @param mp      */     @Override     public void onCompletion(MediaPlayer mp) {         if (model == MODEL_LOOP_ONE) {             //如果是单曲循环             //就不会处理了             //因为我们使用了MediaPlayer的循环模式              //如果使用的第三方框架             //如果没有循环模式             //那就要在这里继续播放当前音乐         } else {             Song data = next();             if (data != null) {                 play(data);             }         }     }     ... } 

外界统一使用播放列表管理器播放音乐,上一曲下一曲:

//播放按钮点击 binding.play.setOnClickListener(v -> {     playOrPause(); });  //下一曲按钮点击 binding.next.setOnClickListener(v -> {     getMusicListManager().play(getMusicListManager().next()); });  //播放列表按钮点击 binding.listButton.setOnClickListener(v -> {     MusicPlayListDialogFragment.show(getSupportFragmentManager()); }); 

媒体控制器/桌面歌词/桌面Widget

高仿Android网易云音乐OkHttp+Retrofit+RxJava+Glide+MVC+MVVM
歌词实现了LRC,KSC两种歌词,封装到LyricListView,单个歌词行封装到LyricView中,外界直接使用LyricListView就行:

private void showLyricData() {     binding.lyricList.setData(getMusicListManager().getData().getParsedLyric()); } 

桌面歌词使用两个LyricView显示两行歌词,桌面歌词使用的是全局悬浮窗API,所以要先判断是否有权限,没有需要先获取权限,然后才能显示,封装到GlobalLyricManagerImpl中:

/**  * 全局(桌面)歌词管理器实现  */ public class GlobalLyricManagerImpl implements GlobalLyricManager, MusicPlayerListener, GlobalLyricView.OnGlobalLyricDragListener, GlobalLyricView.GlobalLyricListener {     public GlobalLyricManagerImpl(Context context) {         this.context = context.getApplicationContext();          //初始化偏好设置工具类         sp = PreferenceUtil.getInstance(this.context);          //初始化音乐播放管理器         musicPlayerManager = MusicPlayerService.getMusicPlayerManager(this.context);          //添加播放监听器         musicPlayerManager.addMusicPlayerListener(this);          //初始化窗口管理器         initWindowManager();          //从偏好设置中获取是否要显示全局歌词         if (sp.isShowGlobalLyric()) {             //创建全局歌词View             initGlobalLyricView();              //如果原来锁定了歌词             if (sp.isGlobalLyricLock()) {                 //锁定歌词                 lock();             }         }     }      public synchronized static GlobalLyricManagerImpl getInstance(Context context) {         if (instance == null) {             instance = new GlobalLyricManagerImpl(context);         }         return instance;     }      /**      * 锁定全局歌词      */     private void lock() {         //保存全局歌词锁定状态         sp.setGlobalLyricLock(true);          //设置全局歌词控件状态         setGlobalLyricStatus();          //显示简单模式         globalLyricView.simpleStyle();          //更新布局         updateView();          //显示解锁全局歌词通知         NotificationUtil.showUnlockGlobalLyricNotification(context);          //注册接收解锁全局歌词广告接收器         registerUnlockGlobalLyricReceiver();     }      /**      * 注册接收解锁全局歌词广告接收器      */     private void registerUnlockGlobalLyricReceiver() {         if (unlockGlobalLyricBroadcastReceiver == null) {             //创建广播接受者             unlockGlobalLyricBroadcastReceiver = new BroadcastReceiver() {                  @Override                 public void onReceive(Context context, Intent intent) {                     if (Constant.ACTION_UNLOCK_LYRIC.equals(intent.getAction())) {                         //歌词解锁事件                         unlock();                     }                 }             };              IntentFilter intentFilter = new IntentFilter();              //只监听歌词解锁事件             intentFilter.addAction(Constant.ACTION_UNLOCK_LYRIC);              //注册             context.registerReceiver(unlockGlobalLyricBroadcastReceiver, intentFilter);         }     }      /**      * 解锁歌词      */     private void unlock() {         //设置没有锁定歌词         sp.setGlobalLyricLock(false);          //设置歌词状态         setGlobalLyricStatus();          //解锁后显示标准样式         globalLyricView.normalStyle();          //更新view         updateView();          //清除歌词解锁通知         NotificationUtil.clearUnlockGlobalLyricNotification(context);          //解除接收全局歌词事件广播接受者         unregisterUnlockGlobalLyricReceiver();     }      /**      * 解除接收全局歌词事件广播接受者      */     private void unregisterUnlockGlobalLyricReceiver() {         if (unlockGlobalLyricBroadcastReceiver != null) {             context.unregisterReceiver(unlockGlobalLyricBroadcastReceiver);             unlockGlobalLyricBroadcastReceiver = null;         }     }      @Override     public void show() {         //检查全局悬浮窗权限         if (!Settings.canDrawOverlays(context)) {             Intent intent = new Intent(context, SplashActivity.class);             intent.setAction(Constant.ACTION_LYRIC);             intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);             context.startActivity(intent);             return;         }          //初始化全局歌词控件         initGlobalLyricView();          //设置显示了全局歌词         sp.setShowGlobalLyric(true);          WidgetUtil.onGlobalLyricShowStatusChanged(context, isShowing());     }      private boolean hasGlobalLyricView() {         return globalLyricView != null;     }      /**      * 全局歌词拖拽回调      *      * @param y y轴方向上移动的距离      */     @Override     public void onGlobalLyricDrag(int y) {         layoutParams.y = y - SizeUtil.getStatusBarHeight(context);          //更新view         updateView();          //保存歌词y坐标         sp.setGlobalLyricViewY(layoutParams.y);     }           ... } 

显示和隐藏只需要调用该管理器的相关方法就行了。

媒体控制器

使用了可以通过系统媒体控制器,通知栏,锁屏界面,耳机,蓝牙耳机等设备控制媒体播放暂停,只需要把媒体信息更新到系统:

MusicPlayerService

/**  * 更新媒体信息  *  * @param data  * @param icon  */ public void updateMetaData(Song data, Bitmap icon) {     MediaMetadataCompat.Builder metaData = new MediaMetadataCompat.Builder()             //标题             .putString(MediaMetadataCompat.METADATA_KEY_TITLE, data.getTitle())              //艺术家,也就是歌手             .putString(MediaMetadataCompat.METADATA_KEY_ARTIST, data.getSinger().getNickname())              //专辑             .putString(MediaMetadataCompat.METADATA_KEY_ALBUM, "专辑")              //专辑艺术家             .putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ARTIST, "专辑艺术家")              //时长             .putLong(MediaMetadataCompat.METADATA_KEY_DURATION, data.getDuration())              //封面             .putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, icon);      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {         //播放列表长度         metaData.putLong(MediaMetadataCompat.METADATA_KEY_NUM_TRACKS, musicListManager.getDatum().size());     }      mediaSession.setMetadata(metaData.build()); } 

接收媒体控制

/**  * 媒体回调  */ private MediaSessionCompat.Callback callback = new MediaSessionCompat.Callback() {     @Override     public void onPlay() {         musicListManager.resume();     }      @Override     public void onPause() {         musicListManager.pause();     }      @Override     public void onSkipToNext() {         musicListManager.play(musicListManager.next());     }      @Override     public void onSkipToPrevious() {         musicListManager.play(musicListManager.previous());     }      @Override     public void onSeekTo(long pos) {         musicListManager.seekTo((int) pos);     } }; 

桌面Widget

创建布局,然后注册,最后就是更新信息:

public class MusicWidget extends AppWidgetProvider {     /**      * 添加,重新运行应用,周期时间,都会调用      *      * @param context      * @param appWidgetManager      * @param appWidgetIds      */     @Override     public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {         super.onUpdate(context, appWidgetManager, appWidgetIds);          //尝试启动service         ServiceUtil.startService(context.getApplicationContext(), MusicPlayerService.class);          //获取播放列表管理器         MusicListManager musicListManager = MusicPlayerService.getListManager(context.getApplicationContext());          //获取当前播放的音乐         final Song data = musicListManager.getData();          final int N = appWidgetIds.length;         // 循环处理每一个,因为桌面上可能添加多个         for (int i = 0; i < N; i++) {             int appWidgetId = appWidgetIds[i];              // 创建远程控件,所有对view的操作都必须通过该view提供的方法             RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.music_widget);              //因为这是在桌面的控件里面显示我们的控件,所以不能直接通过setOnClickListener设置监听器             //这里发送的动作在MusicReceiver处理             PendingIntent iconPendingIntent = IntentUtil.createMainActivityPendingIntent(context, Constant.ACTION_MUSIC_PLAYER_PAGE);              //这里直接启动service,也可以用广播接收             PendingIntent previousPendingIntent = IntentUtil.createMusicPlayerServicePendingIntent(context, Constant.ACTION_PREVIOUS);             PendingIntent playPendingIntent = IntentUtil.createMusicPlayerServicePendingIntent(context, Constant.ACTION_PLAY);             PendingIntent nextPendingIntent = IntentUtil.createMusicPlayerServicePendingIntent(context, Constant.ACTION_NEXT);             PendingIntent lyricPendingIntent = IntentUtil.createMusicPlayerServicePendingIntent(context, Constant.ACTION_LYRIC);              //设置点击事件             views.setOnClickPendingIntent(R.id.icon, iconPendingIntent);             views.setOnClickPendingIntent(R.id.previous, previousPendingIntent);             views.setOnClickPendingIntent(R.id.play, playPendingIntent);             views.setOnClickPendingIntent(R.id.next, nextPendingIntent);             views.setOnClickPendingIntent(R.id.lyric, lyricPendingIntent);              if (data == null) {                 //当前没有播放音乐                 appWidgetManager.updateAppWidget(appWidgetId, views);             } else {                 //有播放音乐                 views.setTextViewText(R.id.title, String.format("%s - %s", data.getTitle(), data.getSinger().getNickname()));                 views.setProgressBar(R.id.progress, (int) data.getDuration(), (int) data.getProgress(), false);                  //显示图标                 RequestOptions options = new RequestOptions();                 options.centerCrop();                 Glide.with(context)                         .asBitmap()                         .load(ResourceUtil.resourceUri(data.getIcon()))                         .apply(options)                         .into(new CustomTarget<Bitmap>() {                              @Override                             public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) {                                 //显示封面                                 views.setImageViewBitmap(R.id.icon, resource);                                 appWidgetManager.updateAppWidget(appWidgetId, views);                             }                              @Override                             public void onLoadCleared(@Nullable Drawable placeholder) {                                 //显示默认图片                                 views.setImageViewBitmap(R.id.icon, BitmapFactory.decodeResource(context.getResources(), R.drawable.placeholder));                                 appWidgetManager.updateAppWidget(appWidgetId, views);                             }                         });             }         }     } } 

登录/注册/验证码登录

高仿Android网易云音乐OkHttp+Retrofit+RxJava+Glide+MVC+MVVM

登录注册没有多大难度,用户名和密码登录,就是把信息传递到服务端,可以加密后在传输,服务端判断登录成功,返回一个标记,客户端保存,其他需要的登录的接口带上;验证码登录就是用验证码代替密码,发送验证码都是服务端发送,客户端只需要调用接口。

评论

高仿Android网易云音乐OkHttp+Retrofit+RxJava+Glide+MVC+MVVM
评论列表包括下拉刷新,上拉加载更多,点赞,发布评论,回复评论,Emoji,话题和提醒人点击,选择好友,选择话题等。

下拉刷新和下拉加载更多

核心逻辑就只需要更改page就行了

//下拉刷新监听器 binding.refresh.setOnRefreshListener(new OnRefreshListener() {     @Override     public void onRefresh(RefreshLayout refreshlayout) {         loadData();     } });  //上拉加载更多 binding.refresh.setOnLoadMoreListener(new OnLoadMoreListener() {     @Override     public void onLoadMore(RefreshLayout refreshlayout) {         loadMore();     } });  @Override protected void loadData(boolean isPlaceholder) {     super.loadData(isPlaceholder);     isRefresh = true;     pageMeta = null;      loadMore(); } 

提醒人和话题点击

通过正则表达式,找到特殊文本,然后使用富文本实现点击。

holder.setText(R.id.content, processContent(data.getContent()));  /**  * 处理文本点击事件  * 这部分可以用监听器回调到Activity中处理  *  * @param content  * @return  */ private SpannableString processContent(String content) {     //设置点击事件     SpannableString result = RichUtil.processContent(getContext(), content,             new RichUtil.OnTagClickListener() {                 @Override                 public void onTagClick(String data, RichUtil.MatchResult matchResult) {                     String clickText = RichUtil.removePlaceholderString(data);                     Timber.d("processContent mention click %s", clickText);                     UserDetailActivity.startWithNickname(getContext(), clickText);                 }             },             (data, matchResult) -> {                 String clickText = RichUtil.removePlaceholderString(data);                 Timber.d("processContent hash tag %s", clickText);             });      //返回结果     return result; } 

选择好友

对数据分组,然后显示右侧索引,选择了通过EventBus发送到评论界面。

adapter.setOnItemClickListener(new OnItemClickListener() {         @Override         public void onItemClick(@NonNull BaseQuickAdapter<?, ?> adapter, @NonNull View view, int position) {             Object data = adapter.getItem(position);             if (data instanceof User) {                 if (Constant.STYLE_FRIEND_SELECT == style) {                     EventBus.getDefault().post(new SelectedFriendEvent((User) data));                      //关闭界面                     finish();                 } else {                     startActivityExtraId(UserDetailActivity.class, ((User) data).getId());                 }             }         }     }); } 

视频和播放

高仿Android网易云音乐OkHttp+Retrofit+RxJava+Glide+MVC+MVVM

真实项目中视频播放大部分都是用第三方服务,例如:阿里云视频服务,腾讯视频服务,因为他们提供一条龙服务,包括审核,转码,CDN,安全,播放器等,这里用不到这么多功能,所以使用了第三方播放器播放普通mp4,这使用饺子播放器框架。

GSYVideoOptionBuilder videoOption = new GSYVideoOptionBuilder(); videoOption //                .setThumbImageView(imageView)         //小屏时不触摸滑动         .setIsTouchWiget(false)         //音频焦点冲突时是否释放         .setReleaseWhenLossAudio(true)         .setRotateViewAuto(false)         .setLockLand(false)         .setAutoFullWithSize(true)         .setSeekOnStart(seek)         .setNeedLockFull(true)         .setUrl(ResourceUtil.resourceUri(data.getUri()))         .setCacheWithPlay(false)          //全屏切换时不使用动画         .setShowFullAnimation(false)         .setVideoTitle(data.getTitle())          //设置右下角 显示切换到全屏 的按键资源         .setEnlargeImageRes(R.drawable.full_screen)          //设置右下角 显示退出全屏 的按键资源         .setShrinkImageRes(R.drawable.normal_screen)         .setVideoAllCallBack(new GSYSampleCallBack() {             @Override             public void onPrepared(String url, Object... objects) {                 super.onPrepared(url, objects);                 //开始播放了才能旋转和全屏                 orientationUtils.setEnable(true);                 isPlay = true;             }              @Override             public void onQuitFullscreen(String url, Object... objects) {                 super.onQuitFullscreen(url, objects);                 if (orientationUtils != null) {                     orientationUtils.backToProtVideo();                 }             }         }).setLockClickListener(new LockClickListener() {     @Override     public void onClick(View view, boolean lock) {         if (orientationUtils != null) {             //配合下方的onConfigurationChanged             orientationUtils.setEnable(!lock);         }     } }).build(binding.player);  //开始播放 binding.player.startPlayLogic(); 

用户详情/更改资料

高仿Android网易云音乐OkHttp+Retrofit+RxJava+Glide+MVC+MVVM

用户详情顶部显示用户信息,好友数量,下面分别显示创建的歌单,收藏的歌单,发布的动态,类似微信朋友圈,右上角可以更改用户资料;整体采用CoordinatorLayout+TabLayout+ViewPager+Fragment实现。

public Fragment getItem(int position) {     switch (position) {         case 0:             return UserDetailSheetFragment.newInstance(userId);         case 1:             return FeedFragment.newInstance(userId);         default:             return UserDetailAboutFragment.newInstance(userId);     } }  /**  * 返回标题  *  * @param position  * @return  */ @Nullable @Override public CharSequence getPageTitle(int position) {     //获取字符串id     int resourceId = titleIds[position];      //获取字符串     return context.getResources().getString(resourceId); } 

发布动态/选择位置/路径规划

高仿Android网易云音乐OkHttp+Retrofit+RxJava+Glide+MVC+MVVM
发布效果和微信朋友圈类似,可以选择图片,和地理位置;地理位置使用高德地图实现选择,路径规划是调用系统中安装的地图,类似微信。

选择位置

/**  * 搜索该位置的poi,方便用户选择,也方便其他人找  * Point Of Interest,兴趣点)  */ private void searchPOI(LatLng data, String keyword) {     try {         Timber.d("searchPOI %s %s", data, keyword);         binding.progress.setVisibility(View.VISIBLE);         adapter.setNewInstance(new ArrayList<>());          // 第一个参数表示一个Latlng,第二参数表示范围多少米,第三个参数表示是火系坐标系还是GPS原生坐标系 //        val query = RegeocodeQuery( //            LatLonPoint(data.latitude, data.longitude) //            , 1000F, GeocodeSearch.AMAP //        ) // //        geocoderSearch.getFromLocationAsyn(query)          //keyWord表示搜索字符串,         //第二个参数表示POI搜索类型,二者选填其一,选用POI搜索类型时建议填写类型代码,码表可以参考下方(而非文字)         //cityCode表示POI搜索区域,可以是城市编码也可以是城市名称,也可以传空字符串,空字符串代表全国在全国范围内进行搜索         PoiSearch.Query query = new PoiSearch.Query(keyword, "");          query.setPageSize(10); // 设置每页最多返回多少条poiitem          query.setPageNum(0); //设置查询页码          PoiSearch poiSearch = new PoiSearch(this, query);         poiSearch.setOnPoiSearchListener(this);          //设置周边搜索的中心点以及半径         if (data != null) {             poiSearch.setBound(new PoiSearch.SearchBound(                     new LatLonPoint(                             data.latitude,                             data.longitude                     ), 1000             ));         }          poiSearch.searchPOIAsyn();     } catch (Exception e) {         e.printStackTrace();     } } 

高德地图路径规划

/**  * 使用高德地图路径规划  *  * @param context  * @param slat    起点纬度  * @param slon    起点经度  * @param sname   起点名称 可不填(0,0,null)  * @param dlat    终点纬度  * @param dlon    终点经度  * @param dname   终点名称 必填  *                官方文档:https://lbs.amap.com/api/amap-mobile/guide/android/route  */ public static void openAmapRoute(         Context context,         double slat,         double slon,         String sname,         double dlat,         double dlon,         String dname ) {     StringBuilder builder = new StringBuilder("amapuri://route/plan?");     //第三方调用应用名称     builder.append("sourceApplication=");     builder.append(context.getString(R.string.app_name));      //开始信息     if (slat != 0.0) {         builder.append("&sname=").append(sname);         builder.append("&slat=").append(slat);         builder.append("&slon=").append(slon);     }      //结束信息     builder.append("&dlat=").append(dlat)             .append("&dlon=").append(dlon)             .append("&dname=").append(dname)             .append("&dev=0")             .append("&t=0");      startActivity(context, Constant.PACKAGE_MAP_AMAP, builder.toString()); } 

聊天/离线推送

高仿Android网易云音乐OkHttp+Retrofit+RxJava+Glide+MVC+MVVM
大部分真实项目中聊天都会选择第三方商业级付费聊天服务,常用的有腾讯云聊天,融云聊天,网易云聊天等,这里选择融云聊天服务,使用步骤是先在服务端生成聊天Token,这里是登录后返回,然后客户端登录聊天服务器,然后设置消息监听,发送消息等。

登录聊天服务器

/**  * 连接聊天服务器  *  * @param data  */ private void connectChat(Session data) {     RongIMClient.connect(data.getChatToken(), new RongIMClient.ConnectCallback() {         /**          * 成功回调          * @param userId 当前用户 ID          */         @Override         public void onSuccess(String userId) {             Timber.d("connect chat success %s", userId);         }          /**          * 错误回调          * @param errorCode 错误码          */         @Override         public void onError(RongIMClient.ConnectionErrorCode errorCode) {             Timber.e("connect chat error %s", errorCode);              if (errorCode.equals(RongIMClient.ConnectionErrorCode.RC_CONN_TOKEN_INCORRECT)) {                 //从 APP 服务获取新 token,并重连             } else {                 //无法连接 IM 服务器,请根据相应的错误码作出对应处理             }              //因为我们这个应用,不是类似微信那样纯聊天应用,所以聊天服务器连接失败,也让进入应用             //真实项目中按照需求实现就行了             SuperToast.show(R.string.error_message_login);         }          /**          * 数据库回调.          * @param databaseOpenStatus 数据库打开状态. DATABASE_OPEN_SUCCESS 数据库打开成功; DATABASE_OPEN_ERROR 数据库打开失败          */         @Override         public void onDatabaseOpened(RongIMClient.DatabaseOpenStatus databaseOpenStatus) {          }     });  } 

设置消息监听

chatClient.addOnReceiveMessageListener(new OnReceiveMessageWrapperListener() {     @Override     public void onReceivedMessage(Message message, ReceivedProfile profile) {         //该方法的调用不再主线程         Timber.e("chat onReceived %s", message);          if (EventBus.getDefault().hasSubscriberForEvent(NewMessageEvent.class)) {             //如果有监听该事件,表示在聊天界面,或者会话界面             EventBus.getDefault().post(new NewMessageEvent(message));         } else {             handler.obtainMessage(0, message).sendToTarget();         }          //发送消息未读数改变了通知         EventBus.getDefault().post(new MessageUnreadCountChangedEvent());     } }); 

发送文本消息

发送图片等其他消息也是差不多。

private void sendTextMessage() {     String content = binding.input.getText().toString().trim();     if (StringUtils.isEmpty(content)) {         SuperToast.show(R.string.hint_enter_message);         return;     }      TextMessage textMessage = TextMessage.obtain(content);     RongIMClient.getInstance().sendMessage(Conversation.ConversationType.PRIVATE, targetId, textMessage, null, MessageUtil.createPushData(MessageUtil.getContent(textMessage), sp.getUserId()), new IRongCallback.ISendMessageCallback() {         @Override         public void onAttached(Message message) {             // 消息成功存到本地数据库的回调             Timber.d("sendTextMessage onAttached %s", message);         }          @Override         public void onSuccess(Message message) {             // 消息发送成功的回调             Timber.d("sendTextMessage success %s", message);              //清空输入框             clearInput();              addMessage(message);         }          @Override         public void onError(Message message, RongIMClient.ErrorCode errorCode) {             // 消息发送失败的回调             Timber.e("sendTextMessage onError %s %s", message, errorCode);         }     });  } 

离线推送

先开启SDK离线推送,还要分别去厂商那边申请推送配置,这里只实现了小米推送,其他的华为推送,OPPO推送等差不多;然后把推送,或者点击都统一代理到主界面,然后再处理。

private void postRun(Intent intent) {     String action = intent.getAction();     if (Constant.ACTION_CHAT.equals(action)) {         //本地显示的消息通知点击          //要跳转到聊天界面         String id = intent.getStringExtra(Constant.ID);         startActivityExtraId(ChatActivity.class, id);     } else if (Constant.ACTION_PUSH.equals(action)) {         //聊天通知点击         String id = intent.getStringExtra(Constant.PUSH);         startActivityExtraId(ChatActivity.class, id);     } } 

商城/订单/支付/购物车

高仿Android网易云音乐OkHttp+Retrofit+RxJava+Glide+MVC+MVVM
高仿Android网易云音乐OkHttp+Retrofit+RxJava+Glide+MVC+MVVM

学到这里,大家不能说熟悉,那么看到上面的界面,那么大体要能实现出来。

商品详情富文本

//详情 HtmlText.from(data.getDetail())     .setImageLoader(new HtmlImageLoader() {         @Override         public void loadImage(String url, final Callback callback) {             Glide.with(getHostActivity())                     .asBitmap()                     .load(url)                     .into(new CustomTarget<Bitmap>() {                          @Override                         public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) {                             callback.onLoadComplete(resource);                         }                          @Override                         public void onLoadCleared(@Nullable Drawable placeholder) {                             callback.onLoadFailed();                         }                     });         }          @Override         public Drawable getDefaultDrawable() {             return ContextCompat.getDrawable(getHostActivity(), R.drawable.placeholder);         }          @Override         public Drawable getErrorDrawable() {             return ContextCompat.getDrawable(getHostActivity(), R.drawable.placeholder_error);         }          @Override         public int getMaxWidth() {             return ScreenUtil.getScreenWith(getHostActivity());         }          @Override         public boolean fitWidth() {             return true;         }     })     .setOnTagClickListener(new OnTagClickListener() {         @Override         public void onImageClick(Context context, List<String> imageUrlList, int position) {             // image click         }          @Override         public void onLinkClick(Context context, String url) {             // link click             Timber.d("onLinkClick %s", url);         }     })     .into(binding.detail); 

支付

客户端先集成微信,支付宝SDK,然后请求服务端获取支付信息,设置到SDK,最后就是处理支付结果。

/**  * 处理支付宝支付  *  * @param data  */ private void processAlipay(String data) {     PayUtil.alipay(getHostActivity(), data); }  /**  * 处理微信支付  *  * @param data  */ private void processWechat(WechatPay data) {     //把服务端返回的参数     //设置到对应的字段     PayReq request = new PayReq();      request.appId = data.getAppid();     request.partnerId = data.getPartnerid();     request.prepayId = data.getPrepayid();     request.nonceStr = data.getNoncestr();     request.timeStamp = data.getTimestamp();     request.packageValue = data.getPackageValue();     request.sign = data.getSign();      AppContext.getInstance().getWxapi().sendReq(request); } 

处理支付结果

/**  * 支付宝支付状态改变了  *  * @param event  */ @Subscribe(threadMode = ThreadMode.MAIN) public void onAlipayStatusChanged(AlipayStatusChangedEvent event) {     String resultStatus = event.getData().getResultStatus();      if ("9000".equals(resultStatus)) {         //本地支付成功          //不能依赖本地支付结果         //一定要以服务端为准         showLoading(R.string.hint_pay_wait);          //延时3秒         //因为支付宝回调我们服务端可能有延迟         binding.primary.postDelayed(() -> {             checkPayStatus();         }, 3000);      } else if ("6001".equals(resultStatus)) {         //支付取消         SuperToast.show(R.string.error_pay_cancel);     } else {         //支付失败         SuperToast.show(R.string.error_pay_failed);     } } 

语音识别输入地址

这里使用百度语音识别SDK,先集成,然后初始化,最后是监听识别结果:

/**  * 百度语音识别事件监听器  * <p>  * https://ai.baidu.com/ai-doc/SPEECH/4khq3iy52  */ EventListener voiceRecognitionEventListener = new EventListener() {     /**      * 事件回调      * @param name 回调事件名称      * @param params 回调参数      * @param data 数据      * @param offset 开始位置      * @param length 长度      */     @Override     public void onEvent(String name, String params, byte[] data, int offset, int length) {         String result = "name: " + name;          if (name.equals(SpeechConstant.CALLBACK_EVENT_ASR_READY)) {             // 引擎就绪,可以说话,一般在收到此事件后通过UI通知用户可以说话了             setStopVoiceRecognition();         } else if (name.equals(SpeechConstant.CALLBACK_EVENT_ASR_PARTIAL)) {             // 一句话的临时结果,最终结果及语义结果              if (params == null || params.isEmpty()) {                 return;             }              // 识别相关的结果都在这里             try {                 JSONObject paramObject = new JSONObject(params);                  //获取第一个结果                 JSONArray resultsRecognition = paramObject.getJSONArray("results_recognition");                  String voiceRecognitionResult = resultsRecognition.getString(0);                  //可以根据result_type是临时结果,还是最终结果                  binding.input.setText(voiceRecognitionResult);                 result += voiceRecognitionResult;             } catch (JSONException e) {                 e.printStackTrace();             }         } else if (name.equals(SpeechConstant.CALLBACK_EVENT_ASR_FINISH)) {             //一句话识别结束(可能含有错误信息) 。最终识别的文字结果在ASR_PARTIAL事件中              if (params.contains(""error":0")) {              } else if (params.contains(""error":7")) {                 SuperToast.show(R.string.voice_error_no_result);             } else {                 //其他错误                 SuperToast.show(getString(R.string.voice_error, params));             }         } else if (name.equals(SpeechConstant.CALLBACK_EVENT_ASR_EXIT)) {             //识别结束,资源释放             setStartVoiceRecognition();         }          Timber.d("baidu voice recognition onEvent %s", result);     } }; 

百度OCR

使用百度OCR从图片中识别文本,主要是识别地址,类似顺丰公众号输入地址时识别功能。

private void recognitionImage(String data) {     GeneralBasicParams param = new GeneralBasicParams();     param.setDetectDirection(true);     param.setImageFile(new File(data));      // 调用通用文字识别服务     OCR.getInstance(getApplicationContext()).recognizeGeneralBasic(param, new OnResultListener<GeneralResult>() {          /**          * 成功          * @param result          */         @Override         public void onResult(GeneralResult result) {             StringBuilder builder = new StringBuilder();             for (WordSimple it : result.getWordList()) {                 builder.append(it.getWords());                  //每一项之间,添加空格,方便OCR失败                 builder.append(" ");             }              binding.input.setText(builder.toString());         }          /**          * 失败          * @param error          */         @Override         public void onError(OCRError error) {             SuperToast.show(getString(R.string.ocr_error, error.getMessage(), error.getErrorCode()));         }     }); } 

还有一些功能,例如:快捷方式等就不在贴代码了。

发表评论

评论已关闭。

相关文章

当前内容话题