简介

这是一个使用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'

用户协议对话框

使用自定义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);}
}

动态权限

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

引导界面

引导界面比较简单,就是多个图片可以左右滚动,整体使用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);}
}

广告界面

实现图片广告和视频广告,广告数据是在首页是缓存到本地,目的是在启动界面加载更快,因为真实项目中,大部分项目启动页面广告时间一共就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() {@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();}
}

显示广告

/*** 显示视频广告** @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());
}

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

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

首页没有顶部是轮播图,然后是可以左右的菜单,接下来是热门歌单,推荐单曲,最后是首页排序模块;整体上使用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());

歌单详情

顶部是歌单信息,通过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);}}

黑胶唱片

上面是黑胶唱片,和网易云音乐差不多,随着音乐滚动或暂停,顶部是控制相关,音乐播放逻辑是封装到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());
});

媒体控制器/桌面歌词/桌面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);}...
}

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

媒体控制器

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

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

桌面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);}});}}}
}

登录/注册/验证码登录

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

评论

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

下拉刷新和下拉加载更多

核心逻辑就只需要更改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();
}

提醒人和话题点击

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

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

选择好友

对数据分组,然后显示右侧索引,选择了通过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());}}}});
}

视频和播放

真实项目中视频播放大部分都是用第三方服务,例如:阿里云视频服务,腾讯视频服务,因为他们提供一条龙服务,包括审核,转码,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();

用户详情/更改资料

用户详情顶部显示用户信息,好友数量,下面分别显示创建的歌单,收藏的歌单,发布的动态,类似微信朋友圈,右上角可以更改用户资料;整体采用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);
}

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

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

选择位置

/*** 搜索该位置的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();}
}

高德地图路径规划

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

聊天/离线推送

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

登录聊天服务器

/*** 连接聊天服务器** @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) {}});}

设置消息监听

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

发送文本消息

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

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

离线推送

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

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

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

商品详情富文本

//详情
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);

支付

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

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

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

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

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

    0.系列文章目录 1.启动界面 2.广告和引导界面 1.项目简介 这是一个使用Java(以后还会推出Kotlin版本)语言,从0开发一个Android平台,接近企业级的项目(我的云音乐),包含了基础内 ...

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  10. 从联通沃指数看网易云音乐的流量收割路径

    联通大数据近日发布了10月的沃指数,从活跃用户数和户均月耗流量两个维度对移动应用进行了排行. 与其他数据不同的是,沃指数以中国联通3亿出账用户作为样本数据,能够更直观且准确地反映移动应用的发展情况和竞 ...

最新文章

  1. (传送门)微信公众号推送文章(个人认为未来可能对我有用的部分)
  2. java jxls 科学计数_java通过jxls框架实现导入导出excel
  3. JQUERY的split
  4. 3/7 SELECT语句:过滤(WHERE)
  5. TensorFlow入门--张量的定义与基本运算
  6. Nginx反向代理及简单负载均衡配置
  7. Android 11 正式版发布!
  8. 零基础学python实战-零基础学习python_类和对象(36-40课)
  9. Google Calendar Sync_ 把 Microsoft Outlook 同步到 G...
  10. 《统计学习基础-数据挖掘、推理与…
  11. win10 1809版本手动安装WSL和ubuntu 18.04
  12. 构建一个属于自己的centos7-php80-swoole的Docker镜像
  13. 微信退款服务器系统失败怎么办,微信缴费失败怎么退款?能退回吗?
  14. 基于stm32的四轴无人机和智能车编程实践目录
  15. 北京理工大学:《Python语言程序设计》____笔记整理
  16. php pdf文档内容修改,php2pdf-如何使用php修改pdf中的内容,并且保证格式不乱
  17. 关于SparkSQL的开窗函数,你应该知道这些!
  18. Adobe XD|不论是安卓还是苹果用户都可在手机上预览Adobe XD预览稿
  19. 远程桌面连接是什么?如何开启远程桌面连接详细教程
  20. swift地图定位(二十)百度地图的使用(POI)

热门文章

  1. gulp打包Replace Autoprefixer browsers option to Browserslist config. Use browserslis
  2. 交互体验设计优秀的产品
  3. 2021 年年度蕞佳开源软件!
  4. 流利阅读 2019.3.9 Young children should be taught in their mother tongue, not in English
  5. Find a Mother Vertex in a Graph
  6. MyBatis Mapper.xml的choose/case标签详解
  7. Circular Coloring
  8. echarts 折线图
  9. 勾股数规律(任意三个数能够满足勾股定理需要满足的条件)
  10. 计算机键盘能直接接手机吗,电脑键盘怎么连接手机