0.系列文章目录

1.启动界面
2.广告和引导界面

1.项目简介

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

2.项目功能点

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

3.开发环境概述

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

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

4.编译和运行

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

5.项目目录结构

├── 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 //百度语音识别

6.依赖框架

内容太多,只列出部分。

//分页组件版本
//这里可以查看最新版本: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'

7.用户协议对话框

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

public class TermServiceDialogFragment extends BaseViewModelDialogFragment<FragmentDialogTermServiceBinding> {...@Overrideprotected void initViews() {super.initViews();//点击弹窗外边不能关闭setCancelable(false);SuperTextUtil.setLinkColor(binding.content, getActivity().getColor(R.color.link));}@Overrideprotected void initListeners() {super.initListeners();binding.primary.setOnClickListener(view -> {dismiss();onAgreementClickListener.onClick(view);});binding.disagree.setOnClickListener(view -> {dismiss();SuperProcessUtil.killApp();});}@Overridepublic void onResume() {super.onResume();//修改宽度,默认比AlertDialog.Builder显示对话框宽度窄,看着不好看//参考:https://stackoverflow.com/questions/12478520/how-to-set-dialogfragments-width-and-heightViewGroup.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);}
}

8.动态权限

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

9.引导界面


引导界面比较简单,就是多个图片可以左右滚动,整体使用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@Overridepublic Fragment getItem(int position) {return GuideFragment.newInstance(getData(position));}
}
/*** 引导界面Fragment*/
public class GuideFragment extends BaseViewModelFragment<FragmentGuideBinding> {...@Overrideprotected void initDatum() {super.initDatum();int data = getArguments().getInt(Constant.ID);binding.icon.setImageResource(data);}
}

10.广告界面

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

10.1下载广告

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() {@Overridepublic 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();}
}

10.2显示广告

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

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

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

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

Banner bannerView = holder.getView(R.id.banner);BannerImageAdapter<Ad> bannerImageAdapter = new BannerImageAdapter<Ad>(data.getData()) {@Overridepublic 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() {@Overridepublic 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());

11.1歌单详情

顶部是歌单信息,通过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() {@Overridepublic void onClick(View v) {SuperDialog.newInstance(fragmentManager).setTitleRes(R.string.confirm_delete).setOnClickListener(new View.OnClickListener() {@Overridepublic 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);}}

11.2黑胶唱片

上面是黑胶唱片,和网易云音乐差不多,随着音乐滚动或暂停,顶部是控制相关,音乐播放逻辑是封装到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;}@Overridepublic 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/23player.setDataSource(context, Uri.parse(uri));} else {//设置数据源player.setDataSource(uri);}//同步准备//真实项目中可能会使用异步//因为如果网络不好//同步可能会卡住player.prepare();
//            player.prepareAsync();//开始播放器player.start();//回调监听器publishPlayingStatus();//启动播放进度通知startPublishProgress();prepareLyric(data);} catch (IOException e) {//TODO 播放错误处理}}@Overridepublic void pause() {if (isPlaying()) {//如果在播放就暂停player.pause();ListUtil.eachListener(listeners, musicPlayerListener -> musicPlayerListener.onPaused(data));stopPublishProgress();}}@Overridepublic void resume() {if (!isPlaying()) {//获取音频焦点if (!requestAudioFocus()) {return;}resumeNow();}}private void resumeNow() {//如果没有播放就播放player.start();//回调监听器publishPlayingStatus();//启动进度通知startPublishProgress();}@Overridepublic void addMusicPlayerListener(MusicPlayerListener listener) {if (!listeners.contains(listener)) {listeners.add(listener);}//启动进度通知startPublishProgress();}@Overridepublic void removeMusicPlayerListener(MusicPlayerListener listener) {listeners.remove(listener);}@Overridepublic 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*/@Overridepublic void onCompletion(MediaPlayer mp) {isPrepare = false;//回调监听器ListUtil.eachListener(listeners, listener -> listener.onCompletion(mp));}@Overridepublic void setLooping(boolean looping) {player.setLooping(looping);}/*** 音频焦点改变了回调** @param focusChange*/@Overridepublic 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 {@Overridepublic void setDatum(List<Song> datum) {//将原来数据playList标志设置为falseDataUtil.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);}@Overridepublic 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);}}//设置最后播放音乐的Idsp.setLastPlaySongId(data.getId());}@Overridepublic void pause() {musicPlayerManager.pause();}@Overridepublic Song next() {if (datum.size() == 0) {//如果没有音乐了//直接返回nullreturn 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);}@Overridepublic 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*/@Overridepublic 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());
});

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


