当前位置: 首页 > news >正文

Android——一个简单的音乐APP(二)

一个简单的音乐APP

  • 效果视频
    • 前言
    • 音乐下载
      • 音乐下载效果图
      • 实习步骤&思想
      • 添加到下载队列
        • 单任务下载
        • 多任务下载
      • 音乐下载
        • 获取音乐下载源
        • 创建本地路径
          • 创建目录
        • 开始音乐下载
        • 下载进度回调
          • 下载进度更新
          • 寻找当前下载项
      • 离线播放
      • mLog视频播放
        • mLog视频播放效果图
        • 样式
          • 进度、声音、亮度
            • 声音
      • 个人信息
        • 个人信息效果图
        • 行政代码解析
          • XML数据格式
          • 代码

效果视频

一个简单的音乐APP

前言

一个简单的音乐APP(第一篇)
第二版基于第一版新增了以下功能:

  1. 音乐下载
  2. 音乐离线播放
  3. mLog视频播放
  4. 个人信息
  5. 音乐信息
  6. 删除本地音乐

基于第一版也稍稍有些变动,废弃了二维码登录功能(依旧能登录,但有少部分功能无法联动使用),部分代码结构有变动

音乐下载

音乐下载效果图

实习步骤&思想

  1. 将所有选中的下载项与数据库中的子项进行匹配,如果存在相同的则不添加到下载队列中
  2. 下载队列采用先入先出,如果某一个音乐获取的音乐源为空,则代表需要会员或者需要购买才能获取下载链接(因为我的账号为非会员,故需要会员下载的歌曲无法进行下载,有会员的账户可以正常获取下载链接);如果为获取链接为空,则从下载队列之中删除此项,以及删除数据库对应项、本地项
  3. 正常获取的音乐下载链接,则通过FileDownloader开源库进行下载,然后通过EventBus跨模块进行下载进度实时更新
  4. 断点续传:如果有未下载完成的下载项,被强制退出APP,当下次重新进去app时,自动断电续传,完成下载

添加到下载队列

下载选中以及下载弹窗提醒就跳过,先讲解添加到下载队列;先取出数据库实例,如果存在相同项则不添加到下载队列中,反之添加;此数据库以歌曲id歌曲名称作为主键,防止重复

private fun isExistIdentity(bean: SongBean): Boolean{
        val dao = PlayListDataBase.getDBInstance().downloadDao()
        val key = "${bean.songId}${bean.songName}"
        val isExist: DownloadBean = dao.findByKey(key)
        /**
         * 如果不为空,则代表存在,则不再进行重复下载*/
        if (isExist != null) return true

        val downloadBean = DownloadBean(key,bean.songName,bean.songId,bean.singerName,"",bean.albumCover,"","",0,0,false,false,"")

        /*update room*/
        dao.insert(downloadBean)

        /*update binder*/
        DownloadBinder.downloadList.add(downloadBean)
        return false
    }

单任务下载

          val flag = isExistIdentity(bean)
                if (flag){
                    ToastUtil.setFailToast(this@SongListActivity,"已存在相同下载项,请勿重复添加!")
                }else{
                    DownloadBinder.download()
                    ToastUtil.setSuccessToast(this@SongListActivity,"已添加到下载队列中!")
                }

多任务下载

          var count = 0
            for (i in songBeanList.indices){
                if (songBeanList[i].isSelect){
                    var flag = isExistIdentity(songBeanList[i])
                    if (flag)count++
                }
            }
            if (count> 0)ToastUtil.setFailToast(this@SongListActivity,"部分音乐可能已经存在下载队列之后,请勿重复添加!")
            DownloadBinder.download()
            downloadPop.dismiss()
        }

音乐下载

下载class为一个Service服务的单例Binder实现类

获取音乐下载源

