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

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 第二版基于第一版新增了以下功能: 1. 音乐下载 2. 音乐离线播放 3. mLog视频播放 4. 个人信息 5. 音乐信息 6. 删除本地音乐

效果视频

效果视频地址

前言

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

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

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

音乐下载

音乐下载效果图

e7f7e44319a04f128ac5173f2a97b85d.jpeg
3fb7f37e800d494489a0f52ae0e79af2.png
318587d1b95946d1be8478db2a5ad02b.jpeg

实习步骤&思想

  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视频播放效果图

f2e5f1fe0811400b8516c2a38ba4018c.jpeg#pic_center

样式

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

进度、声音、亮度

8f106ea7732d4b61912d0eb37aa76885.jpeg
0a9904694e584e9790376c6b2dfb01c3.jpeg
56a154a3a3bc4c849631f64ace8d869c.jpeg

声音

2de266a7ea064a919c996e43cc9f9791.png#pic_center
亮度、声音、进度类似,就以声音为例;下列代码只是显示了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);
        }
    }

个人信息

个人信息效果图

63aa174e233e4860b2340d47f36bbe60.jpeg#pic_center

行政代码解析

个人信息界面就是普通的数据展示,此处讲解一下地区,因为接口返回的是省和市的行政代码,然后找了一个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
    }
}
相关文章
|
1月前
|
XML Java 数据库
安卓项目:app注册/登录界面设计
本文介绍了如何设计一个Android应用的注册/登录界面,包括布局文件的创建、登录和注册逻辑的实现,以及运行效果的展示。
147 0
安卓项目:app注册/登录界面设计
|
2月前
|
Java 数据库 Android开发
一个Android App最少有几个线程?实现多线程的方式有哪些?
本文介绍了Android多线程编程的重要性及其实现方法,涵盖了基本概念、常见线程类型(如主线程、工作线程)以及多种多线程实现方式(如`Thread`、`HandlerThread`、`Executors`、Kotlin协程等)。通过合理的多线程管理,可大幅提升应用性能和用户体验。
127 15
一个Android App最少有几个线程?实现多线程的方式有哪些?
|
2月前
|
存储 开发工具 Android开发
使用.NET MAUI开发第一个安卓APP
【9月更文挑战第24天】使用.NET MAUI开发首个安卓APP需完成以下步骤:首先,安装Visual Studio 2022并勾选“.NET Multi-platform App UI development”工作负载;接着,安装Android SDK。然后,创建新项目时选择“.NET Multi-platform App (MAUI)”模板,并仅针对Android平台进行配置。了解项目结构,包括`.csproj`配置文件、`Properties`配置文件夹、平台特定代码及共享代码等。
165 2
|
2月前
|
XML Android开发 数据格式
🌐Android国际化与本地化全攻略!让你的App走遍全球无障碍!🌍
在全球化背景下,实现Android应用的国际化与本地化至关重要。本文以一款旅游指南App为例,详细介绍如何通过资源文件拆分与命名、适配布局与方向、处理日期时间及货币格式、考虑文化习俗等步骤,完成多语言支持和本地化调整。通过邀请用户测试并收集反馈,确保应用能无缝融入不同市场,提升用户体验与满意度。
103 3
|
2月前
|
Java 数据库 Android开发
一个Android App最少有几个线程?实现多线程的方式有哪些?
本文介绍了Android应用开发中的多线程编程,涵盖基本概念、常见实现方式及最佳实践。主要内容包括主线程与工作线程的作用、多线程的多种实现方法(如 `Thread`、`HandlerThread`、`Executors` 和 Kotlin 协程),以及如何避免内存泄漏和合理使用线程池。通过有效的多线程管理,可以显著提升应用性能和用户体验。
72 10
|
1月前
|
安全 网络安全 Android开发
深度解析:利用Universal Links与Android App Links实现无缝网页至应用跳转的安全考量
【10月更文挑战第2天】在移动互联网时代,用户经常需要从网页无缝跳转到移动应用中。这种跳转不仅需要提供流畅的用户体验,还要确保安全性。本文将深入探讨如何利用Universal Links(仅限于iOS)和Android App Links技术实现这一目标,并分析其安全性。
230 0
|
2月前
|
XML 数据库 Android开发
10分钟手把手教你用Android手撸一个简易的个人记账App
该文章提供了使用Android Studio从零开始创建一个简单的个人记账应用的详细步骤,包括项目搭建、界面设计、数据库处理及各功能模块的实现方法。
|
3月前
|
API Android开发
Android P 性能优化:创建APP进程白名单,杀死白名单之外的进程
本文介绍了在Android P系统中通过创建应用进程白名单并杀死白名单之外的进程来优化性能的方法,包括设置权限、获取运行中的APP列表、配置白名单以及在应用启动时杀死非白名单进程的代码实现。
64 1
|
3月前
|
Android开发 iOS开发 C#
Xamarin:用C#打造跨平台移动应用的终极利器——从零开始构建你的第一个iOS与Android通用App,体验前所未有的高效与便捷开发之旅
【8月更文挑战第31天】Xamarin 是一个强大的框架,允许开发者使用单一的 C# 代码库构建高性能的原生移动应用,支持 iOS、Android 和 Windows 平台。作为微软的一部分,Xamarin 充分利用了 .NET 框架的强大功能,提供了丰富的 API 和工具集,简化了跨平台移动应用开发。本文通过一个简单的示例应用介绍了如何使用 Xamarin.Forms 快速创建跨平台应用,包括设置开发环境、定义用户界面和实现按钮点击事件处理逻辑。这个示例展示了 Xamarin.Forms 的基本功能,帮助开发者提高开发效率并实现一致的用户体验。
153 0
|
3月前
|
存储 XML Linux
深入理解操作系统:进程管理与调度策略探索安卓应用开发:从零开始构建你的第一个App
【8月更文挑战第28天】在数字世界里航行,操作系统是掌控一切的舵手。本文将带你领略操作系统的精妙设计,特别是进程管理和调度策略这两大核心领域。我们将从基础概念出发,逐步深入到复杂的实现机制,最后通过实际代码示例,揭示操作系统如何高效协调资源,确保多任务顺畅运行的秘密。准备好了吗?让我们启航,探索那些隐藏在日常电脑使用背后的奥秘。 【8月更文挑战第28天】在这个数字时代,拥有一款自己的移动应用程序不仅是技术的展示,也是实现创意和解决问题的一种方式。本文将引导初学者了解安卓开发的基础知识,通过一个简单的待办事项列表App项目,逐步介绍如何利用安卓开发工具和语言来创建、测试并发布一个基本的安卓应用

热门文章

最新文章

下一篇
无影云桌面