歌词实现了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()) {//创建全局歌词ViewinitGlobalLyricView();//如果原来锁定了歌词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() {@Overridepublic 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();//更新viewupdateView();//清除歌词解锁通知NotificationUtil.clearUnlockGlobalLyricNotification(context);//解除接收全局歌词事件广播接受者unregisterUnlockGlobalLyricReceiver();}/*** 解除接收全局歌词事件广播接受者*/private void unregisterUnlockGlobalLyricReceiver() {if (unlockGlobalLyricBroadcastReceiver != null) {context.unregisterReceiver(unlockGlobalLyricBroadcastReceiver);unlockGlobalLyricBroadcastReceiver = null;}}@Overridepublic 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轴方向上移动的距离*/@Overridepublic void onGlobalLyricDrag(int y) {layoutParams.y = y - SizeUtil.getStatusBarHeight(context);//更新viewupdateView();//保存歌词y坐标sp.setGlobalLyricViewY(layoutParams.y);}...
}

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

12.1媒体控制器

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

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());
}

12.2接收媒体控制

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

12.3桌面Widget

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

public class MusicWidget extends AppWidgetProvider {/*** 添加,重新运行应用,周期时间,都会调用** @param context* @param appWidgetManager* @param appWidgetIds*/@Overridepublic void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {super.onUpdate(context, appWidgetManager, appWidgetIds);//尝试启动serviceServiceUtil.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>() {@Overridepublic void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) {//显示封面views.setImageViewBitmap(R.id.icon, resource);appWidgetManager.updateAppWidget(appWidgetId, views);}@Overridepublic void onLoadCleared(@Nullable Drawable placeholder) {//显示默认图片views.setImageViewBitmap(R.id.icon, BitmapFactory.decodeResource(context.getResources(), R.drawable.placeholder));appWidgetManager.updateAppWidget(appWidgetId, views);}});}}}
}

13.登录/注册/验证码登录

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

14.评论


评论列表包括下拉刷新,上拉加载更多,点赞,发布评论,回复评论,Emoji,话题和提醒人点击,选择好友,选择话题等。

14.1下拉刷新和下拉加载更多

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

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

14.2提醒人和话题点击

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

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() {@Overridepublic 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;
}

14.3选择好友

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

adapter.setOnItemClickListener(new OnItemClickListener() {@Overridepublic 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());}}}});
}

15.视频和播放

真实项目中视频播放大部分都是用第三方服务,例如:阿里云视频服务,腾讯视频服务,因为他们提供一条龙服务,包括审核,转码,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() {@Overridepublic void onPrepared(String url, Object... objects) {super.onPrepared(url, objects);//开始播放了才能旋转和全屏orientationUtils.setEnable(true);isPlay = true;}@Overridepublic void onQuitFullscreen(String url, Object... objects) {super.onQuitFullscreen(url, objects);if (orientationUtils != null) {orientationUtils.backToProtVideo();}}}).setLockClickListener(new LockClickListener() {@Overridepublic void onClick(View view, boolean lock) {if (orientationUtils != null) {//配合下方的onConfigurationChangedorientationUtils.setEnable(!lock);}}
}).build(binding.player);//开始播放
binding.player.startPlayLogic();

16.用户详情/更改资料