通过音乐id获取其下载链接,然后回调给调用放,如上述表示所言,非会员账号,下载部分歌曲无法获取音乐下载源,故需要进行字符判断

 /**
     * 获取音乐下载源*/
    private fun startDownload(bean: DownloadBean,callback: IGenerallyInfo) {
        val url = HttpUtil.getDownloadURL(bean.songId.toString())

        HttpUtil.getGenerallyInfo(url, object : IGenerallyInfo {
            override fun onRespond(json: String?) {
                val downloadUrl = JSONObject(json.toString()).getJSONObject("data").getString("url").toString()
                if (!TextUtils.isEmpty(downloadUrl) && downloadUrl != "null") {
                    callback.onRespond(downloadUrl)
                } else {
                    callback.onFailed("此音乐需要开通才能进行下载!")
                }
            }

            override fun onFailed(e: String?) {
                callback.onFailed(e.toString())
            }
        })
    }

创建本地路径

对下载队列进行大小判断,防止下标越界异常,然后通过创建本地文件,对正常下载源进行下载,非正常音乐源进行删除(下载队列删除、数据库删除)

fun download() {
        if (downloadList.size == 0) return

        if (current >= downloadList.size) {
            ToastUtil.setSuccessToast(HomePageActivity.MA, "下载完成!")
            current = 0
            downloadList.clear()
            return
        }

        val name = downloadList[current].songId.toString() + downloadList[current].songName + ".mp3"
        val path = locationDir + File.separator + name
        downloadList[current].path = path

        startDownload(downloadList[current],object :IGenerallyInfo{
            override fun onRespond(json: String?) {
                downloadList[current].url = json.toString()
                bindDownload(path, downloadList[current].url)
            }

            override fun onFailed(e: String?) {
                delete(downloadList[current])
                EventBus.getDefault().postSticky(DownloadingBean(111, "", percentage, e.toString(), null))
                download()
            }
        })
    }
创建目录
public String mainCatalogue() {
        String path = "EasyMusicDownload";
        File dir = new File(BaseApplication.getContext().getCacheDir(), path);
        File movie = BaseApplication.getContext().getExternalFilesDir(Environment.DIRECTORY_MUSIC);
        if (movie != null) {
            dir = new File(movie, path);
        }
        if (!dir.exists()) {
            dir.mkdirs();
        }
        return dir.toString();
    }

开始音乐下载

在不同的回调中进行不同的处理,通过EventBus进行下载进度跨模块更新,其中需要关注的只有progresscompleted两个回调,前者需要不断进行下载进度回调,后面在下载完成之后需要改变下载项,并且更新数据库相对应实例,方便下载完成显示界面正常;而warn回调则无需进行太多关注,因为我们添加到下载队列之前已经判断过是否存在重复项啦,在代码正常情况下,无需担心

