锐单电子商城 , 一站式电子元器件采购平台!
  • 电话:400-990-0325

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

时间:2022-08-20 20:30:00 耐高温抗渗碳ksc轴装电阻丝

image.png

0.系列文章目录

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

1.项目简介

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

2.项目功能点

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

3.开发环境概述

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

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

4.编译和操作

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

5.项目录结构

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

6.依赖框架

内容太多,只列出部分。

//分页组件版本 //这里可以查看最新版本:https://developer.android.google.cn/jetpack/androidx/releases/paging def paging_version = "3.1.1"  //添加所有libs目录里面的jar,aar implementation fileTree(dir: 'libs', include: ['*.jar','*.aar'])  //官方兼容组件,像AppCompatActivity是时候依靠里面了 implementation 'androidx.appcompat:appcompat:1.4.1'  //Material Design组件,像FloatingActionButton是时候依靠里面了 implementation 'com.google.android.material:material:1.4.0'  ///官方约束布局,像ConstraintLayout是时候依靠里面了 implementation 'androidx.constraintlayout:constraintlayout:2.1.0'  //UI框架使用他的工具类框架,也可单独复制 //https://qmuiteam.com/android/get-started implementation 'com.qmuiteam:qmui:2.0.1'  //动态处理权限 //https://github.com/permissions-dispatcher/PermissionsDispatcher implementation "com.github.permissions-dispatcher:permissionsdispatcher:4.8.0" annotationProcessor "com.github.permissions-dispatcher:permissionsdispatcher-processor:4.8.0"  //api:依赖会传递到其他应用本模块的项目 implementation project(path: ':super-j') ...  //使用gson解析json //https://github.com/google/gson implementation 'com.google.code.gson:gson:2.9.0'  //自动释放RxJava相关资源 //https://github.com/uber/AutoDispose implementation "com.uber.autodispose2:autodispose-androidx-lifecycle:2.1.1"  //banner轮播图框架 //https://github.com/youth5201314/banner implementation 'io.github.youth5201314:banner:2.2.2'  //图片加载框架,引用他的目的是,coil有些功能很难实现 //https://github.com/bumptech/glide implementation 'com.github.bumptech.glide:glide: ' annotationProcessor 'com.github.bumptech.glide:compiler: '  implementation 'androidx.recyclerview:recyclerview:1.2.1'  ///添加未读信息红点到控件 //https://github.com/bingoogolapple/BGABadgeView-Android implementation 'com.github.bingoogolapple.BGABadgeView-Android:api:1.2.0' annotationProcessor 'com.github.bingoogolapple.BGABadgeView-Android:compiler:1.2.0'  //webview进度条 //https://github.com/youlookwhat/WebProgress implementation 'com.github.youlookwhat:WebProgress:1.2.0'  //日志框架 //https://github.com/JakeWharton/timber implementation 'com.jakewharton.timber:timber:5.0.1'  implementation "androidx.media:media: "  //和Glide配合处理图片 //可以达到很多效果 //模糊;圆角;圆 ///我们在这里用它来达到模糊效果 //https://github.com/wasabeef/glide-transformations implementation 'jp.wasabeef:glide-transformations: '  ///圆形图片控件 //https://github.com/hdodenhof/CircleImageView implementation 'de.hdodenhof:circleimageview: '  //下载框架 //https://github.com/ixuea/android-downloader implementation 'com.ixuea:android-downloader:3.0.0'  //阿里云oss //官方文件:https://help.aliyun.com/document_detail/32043.html //sdk地址:https://github.com/aliyun/aliyun-oss-android-sdk implementation 'com.aliyun.dpa:oss-android-sdk: '  //高德地图,这里引用的是3d //https://lbs.amap.com/api/android-sdk/guide/create-project/android-studio-create-project#gradle_sdk implementation 'com.amap.api:3dmap: '  //定位功能 implementation 'com.amap.api:location: '  ///百度语音相关技术,目前主要用于收货地址编辑界面,语音输入收货地址 //https://ai.baidu.com/ai-doc/SPEECH/Pkgt4wwdx#集成指南 implementation project(path: ':super-speech-baidu')  //TextView显示富文本,目前主要用于商品详情界面,显示富文本商品描述 //https://github.com/wagchenyan/html-text
implementation 'com.github.wangchenyan:html-text:+'