用户详情顶部显示用户信息,好友数量,下面分别显示创建的歌单,收藏的歌单,发布的动态,类似微信朋友圈,右上角可以更改用户资料;整体采用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) {//获取字符串idint resourceId = titleIds[position];//获取字符串return context.getResources().getString(resourceId);
}

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


发布效果和微信朋友圈类似,可以选择图片,和地理位置;地理位置使用高德地图实现选择,路径规划是调用系统中安装的地图,类似微信。

17.1选择位置

/*** 搜索该位置的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); // 设置每页最多返回多少条poiitemquery.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();}
}

17.2高德地图路径规划

/*** 使用高德地图路径规划** @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());
}

18.聊天/离线推送


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

18.1登录聊天服务器

/*** 连接聊天服务器** @param data*/
private void connectChat(Session data) {RongIMClient.connect(data.getChatToken(), new RongIMClient.ConnectCallback() {/*** 成功回调* @param userId 当前用户 ID*/@Overridepublic void onSuccess(String userId) {Timber.d("connect chat success %s", userId);}/*** 错误回调* @param errorCode 错误码*/@Overridepublic 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 数据库打开失败*/@Overridepublic void onDatabaseOpened(RongIMClient.DatabaseOpenStatus databaseOpenStatus) {}});}

18.2设置消息监听

chatClient.addOnReceiveMessageListener(new OnReceiveMessageWrapperListener() {@Overridepublic 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());}
});

18.3发送文本消息

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

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() {@Overridepublic void onAttached(Message message) {// 消息成功存到本地数据库的回调Timber.d("sendTextMessage onAttached %s", message);}@Overridepublic void onSuccess(Message message) {// 消息发送成功的回调Timber.d("sendTextMessage success %s", message);//清空输入框clearInput();addMessage(message);}@Overridepublic void onError(Message message, RongIMClient.ErrorCode errorCode) {// 消息发送失败的回调Timber.e("sendTextMessage onError %s %s", message, errorCode);}});}

19.离线推送

先开启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);}
}

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


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

20.1商品详情富文本

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

20.2支付宝/微信支付

客户端先集成微信,支付宝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);
}

20.3处理支付结果

/*** 支付宝支付状态改变了** @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);}
}

24.语音识别输入地址

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

/*** 百度语音识别事件监听器* <p>* https://ai.baidu.com/ai-doc/SPEECH/4khq3iy52*/
EventListener voiceRecognitionEventListener = new EventListener() {/*** 事件回调* @param name 回调事件名称* @param params 回调参数* @param data 数据* @param offset 开始位置* @param length 长度*/@Overridepublic 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);}
};

25.百度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*/@Overridepublic 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*/@Overridepublic void onError(OCRError error) {SuperToast.show(getString(R.string.ocr_error, error.getMessage(), error.getErrorCode()));}});
}

26.项目总结

总体来说项目功能还是很全的,还有一些小功能,例如:快捷方式等就不在贴代码了,但肯定没发和原版比,相信大家只要做过程序员就能理解,毕竟原版是一个商业级项目,几十个人天天开发和维护,而且持续了几年了;不过恕我直言,现在的常见的音乐软件都太复杂了,各种功能,不过都要恰饭,好像又能理解了