private fun bindDownload(path:String,url:String){
        FileDownloader.getImpl()
            .create(url)
            .setPath(path, false)
            .setListener(object : FileDownloadListener() {
                override fun pending(task: BaseDownloadTask, soFarBytes: Int, totalBytes: Int) {
                    //已经进入下载队列,正在等待下载
                    EventBus.getDefault().postSticky(DownloadingBean(task.status, "", 0, "", null))
                }

                override fun started(task: BaseDownloadTask?) {
                    //结束了pending,并且开始当前任务的Runnable
                    if (task != null) {
                        EventBus.getDefault().postSticky(DownloadingBean(task.status, "", 0, "", null))
                    }
                }

                override fun connected(task: BaseDownloadTask?, etag: String?, isContinue: Boolean, soFarBytes: Int, totalBytes: Int) {
                    //已经连接上
                }

                override fun progress(task: BaseDownloadTask, soFarBytes: Int, totalBytes: Int) {
                    //soFarBytes:已下载字节数
                    //totalBytes:文件总字节数
                    Log.d("downloadTAG", "running:${task.speed}")
                    percentage = ((soFarBytes * 1.0 / totalBytes) * 100).toInt()
                    EventBus.getDefault().postSticky(DownloadingBean(task.status, "${remainDigit(task.speed / 8.0)}KB/s", percentage, "", downloadList[current]))
                }

                override fun completed(task: BaseDownloadTask) {
                    //status = -3
//                /**
//                 * 除2个1024的到大小MB
//                 * 记得最后保留一位小数*/
                    val primary = "${downloadList[current].songId}${downloadList[current].songName}"
//
//                /**
//                 * 下载完成之后,更新数据库字段*/
                    PlayListDataBase.getDBInstance().downloadDao().updateComplete(primary, true)
                    PlayListDataBase.getDBInstance().downloadDao().updatePath(primary, task.path)
                    PlayListDataBase.getDBInstance().downloadDao().updateUrl(primary, task.url)
                    val size = remainDigit(task.smallFileTotalBytes * 1.0 / 1024 / 1024)
                    PlayListDataBase.getDBInstance().downloadDao().updateSize(primary, "${size}MB")
                    EventBus.getDefault().postSticky(DownloadingBean(task.status, "", percentage, "", downloadList[current]))

                    current++
                    download()
                }

                override fun paused(task: BaseDownloadTask, soFarBytes: Int, totalBytes: Int) {
                    //callback.pause(task)
                    Log.d("downloadTAG", "pause:${task.status}")
                    EventBus.getDefault().postSticky(DownloadingBean(task.status, "", percentage, "", downloadList[current]))
                }

                override fun error(task: BaseDownloadTask, e: Throwable) {
                    // callback.failed(task,e.message)
                    //error = -1
                    Log.d("downloadTAG", "failed:${task.status}")
                    Log.d("downloadTAG", "failed:${e.message}")
                    EventBus.getDefault().postSticky(DownloadingBean(task.status, "", percentage, e.message!!, downloadList[current]))

                    download()
                }

                /**
                 * 存在相同项目*/
                override fun warn(task: BaseDownloadTask) {
                    // callback.exist(task)
                    /**
                     * 不会进入此处
                     * 因为外面已经判断过重复项*/
                    Log.d("downloadTAG", "exist:${task.status}")
                    EventBus.getDefault().postSticky(DownloadingBean(111, "", percentage, "", downloadList[current]))

                    //current++
                    download()
                }
            }).start()
    }

下载进度回调

 /**
     * 下载回调监听*/
    @Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
    fun onEvent(bean: DownloadingBean){
        update(bean)
    }
下载进度更新

下载进度总共有四种状态:默认状态(灰色)、绿色(当前下载项)、黄色(下载暂停)、红色(下载异常);为了提醒四种状态,就没有贴代码,而是放了图片,因为旁边有颜色提示,更加明了
在这里插入图片描述
对应上面的状态进行不同的处理

    /**
     * currentItem等于-1代表没有相同项
     * currentItem等于-2代表状态改变,比如下载完成,初始化-2*/
    private fun update(bean: DownloadingBean){
        when(bean.status){
            pending_start->{}
            running->{
                updateItem(bean,1)
            }
            completed->{
                if (currentItem != -1 && currentItem != -2){
                    adapter.deleteCompletedItem(currentItem)
                    currentItem = -2
                    EventBus.getDefault().postSticky(DownloadCompleteBean(true))
                }
            }
            failed->{updateItem(bean,3)}
            error->{
                ToastUtil.setFailToast(HomePageActivity.MA,bean.error)
            }
        }
    }
寻找当前下载项

-2(默认)下载完成,需要重新寻找下载项,-1代表下载队列已经全部下载完成,已经没有匹配项;反之,为当前下载项,进行进度更新

    private fun updateItem(bean: DownloadingBean,status: Int){
        when (currentItem) {
            -2 -> {
                searchItem(bean)
            }
            -1 -> {
                //没有相同项
            }
            else -> {
                downloadList[currentItem].progress = bean.percentage
                downloadList[currentItem].speed = bean.speed
                adapter.notifyItemChanged(currentItem,status)
            }
        }
    }

离线播放

离线播放较为简单,因为之前在线播放使用的都是同一个组件,在线播放传入音乐url,而离线播放传入本地音乐的绝对地址即可,故而没有任何变化。

mLog视频播放

mLog视频播放效果图

样式

