1.我的项目简介

这是一个应用Java(当前还会推出Kotlin版本)语言,从0开发一个Android平台,靠近企业级的我的项目(我的云音乐),蕴含了根底内容,高级内容,我的项目封装,我的项目重构等常识;次要是应用零碎性能,风行的第三方框架,第三方服务,实现靠近企业级商业级我的项目。

2.我的项目性能点

隐衷协定对话框
启动界面和动静解决权限
疏导界面和广告
轮播图和侧滑菜单
首页简单列表和列表排序
音乐播放和音乐列表治理
全局音乐管制条
桌面歌词和自定义款式
全局媒体控制中心
评论和回复评论
评论富文本点击
评论揭示人和话题
朋友圈动静列表和公布
高德地图定位和门路布局
阿里云OSS上传
视频播放和管制
QQ/微信登录和分享
商城/购物车\微信\支付宝领取
文本和图片聊天
音讯离线推送
主动和手动查看更新
内存透露和优化
...

3.开发环境概述

2022年5月开发实现的,所以全部都是最新的,均匀每3年会从新制作,当初曾经是第三版了。

JDK17Android 12/13最低兼容版本:Android 6.0Android 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/pagingdef paging_version = "3.1.1"//增加所有libs目录外面的jar,aarimplementation 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-startedimplementation 'com.qmuiteam:qmui:2.0.1'//动静解决权限//https://github.com/permissions-dispatcher/PermissionsDispatcherimplementation "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/gsonimplementation 'com.google.code.gson:gson:2.9.0'//主动开释RxJava相干资源//https://github.com/uber/AutoDisposeimplementation "com.uber.autodispose2:autodispose-androidx-lifecycle:2.1.1"//banner轮播图框架//https://github.com/youth5201314/bannerimplementation 'io.github.youth5201314:banner:2.2.2'//图片加载框架,还援用他目标是,coil有些性能不好实现//https://github.com/bumptech/glideimplementation 'com.github.bumptech.glide:glide:+'annotationProcessor 'com.github.bumptech.glide:compiler:+'implementation 'androidx.recyclerview:recyclerview:1.2.1'//给控件增加未读音讯数红点//https://github.com/bingoogolapple/BGABadgeView-Androidimplementation 'com.github.bingoogolapple.BGABadgeView-Android:api:1.2.0'annotationProcessor 'com.github.bingoogolapple.BGABadgeView-Android:compiler:1.2.0'//webview进度条//https://github.com/youlookwhat/WebProgressimplementation 'com.github.youlookwhat:WebProgress:1.2.0'//日志框架//https://github.com/JakeWharton/timberimplementation 'com.jakewharton.timber:timber:5.0.1'implementation "androidx.media:media:+"//和Glide配合解决图片//能够实现很多成果//含糊;圆角;圆//咱们这里是用它实现含糊成果//https://github.com/wasabeef/glide-transformationsimplementation 'jp.wasabeef:glide-transformations:+'//圆形图片控件//https://github.com/hdodenhof/CircleImageViewimplementation 'de.hdodenhof:circleimageview:+'//下载框架//https://github.com/ixuea/android-downloaderimplementation 'com.ixuea:android-downloader:3.0.0'//阿里云oss//官网文档:https://help.aliyun.com/document_detail/32043.html//sdk地址:https://github.com/aliyun/aliyun-oss-android-sdkimplementation 'com.aliyun.dpa:oss-android-sdk:+'//高德地图,这里援用的是3d//https://lbs.amap.com/api/android-sdk/guide/create-project/android-studio-create-project#gradle_sdkimplementation '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%97implementation project(path: ':super-speech-baidu')//TextView显示富文本,目前次要用在商品详情界面,显示富文本商品形容//https://github.com/wangchenyan/html-textimplementation 'com.github.wangchenyan:html-text:+'//Hutool是一个小而全的Java工具类库// 通过静态方法封装,升高相干API的学习老本// 进步工作效率,使Java领有函数式语言般的优雅//https://github.com/looly/hutoolimplementation 'cn.hutool:hutool-all:5.7.14'//支付宝领取//https://opendocs.alipay.com/open/204/105296implementation 'com.alipay.sdk:alipaysdk-android:+@aar'//融云IM//https://docs.rongcloud.cn/v4/5X/views/im/ui/guide/quick/include/android.htmlimplementation '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_5implementation '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标签,设置后就能够点击了,而后批改默认对话框宽度,因为默认的有点窄。

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 */@Overridepublic 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    @Override    public Fragment getItem(int position) {        return GuideFragment.newInstance(getData(position));    }}
/** * 疏导界面Fragment */public class GuideFragment extends BaseViewModelFragment<FragmentGuideBinding> {    ...    @Override    protected void initDatum() {        super.initDatum();        int data = getArguments().getInt(Constant.ID);        binding.icon.setImageResource(data);    }}

10.广告界面


image.png