0.高仿Android网易云音乐OkHttp+Retrofit+RxJava+Glide+MVC+MVVM相关推荐

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

    简介 这是一个使用Java(以后还会推出Kotlin版本)语言,从0开发一个Android平台,接近企业级的项目(我的云音乐),包含了基础内容,高级内容,项目封装,项目重构等知识:主要是使用系统功能, ...

  2. 卡拉OK歌词原理和实现高仿Android网易云音乐

    大家好,我们是爱学啊,继上一篇讲解了[LRC歌词原理和实现高仿Android网易云音乐],今天给大家带来一篇关于卡拉OK歌词原理和在Android上如何实现歌词逐字滚动的效果,本文来自[Android ...

  3. LRC歌词原理和实现高仿Android网易云音乐

    大家好,我们是爱学啊,今天给大家带来一篇关于LRC歌词原理和在Android上如何实现歌词逐行滚动的效果,本文来自[Android开发项目实战我的云音乐]课程:逐字滚动下一篇文章讲解. 效果图 相信大 ...

  4. 体验Vue3.0, 仿一个网易云音乐客户端

    一.用到的技术栈 前端: vue3.0全家桶:(ts+jsx) vuex: vuex-module-decorators swiper:非常受欢迎与实用的轮播图插件,swiper create-key ...

  5. android 网易云音乐上滑动画,Android 仿网易云音乐 音轨跳动效果

    网易云音乐的Loading效果,大家应该也比较熟悉了,效果是一个红色音轨不断跳动的效果,一般用于Loading等待时填充使用.本篇来自定义这个效果. Android 仿网易云音乐 音轨跳动View.g ...

  6. android+仿最新网易云音乐底面栏,安卓仿网易云音乐通知栏控制音乐,默认显示Notification bigView...

    最近在做一个音乐播放器的时候遇到了一个关于notification的问题,在网上找了很久都没有头绪.后来找到了解决的办法,特意记录一下. 问题描述 首先请看网易云音乐的通知栏 普通高度的notific ...

  7. android 网易云音乐上滑动画,Android_Activity切换动画OverridePendingTransition(Cover 网易云音乐动画)...

    今天我想讲一个研究别人好动画的方法,并实现出来,我是网易云音乐的铁粉啊,很喜欢网易音乐那个开屏切换动画,还有点击一个页面然后返回的那个退出动画,所以呢,我把它实现出来了,还是蛮开心的,依然,我不讲那个 ...

  8. 仿写网易云音乐Demo项目(Vue3+Vite+Vuex+Vue-Router4.0)

    前言 学习了一段时间vue3的基础知识学习,百学不如一练,想着还是做出一个实际的demo项目(适配为移动端),来实践巩固自己所学的知识点

  9. 基于Django3.0的Python版网易云音乐API

    文章目录 项目地址 文档 测试链接 关于 新增 支持直接用js引用api 支持简易的日志记录 安装 运行 接口调用须知 目录 项目地址 https://github.com/Kevin0z0/Pyth ...

最新文章

  1. oracle 截取字符串和查找字符
  2. saber仿真软件_电力电子应用技术的MATLAB仿真
  3. SQL SERVER 2008的错误日志太大的解决办法
  4. JAVA 基础 (一)
  5. nvarchar,nchar,vchar,nvchar,char…
  6. spring mvc 伪静态处理
  7. layedit-jfinal后台获取
  8. 阿里云服务器部署GeoServer以及跨域处理
  9. CentOS忘记root登录密码
  10. 《UNIX环境高级编程——APUE》
  11. python实现元胞自动机
  12. Redis雪崩,击穿,穿透产生原因以及其解决方案
  13. 语速对科大讯飞,百度,思必驰,云知声的语音引擎识别结果影响对比
  14. 轩小陌的Python笔记-Pandas 高级数据处理
  15. Linux并行执行权限,如何在Linux中使用flock控制程序的异步执行
  16. BFS模版题----水缸灌水(c++)
  17. @3-1 CCF 2020-09-1 称检测点查询
  18. 主板开启网络唤醒_网络唤醒bios设置【应用方式】
  19. 你公司哪个部门是“老大”?
  20. Fork/Join框架基本使用

热门文章

  1. 【时间之外】10分钟搞懂Python+Tushare+Excel股票分析
  2. 展会推广有哪些好的创意 怎样让展会推广更有创意
  3. SIMATIC S7-300 Profibus通讯——(2)EM277与S7-300通讯
  4. 规则引擎解决方案浅析
  5. Goldsrc 地图 BSP 文件格式规范
  6. 【2021年数学建模国赛C题第一问】基于TOPSIS法评价类模型
  7. 2023北京老博会,CISSE中国国际养老服务业博览会
  8. 华为双前置摄像头_华为第一款“刘海屏”手机发布,前置摄像头逆天
  9. python学习笔记_week14
  10. [poj1797] Heavy Transportation