原理与MV视频播放一致,只是界面发生些许变化;因为重写了GSYVideoPlayer的样式,此处就介绍一下样式。

进度、声音、亮度
声音
亮度、声音、进度类似,就以声音为例;下列代码只是显示了`Dialog`以及相关属性,具体通过滑动屏幕计算音量大小,然后改变系统音量大小由下列代码实现(部分代码)
           deltaY = -deltaY;
            int max = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
            int deltaV = (int) (max * deltaY * 3 / curHeight);
            mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, mGestureDownVolume + deltaV, 0);
            int volumePercent = (int) (mGestureDownVolume * 100 / max + deltaY * 3 * 100 / curHeight);
            showVolumeDialog(-deltaY, volumePercent);
@Override
    protected void showVolumeDialog(float deltaY, int volumePercent) {
        WindowManager.LayoutParams localLayoutParams = null;
        if (mVolumeDialog == null) {
            View localView = LayoutInflater.from(getActivityContext()).inflate(getVolumeLayoutId(), null);
            if (localView.findViewById(getVolumeProgressId()) instanceof ProgressBar) {
                mDialogVolumeProgressBar = ((ProgressBar) localView.findViewById(getVolumeProgressId()));
                if (mVolumeProgressDrawable != null && mDialogVolumeProgressBar != null) {
                    mDialogVolumeProgressBar.setProgressDrawable(mVolumeProgressDrawable);
                }
            }
            mVolumeDialog = new Dialog(getActivityContext(), R.style.video_style_dialog_progress);
            //mVolumeDialog = new Dialog(getActivityContext());
            mVolumeDialog.setContentView(localView);
            mVolumeDialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
            mVolumeDialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL);
            mVolumeDialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE);
            //mVolumeDialog.getWindow().setLayout(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
            mVolumeDialog.getWindow().setLayout(380, 50);
            localLayoutParams = mVolumeDialog.getWindow().getAttributes();
            localLayoutParams.gravity = Gravity.CENTER;
            localLayoutParams.width = getWidth();
            localLayoutParams.height = getHeight();
        }
        if (!mVolumeDialog.isShowing()) {
            mVolumeDialog.show();
            mVolumeDialog.getWindow().setAttributes(localLayoutParams);
        }
        if (mDialogVolumeProgressBar != null) {
            mDialogVolumeProgressBar.setProgress(volumePercent);
        }
    }

个人信息

个人信息效果图

行政代码解析

个人信息界面就是普通的数据展示,此处讲解一下地区,因为接口返回的是省和市的行政代码,然后找了一个XML 全国省市区+区行政代码的文件,通过PULL方式进行解析,然后展示,此处讲解这个

XML数据格式

以安徽省-安庆市为例,此文件没有Text内容,所有信息全在标头上

<province name="安徽省" zipcode="340000">
    <city name="安庆市" zipcode="340800">
      <district name="枞阳县" zipcode="340800" />
      <district name="大观区" zipcode="340800" />
      <district name="怀宁县" zipcode="340800" />
      <district name="潜山县" zipcode="340800" />
      <district name="宿松县" zipcode="340800" />
      <district name="太湖县" zipcode="340800" />
      <district name="桐城市" zipcode="340800" />
      <district name="望江县" zipcode="340800" />
      <district name="宜秀区" zipcode="340800" />
      <district name="迎江区" zipcode="340800" />
      <district name="岳西县" zipcode="340800" />
      <district name="其他" zipcode="340800" />
    </city>
    </province>
代码

这是为解析这个文件写的一个解析类,首先获取这个资源文件,这个文件可以存储在xml目录下、raw目录下、assets目录下都可以;前者与后两者有稍微不同,但解析步骤一样,只是获取资源文件方式不同。我这个采用的是一个嵌套类,

  • 省数据类中包括:省名称、省行政代码、城市集合
  • 城市数据类中包括:城市名称、城市行政代码、区县集合
  • 区县数据类中包括:区县名称、区县行政代码(区县在该文件中没有行政代码,所有代码均为该市行政代码)
