这是一个使用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动态权限{ ... @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); } }
高版本必须要动态处理权限,这里在启动界面请求了一些权限,但推荐在用到的时候才获取,写法差不多,这里使用第三方框架实现,当然也可以直接使用系统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();
}
@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{ public GuideAdapter(Context context, @NonNull FragmentManager fm) { super(context, fm); } @NonNull @Override public Fragment getItem(int position) { return GuideFragment.newInstance(getData(position)); } }
public class GuideFragment extends BaseViewModelFragment广告界面{ ... @Override protected 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() {
@Override
public void run() {
try {
//FutureTarget会阻塞
//所以需要在子线程调用
FutureTarget 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();
}
}
显示广告
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());
}
显示图片就是显示本地图片了,没什么难点,就不贴代码了。
首页/歌单详情/黑胶唱片界面首页没有顶部是轮播图,然后是可以左右的菜单,接下来是热门歌单,推荐单曲,最后是首页排序模块;整体上使用RecycerView实现,轮播图:
Banner bannerView = holder.getView(R.id.banner); BannerImageAdapterbannerImageAdapter = new BannerImageAdapter (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());
@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 {
...
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));
}
@Override
public void onCompletion(MediaPlayer mp) {
isPrepare = false;
//回调监听器
ListUtil.eachListener(listeners, listener -> listener.onCompletion(mp));
}
@Override
public void setLooping(boolean looping) {
player.setLooping(looping);
}
@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 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));
}
@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
歌词实现了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;
}
@Override
public void onGlobalLyricDrag(int y) {
layoutParams.y = y - SizeUtil.getStatusBarHeight(context);
//更新view
updateView();
//保存歌词y坐标
sp.setGlobalLyricViewY(layoutParams.y);
}
...
}
显示和隐藏只需要调用该管理器的相关方法就行了。
媒体控制器使用了可以通过系统媒体控制器,通知栏,锁屏界面,耳机,蓝牙耳机等设备控制媒体播放暂停,只需要把媒体信息更新到系统:
MusicPlayerService
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 {
@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() {
@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);
}
});
}
}
}
}
登录/注册/验证码登录
登录注册没有多大难度,用户名和密码登录,就是把信息传递到服务端,可以加密后在传输,服务端判断登录成功,返回一个标记,客户端保存,其他需要的登录的接口带上;验证码登录就是用验证码代替密码,发送验证码都是服务端发送,客户端只需要调用接口。
评论
评论列表包括下拉刷新,上拉加载更多,点赞,发布评论,回复评论,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()));
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());
}
}
}
});
}
视频和播放
真实项目中视频播放大部分都是用第三方服务,例如:阿里云视频服务,腾讯视频服务,因为他们提供一条龙服务,包括审核,转码,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();
用户详情/更改资料
用户详情顶部显示用户信息,好友数量,下面分别显示创建的歌单,收藏的歌单,发布的动态,类似微信朋友圈,右上角可以更改用户资料;整体采用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);
}
}
@Nullable
@Override
public CharSequence getPageTitle(int position) {
//获取字符串id
int resourceId = titleIds[position];
//获取字符串
return context.getResources().getString(resourceId);
}
发布动态/选择位置/路径规划
发布效果和微信朋友圈类似,可以选择图片,和地理位置;地理位置使用高德地图实现选择,路径规划是调用系统中安装的地图,类似微信。
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();
}
}
高德地图路径规划
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,这里是登录后返回,然后客户端登录聊天服务器,然后设置消息监听,发送消息等。
private void connectChat(Session data) {
RongIMClient.connect(data.getChatToken(), new RongIMClient.ConnectCallback() {
@Override
public void onSuccess(String userId) {
Timber.d("connect chat success %s", userId);
}
@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);
}
@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);
}
}
商城/订单/支付/购物车
学到这里,大家不能说熟悉,那么看到上面的界面,那么大体要能实现出来。
商品详情富文本//详情
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() {
@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 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,最后就是处理支付结果。
private void processAlipay(String data) {
PayUtil.alipay(getHostActivity(), 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);
}
处理支付结果
@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,先集成,然后初始化,最后是监听识别结果:
EventListener voiceRecognitionEventListener = new EventListener() {
@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() {
@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());
}
@Override
public void onError(OCRError error) {
SuperToast.show(getString(R.string.ocr_error, error.getMessage(), error.getErrorCode()));
}
});
}
还有一些功能,例如:快捷方式等就不在贴代码了。