实现图片广告和视频广告,广告数据是在首页是缓存到本地,目标是在启动界面加载更快,因为实在我的项目中,大部分我的项目启动页面广告工夫一共就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() {                    @Override                    public void run() {                        try {                            //FutureTarget会阻塞                            //所以须要在子线程调用                            FutureTarget<File> target = Glide.with(getHostActivity().getApplicationContext())                                    .asFile()                                    .load(ResourceUtil.resourceUri(data.getIcon()))                                    .submit();                            //获取下载的文件                            File file = target.get();                            //将文件拷贝到咱们须要的地位                            FileUtils.moveFile(file, targetFile);                        } catch (Exception e) {                            e.printStackTrace();                        }                    }                }        ).start();    }}

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 对象与界面 view    player.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()) {    @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());

11.1歌单详情

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

//增加头部adapter.addHeaderView(createHeaderView());
/** * 显示数据的办法 * * @param holder * @param data */@Overrideprotected 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);    }}

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;    }    @Override    public void play(String uri, Song data) {        //保存信息        this.uri = uri;        this.data = data;        //开释播放器        player.reset();        //获取音频焦点        if (!requestAudioFocus()) {            return;        }        playNow();    }    private void playNow() {        isPrepare = true;        try {            if (uri.startsWith("content://")) {                //内容提供者格局                //本地音乐                //uri示例:content://media/external/audio/media/23                player.setDataSource(context, Uri.parse(uri));            } else {                //设置数据源                player.setDataSource(uri);            }            //同步筹备            //实在我的项目中可能会应用异步            //因为如果网络不好            //同步可能会卡住            player.prepare();//            player.prepareAsync();            //开始播放器            player.start();            //回调监听器            publishPlayingStatus();            //启动播放进度告诉            startPublishProgress();            prepareLyric(data);        } catch (IOException e) {            //TODO 播放错误处理        }    }    @Override    public void pause() {        if (isPlaying()) {            //如果在播放就暂停            player.pause();            ListUtil.eachListener(listeners, musicPlayerListener -> musicPlayerListener.onPaused(data));            stopPublishProgress();        }    }    @Override    public void resume() {        if (!isPlaying()) {            //获取音频焦点            if (!requestAudioFocus()) {                return;            }            resumeNow();        }    }    private void resumeNow() {        //如果没有播放就播放        player.start();        //回调监听器        publishPlayingStatus();        //启动进度告诉        startPublishProgress();    }    @Override    public void addMusicPlayerListener(MusicPlayerListener listener) {        if (!listeners.contains(listener)) {            listeners.add(listener);        }        //启动进度告诉        startPublishProgress();    }    @Override    public void removeMusicPlayerListener(MusicPlayerListener listener) {        listeners.remove(listener);    }    @Override    public void seekTo(int progress) {        player.seekTo(progress);    }    /**     * 公布播放中状态     */    private void publishPlayingStatus() {//        for (MusicPlayerListener listener : listeners) {//            listener.onPlaying(data);//        }        //应用重构后的办法        ListUtil.eachListener(listeners, musicPlayerListener -> musicPlayerListener.onPlaying(data));    }    /**     * 播放结束了回调     *     * @param mp     */    @Override    public void onCompletion(MediaPlayer mp) {        isPrepare = false;        //回调监听器        ListUtil.eachListener(listeners, listener -> listener.onCompletion(mp));    }    @Override    public void setLooping(boolean looping) {        player.setLooping(looping);    }    /**     * 音频焦点扭转了回调     *     * @param focusChange     */    @Override    public void onAudioFocusChange(int focusChange) {        Timber.d("onAudioFocusChange %s", focusChange);        switch (focusChange) {            case AudioManager.AUDIOFOCUS_GAIN:                //获取到焦点了                if (resumeOnFocusGain) {                    if (isPrepare) {                        resumeNow();                    } else {                        playNow();                    }                    resumeOnFocusGain = false;                }                break;            case AudioManager.AUDIOFOCUS_LOSS:                //永恒失去焦点,例如:其余利用申请时,也是播放音乐                if (isPlaying()) {                    pause();                }                break;            case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:            case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:                //暂时性失去焦点,例如:通话了,或者呼叫了语音助手等申请                if (isPlaying()) {                    resumeOnFocusGain = true;                    pause();                }                break;        }    }}

音乐列表逻辑封装到MusicListManager:

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

外界对立应用播放列表管理器播放音乐,上一曲下一曲:

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

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()) {            //创立全局歌词View            initGlobalLyricView();            //如果原来锁定了歌词            if (sp.isGlobalLyricLock()) {                //锁定歌词                lock();            }        }    }    public synchronized static GlobalLyricManagerImpl getInstance(Context context) {        if (instance == null) {            instance = new GlobalLyricManagerImpl(context);        }        return instance;    }    /**     * 锁定全局歌词     */    private void lock() {        //保留全局歌词锁定状态        sp.setGlobalLyricLock(true);        //设置全局歌词控件状态        setGlobalLyricStatus();        //显示简略模式        globalLyricView.simpleStyle();        //更新布局        updateView();        //显示解锁全局歌词告诉        NotificationUtil.showUnlockGlobalLyricNotification(context);        //注册接管解锁全局歌词广告接收器        registerUnlockGlobalLyricReceiver();    }    /**     * 注册接管解锁全局歌词广告接收器     */    private void registerUnlockGlobalLyricReceiver() {        if (unlockGlobalLyricBroadcastReceiver == null) {            //创立播送接受者            unlockGlobalLyricBroadcastReceiver = new BroadcastReceiver() {                @Override                public void onReceive(Context context, Intent intent) {                    if (Constant.ACTION_UNLOCK_LYRIC.equals(intent.getAction())) {                        //歌词解锁事件                        unlock();                    }                }            };            IntentFilter intentFilter = new IntentFilter();            //只监听歌词解锁事件            intentFilter.addAction(Constant.ACTION_UNLOCK_LYRIC);            //注册            context.registerReceiver(unlockGlobalLyricBroadcastReceiver, intentFilter);        }    }    /**     * 解锁歌词     */    private void unlock() {        //设置没有锁定歌词        sp.setGlobalLyricLock(false);        //设置歌词状态        setGlobalLyricStatus();        //解锁后显示规范款式        globalLyricView.normalStyle();        //更新view        updateView();        //革除歌词解锁告诉        NotificationUtil.clearUnlockGlobalLyricNotification(context);        //解除接管全局歌词事件播送接受者        unregisterUnlockGlobalLyricReceiver();    }    /**     * 解除接管全局歌词事件播送接受者     */    private void unregisterUnlockGlobalLyricReceiver() {        if (unlockGlobalLyricBroadcastReceiver != null) {            context.unregisterReceiver(unlockGlobalLyricBroadcastReceiver);            unlockGlobalLyricBroadcastReceiver = null;        }    }    @Override    public void show() {        //查看全局悬浮窗权限        if (!Settings.canDrawOverlays(context)) {            Intent intent = new Intent(context, SplashActivity.class);            intent.setAction(Constant.ACTION_LYRIC);            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);            context.startActivity(intent);            return;        }        //初始化全局歌词控件        initGlobalLyricView();        //设置显示了全局歌词        sp.setShowGlobalLyric(true);        WidgetUtil.onGlobalLyricShowStatusChanged(context, isShowing());    }    private boolean hasGlobalLyricView() {        return globalLyricView != null;    }    /**     * 全局歌词拖拽回调     *     * @param y y轴方向上挪动的间隔     */    @Override    public void onGlobalLyricDrag(int y) {        layoutParams.y = y - SizeUtil.getStatusBarHeight(context);        //更新view        updateView();        //保留歌词y坐标        sp.setGlobalLyricViewY(layoutParams.y);    }        ...}

显示和暗藏只须要调用该管理器的相干办法就行了。

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

12.3桌面Widget

创立布局,而后注册,最初就是更新信息:

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

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

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

14.评论


评论列表包含下拉刷新,上拉加载更多,点赞,公布评论,回复评论,Emoji,话题和揭示人点击,抉择好友,抉择话题等。

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

外围逻辑就只须要更改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();    }});@Overrideprotected 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() {                @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;}