//Hutool是一个小而全的Java工具类库
// 通过静态方法封装,降低相关API的学习成本
// 提高工作效率,使Java拥有函数式语言般的优雅
//https://github.com/looly/hutool
implementation 'cn.hutool:hutool-all:5.7.14'

//支付宝支付
//https://opendocs.alipay.com/open/204/105296
implementation 'com.alipay.sdk:alipaysdk-android:+@aar'

//融云IM
//https://docs.rongcloud.cn/v4/5X/views/im/ui/guide/quick/include/android.html
implementation 'cn.rongcloud.sdk:im_lib:+'

//微信支付
//官方sdk下载文档:https://developers.weixin.qq.com/doc/oplatform/Downloads/Android_Resource.html
//官方集成文档:https://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=8_5
implementation 'com.tencent.mm.opensdk:wechat-sdk-android:+'

//内存泄漏检测工具
//https://github.com/square/leakcanary
//只有调试模式下才添加该依赖
debugImplementation 'com.squareup.leakcanary:leakcanary-android:+'

testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'

7.用户协议对话框

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

public class TermServiceDialogFragment extends BaseViewModelDialogFragment<FragmentDialogTermServiceBinding> { 
        

    ...

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

8.动态权限

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

/** * 权限授权了就会调用该方法 * 请求相机权限目的是扫描二维码,拍照 */
@NeedsPermission({ 
        
        Manifest.permission.CAMERA,
        Manifest.permission.READ_EXTERNAL_STORAGE,
        Manifest.permission.WRITE_EXTERNAL_STORAGE,
        Manifest.permission.ACCESS_COARSE_LOCATION,
        Manifest.permission.ACCESS_FINE_LOCATION
})
void onPermissionGranted() { 
        
    //如果有权限就进入下一步
    prepareNext();
}

/** * 显示权限授权对话框 * 目的是提示用户 */
@OnShowRationale({ 
        
        Manifest.permission.CAMERA,
        Manifest.permission.READ_EXTERNAL_STORAGE,
        Manifest.permission.WRITE_EXTERNAL_STORAGE,
        Manifest.permission.ACCESS_COARSE_LOCATION,
        Manifest.permission.ACCESS_FINE_LOCATION
})
void showRequestPermission(PermissionRequest request) { 
        
    new AlertDialog.Builder(getHostActivity())
            .setMessage(R.string.permission_hint)
            .setPositiveButton(R.string.allow, (dialog, which) -> request.proceed())
            .setNegativeButton(R.string.deny, (dialog, which) -> request.cancel()).show();
}

/** * 拒绝了权限调用 */
@OnPermissionDenied({ 
        
        Manifest.permission.CAMERA,
        Manifest.permission.READ_EXTERNAL_STORAGE,
        Manifest.permission.WRITE_EXTERNAL_STORAGE,
        Manifest.permission.ACCESS_COARSE_LOCATION,
        Manifest.permission.ACCESS_FINE_LOCATION
})
void showDenied() { 
        
    //退出应用
    finish();
}

/** * 再次获取权限的提示 */
@OnNeverAskAgain({ 
        
        Manifest.permission.CAMERA,
        Manifest.permission.READ_EXTERNAL_STORAGE,
        Manifest.permission.WRITE_EXTERNAL_STORAGE,
        Manifest.permission.ACCESS_COARSE_LOCATION,
        Manifest.permission.ACCESS_FINE_LOCATION
})
void showNeverAsk() { 
        
    //继续请求权限
    checkPermission();
}


/** * 授权后回调 * * @param requestCode * @param permissions * @param grantResults */
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { 
        
    super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    //将授权结果传递到框架
    SplashActivityPermissionsDispatcher.onRequestPermissionsResult(this, requestCode, grantResults);
}

9.引导界面


引导界面比较简单,就是多个图片可以左右滚动,整体使用ViewPager+Fragment实现,也可以使用ViewPager2,后面有讲解。

/** * 引导界面适配器 */
public class GuideAdapter extends BaseFragmentStatePagerAdapter<Integer> { 
        

    /*** * @param context 上下文 * @param fm Fragment管理器 */
    public GuideAdapter(Context context, @NonNull FragmentManager fm) { 
        
        super(context, fm);
    }

    /** * 返回当前位置Fragment * * @param position * @return */
    @NonNull
    @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.广告界面

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

相关文章