object XMLParserUtil {
    private val provinceTag = "province"
    private val cityTag = "city"
    private val countyTag = "district"
    private val NAME = "name"
    private val CODE = "zipcode"
    private lateinit var pullParser : XmlPullParser
    private lateinit var provinceList:MutableList<ProvinceBean>

    init {
        provinceList = ArrayList<ProvinceBean>()
        create()
        resolve()
    }


    private fun create(){
        val factory = XmlPullParserFactory.newInstance();
        pullParser = factory.newPullParser();
        val inputStream = BaseApplication.context.resources.openRawResource(R.raw.region);
        pullParser.setInput(InputStreamReader(inputStream));
    }
    
    private fun resolve(){
        var province: ProvinceBean = ProvinceBean()
        var currencyProvince = false
        var currentCityFlag = false
        var currentCity: Int = -1
        var currentCounty: Int = -1
        try {
        var type = pullParser.eventType
        while (type != XmlPullParser.END_DOCUMENT){
            when(type){
                XmlPullParser.START_DOCUMENT-> provinceList = ArrayList<ProvinceBean>()

                XmlPullParser.START_TAG->{
                    when (pullParser.name) {
                        provinceTag -> {
                            /**
                             * 省份*/
                            province = ProvinceBean()
                            currencyProvince = true

                            province.provinceName = pullParser.getAttributeValue(null, NAME)
                            province.provinceCode = pullParser.getAttributeValue(null, CODE).toInt()
                        }
                        cityTag -> {
                            /**
                             * 城市*/
                            if (currencyProvince){
                                currentCity = -1
                                currencyProvince = false
                                province.cityList = ArrayList<CityBean>()
                            }
                            currentCityFlag = true
                            currentCity++
                            val bean = CityBean()
                            bean.cityName =  pullParser.getAttributeValue(null, NAME)
                            bean.cityCode = pullParser.getAttributeValue(null, CODE).toInt()
                            province.cityList.add(bean)
                        }
                        countyTag -> {
                            /**
                             * 区县*/
                            if (currentCityFlag){
                                currentCounty = -1
                                currentCityFlag = false
                                province.cityList[currentCity].countyList = ArrayList<CountyBean>()
                            }
                            currentCounty++
                            val bean = CountyBean()
                            bean.countyName = pullParser.getAttributeValue(null, NAME)
                            bean.countyCode = pullParser.getAttributeValue(null, CODE).toInt()
                            province.cityList[currentCity].countyList.add(bean)
                        }
                    }
                }

                XmlPullParser.END_TAG->{
                    val provinceName = pullParser.name
                    if (pullParser.name == provinceTag) provinceList.add(province)
                }
                XmlPullParser.END_DOCUMENT->{}
            }
            type = pullParser.next()
        }
        }catch (e: XmlPullParserException){
            e.printStackTrace()
        }catch (e:IOException){
            e.printStackTrace()
        }catch (e:NullPointerException){
            e.printStackTrace()
        }
    }

    /**
     * 根据省/城市名获取行政代码*/
    private fun getCodeByName(){

    }

    /**
     * 查询省级*/
    fun getProvinceNameByCode_B(code: Int):ProvinceBean?{
        val province = searchProvince(code)
        province?.let { return province }
        return null
    }

    fun getProvinceNameByCode_S(code: Int):String?{
        val province = searchProvince(code)
        province?.let { province.provinceName }
        return null
    }

    /**
     * 根据行政代码获取省/城市名
     * 模糊搜索:传入省或者城市编码,具体类型不明
     * 查找某个城市的名称,首先通过找到该省然后在进一步通过城市行政代码查询该市名称*/

    /**
     * 返回组合类型
     * 例如:湖南-长沙*/
    fun getCityNameByCode(provinceCode: Int,cityCode: Int,split: String): String{
        val builder = StringBuilder()
        val province = searchProvince(provinceCode)
        if (province != null){
            builder.append(province.provinceName).append(split)
            val city = searchCity(cityCode, province)
            city?.let {
                builder.append(city.cityName)
                return builder.toString()
            }
        }
        return ""
    }