14.3抉择好友

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

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

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@Overridepublic CharSequence getPageTitle(int position) {    //获取字符串id    int 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); // 设置每页最多返回多少条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();    }}

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         */        @Override        public void onSuccess(String userId) {            Timber.d("connect chat success %s", userId);        }        /**         * 谬误回调         * @param errorCode 错误码         */        @Override        public void onError(RongIMClient.ConnectionErrorCode errorCode) {            Timber.e("connect chat error %s", errorCode);            if (errorCode.equals(RongIMClient.ConnectionErrorCode.RC_CONN_TOKEN_INCORRECT)) {                //从 APP 服务获取新 token,并重连            } else {                //无奈连贯 IM 服务器,请依据相应的错误码作出对应解决            }            //因为咱们这个利用,不是相似微信那样纯聊天利用,所以聊天服务器连贯失败,也让进入利用            //实在我的项目中依照需要实现就行了            SuperToast.show(R.string.error_message_login);        }        /**         * 数据库回调.         * @param databaseOpenStatus 数据库关上状态. DATABASE_OPEN_SUCCESS 数据库关上胜利; DATABASE_OPEN_ERROR 数据库关上失败         */        @Override        public void onDatabaseOpened(RongIMClient.DatabaseOpenStatus databaseOpenStatus) {        }    });}

18.2设置音讯监听

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

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

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

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 长度     */    @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);    }};

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         */        @Override        public void onResult(GeneralResult result) {            StringBuilder builder = new StringBuilder();            for (WordSimple it : result.getWordList()) {                builder.append(it.getWords());                //每一项之间,增加空格,不便OCR失败                builder.append(" ");            }            binding.input.setText(builder.toString());        }        /**         * 失败         * @param error         */        @Override        public void onError(OCRError error) {            SuperToast.show(getString(R.string.ocr_error, error.getMessage(), error.getErrorCode()));        }    });}

26.我的项目总结

总体来说我的项目性能还是很全的,还有一些小性能,例如:快捷方式等就不在贴代码了,但必定没发和原版比,置信大家只有做过程序员就能了解,毕竟原版是一个商业级我的项目,几十个人天天开发和保护,而且继续了几年了;不过恕我直言,当初的常见的音乐软件都太简单了,各种性能,不过都要恰饭,如同又能了解了。