    /**
     * 单独返回城市名称*/
    fun getCityNameByCode(province:ProvinceBean,cityCode: Int): String{
        val city = searchCity(cityCode, province)
        city?.let { return city.cityName }
        return ""
    }

    private fun searchProvince(code: Int): ProvinceBean?{
        if (provinceList.size == 0)return null
        for (i in 0 until provinceList.size){
            if (code == provinceList[i].provinceCode) return provinceList[i]
        }
        return null
    }

    private fun searchCity(code: Int,bean: ProvinceBean):CityBean?{
        if (bean.cityList.size == 0) return null
        for (i in 0 until bean.cityList.size){
            if (code == bean.cityList[i].cityCode) return bean.cityList[i]
        }
        return null
    }
}

相关文章:

  • 在Eclipse 中使用 Maven 创建雅加达 EE 应用程序
  • 112-JavaSE基础进阶:XML的创建、文档约束、文件的解析技术-Dom4J、解析案例、文件的数据检索技术-XPath
  • 计算机网络 第3 章 数据链路层
  • 如何让不给听得ge乖乖听话?python教你如何做...
  • C# Winform跨线程更新UI控件的方法
  • Linux学习 -- shell工具的复习(cut/sed/awk/sort)
  • C语言百日刷题第四天
  • C生万物 | 初识C语言【1024,从0开始】
  • 计算机网络-物理层(数据交换方式(电报交换,报文交换,分组交换),数据报,虚电路,传输介质,物理层设备(中继器,集线器))
  • 【趣学算法】第二章 算法之美(下)
  • 对话PPIO边缘云联合创始人王闻宇,聊聊边缘计算与元宇宙
  • 原生App-云打包
  • RK3399快速上手 | 03-RK3399启动流程分析
  • Opencv实战项目:13 手部追踪
  • 空间滤波-几何均值滤波器
  • 【node学习】协程
  • 【技术性】Search知识
  • ES6系统学习----从Apollo Client看解构赋值
  • k8s如何管理Pod
  • Laravel Telescope:优雅的应用调试工具
  • Material Design
  • Redis学习笔记 - pipline(流水线、管道)
  • SpiderData 2019年2月13日 DApp数据排行榜
  • V4L2视频输入框架概述
  • 入门级的git使用指北
  • 使用权重正则化较少模型过拟合
  • 它承受着该等级不该有的简单, leetcode 564 寻找最近的回文数
  • 通过git安装npm私有模块
  • 一天一个设计模式之JS实现——适配器模式
  • 最简单的无缝轮播
  • ​你们这样子,耽误我的工作进度怎么办?
  • ​如何防止网络攻击?
  • #绘制圆心_R语言——绘制一个诚意满满的圆 祝你2021圆圆满满
  • #微信小程序:微信小程序常见的配置传值
  • (04)odoo视图操作
  • (1)Nginx简介和安装教程
  • (4)logging(日志模块)
  • (Redis使用系列) Springboot 在redis中使用BloomFilter布隆过滤器机制 六
  • (求助)用傲游上csdn博客时标签栏和网址栏一直显示袁萌 的头像
  • (转)自己动手搭建Nginx+memcache+xdebug+php运行环境绿色版 For windows版
  • (轉)JSON.stringify 语法实例讲解
  • *1 计算机基础和操作系统基础及几大协议
  • .Net - 类的介绍
  • .NET 4.0中使用内存映射文件实现进程通讯
  • .NET Core中的去虚
  • .NET Framework .NET Core与 .NET 的区别
  • .Net mvc总结
  • .NET/C# 使用反射注册事件
  • .NetCore Flurl.Http 升级到4.0后 https 无法建立SSL连接
  • .Net高阶异常处理第二篇~~ dump进阶之MiniDumpWriter
  • .NET连接数据库方式
  • @ConditionalOnProperty注解使用说明
  • @javax.ws.rs Webservice注解
  • @RestControllerAdvice异常统一处理类失效原因
  • [ Linux ] git工具的基本使用(仓库的构建,提交)