Android实现断点多线程下载
程序员文章站
2022-07-04 09:46:45
断点多线程下载的几个关键点:①:得到要下载的文件大小后,均分给几个线程。②:使用randomaccessfile类进行读写,可以指定开始写入的位置。③:数据库保存下载信息,...
断点多线程下载的几个关键点:①:得到要下载的文件大小后,均分给几个线程。②:使用randomaccessfile类进行读写,可以指定开始写入的位置。③:数据库保存下载信息,下一次继续下载的时候从数据库取出数据,然后从上次下载结束的地方开始。
这里我使用了finaldb的数据库框架,同时在内存中存储了一份所有线程的下载信息,负责时时更新和查询下载进度。我测试用的是百度云网盘,文件大小50m左右。注意,线程中指定了开始结束下载位置的网络请求成功的返回码是206,并不是200。
效果图:
线程类:线程类负责具体的下载,线程的下载信息被存储到了数据库。线程开始下载时,根据线程id查询自己的存储信息,然后开始从指定的位置下载和写入文件。完毕后根据自己的当前下载结果设置自己当前的下载状态。时时的下载进度存储只存储到了内存,只在本次下载结束才存储到数据库。
public class downloadthread extends thread { /** * 数据库操作工具 */ private finaldb finaldb; /** * 下载状态:未开始 */ public static final int state_ready = 1; /** * 下载状态:下载中 */ public static final int state_loading = 2; /** * 下载状态:下载暂停中 */ public static final int state_pausing = 3; /** * 下载状态:下载完成 */ public static final int state_finish = 4; /** * 下载状态 */ public int downloadstate; /** * 线程id */ private int threadid; /** * 要下载的url路径 */ private string url; /** * 本线程要下载的文件 */ public randomaccessfile file; /** * 构造器 */ public downloadthread(context context, int threadid, string downloadurl, randomaccessfile randomaccessfile) { this.threadid = threadid; this.url = downloadurl; this.file = randomaccessfile; finaldb = dbutil.getfinaldb(context); } @override public void run() { //数据库查询本线程下载进度 list<threaddownloadinfobean> list = finaldb.findallbywhere(threaddownloadinfobean.class, "threadid='" + threadid + "'"); //下载信息存放到内存 if (list.get(0) != null) { maputil.map.put(threadid, list.get(0)); } //取出实体类 threaddownloadinfobean bean = maputil.map.get(threadid); utils.print("bean:" + bean.tostring()); inputstream is; httpurlconnection conn; try { utils.print("线程" + threadid + "开始连接"); conn = (httpurlconnection) new url(url).openconnection(); conn.setconnecttimeout(5000); conn.setreadtimeout(5000); conn.setrequestmethod("get"); //设置下载开始和结束的位置 conn.setrequestproperty("range", "bytes=" + (bean.startdownloadposition + bean.downloadedsize) + "-" + bean.enddownloadposition); conn.connect(); if (conn.getresponsecode() == 206) { //更改下载状态 downloadstate = state_loading; bean.downloadstate = state_loading; utils.print("线程" + threadid + "连接成功"); is = conn.getinputstream(); // 1k的数据缓冲 byte[] bs = new byte[1024]; // 读取到的数据长度 int len; //从指定的位置开始下载 file.seek(bean.startdownloadposition); // 循环读取,当已经下载的大小达到了指定的本线程负责的大小时跳出循环,线程之间负责的文件首尾有重合的话没有影响,因为写入的内容时相同的 while ((len = is.read(bs)) != -1) { //不用在这个循环里面更新数据库 file.write(bs, 0, len); //时时更新内存中的已下载大小信息 bean.downloadedsize += len; //如果调用者暂停下载,则跳出结束方法 if (downloadstate == state_pausing) { utils.print("线程" + threadid + "暂停下载"); break; } } is.close(); file.close(); } else { utils.print("线程" + threadid + "连接失败"); } conn.disconnect(); //如果这个线程已经下载完了自己负责的部分就修改下载状态 if (bean.downloadedsize >= bean.downloadtotalsize) { bean.downloadstate = state_finish; } else { bean.downloadstate = state_pausing; } //内存中信息更新至数据库 finaldb.update(bean, "threadid='" + bean.threadid + "'"); } catch (ioexception e) { utils.print("线程" + threadid + "io异常"); e.printstacktrace(); } } }
线程信息的封装类:负责存储每个线程的id,开始下载的位置,结束下载的位置,已经下载的大小,下载状态等;这个类用finaldb数据库存储到数据库,一定要写get,set方法和空参构造器
@table(name = "threadinfo") public class threaddownloadinfobean { /** * id */ public int id; /** * 线程id */ public int threadid; /** * 本线程时时下载开始的位置 */ public long startdownloadposition; /** * 本线程时时下载结束的位置 */ public long enddownloadposition; /** * 本线程负责下载的文件大小 */ public long downloadtotalsize; /** * 已经下载了的文件大小 */ public long downloadedsize; /** * 本线程的下载状态 */ public int downloadstate; public threaddownloadinfobean() { } public threaddownloadinfobean(int downloadstate, long downloadedsize, long downloadtotalsize, long enddownloadposition, long startdownloadposition, int threadid) { this.downloadstate = downloadstate; this.downloadedsize = downloadedsize; this.downloadtotalsize = downloadtotalsize; this.enddownloadposition = enddownloadposition; this.startdownloadposition = startdownloadposition; this.threadid = threadid; } public int getid() { return id; } public void setid(int id) { this.id = id; } public int getthreadid() { return threadid; } public void setthreadid(int threadid) { this.threadid = threadid; } public long getstartdownloadposition() { return startdownloadposition; } public void setstartdownloadposition(long startdownloadposition) { this.startdownloadposition = startdownloadposition; } public long getenddownloadposition() { return enddownloadposition; } public void setenddownloadposition(long enddownloadposition) { this.enddownloadposition = enddownloadposition; } public long getdownloadtotalsize() { return downloadtotalsize; } public void setdownloadtotalsize(long downloadtotalsize) { this.downloadtotalsize = downloadtotalsize; } public long getdownloadedsize() { return downloadedsize; } public void setdownloadedsize(long downloadedsize) { this.downloadedsize = downloadedsize; } public int getdownloadstate() { return downloadstate; } public void setdownloadstate(int downloadstate) { this.downloadstate = downloadstate; } @override public string tostring() { return "threaddownloadinfobean{" + "id=" + id + ", threadid=" + threadid + ", startdownloadposition=" + startdownloadposition + ", enddownloadposition=" + enddownloadposition + ", downloadtotalsize=" + downloadtotalsize + ", downloadedsize=" + downloadedsize + ", downloadstate=" + downloadstate + '}'; } }
下载工具类:这个类负责得到下载文件大小,分配线程下载大小,管理下载线程
public class downutil { /** * 数据库操作工具 */ public finaldb finaldb; /** * 下载状态:准备好 */ public static final int state_ready = 1; /** * 下载状态:下载中 */ public static final int state_loading = 2; /** * 下载状态:暂停中 */ public static final int state_pausing = 3; /** * 下载状态:下载完成 */ public static final int state_finish = 4; /** * 下载状态 */ public int downloadstate; /** * context */ private context context; /** * 要下载文件的大小 */ public long filesize; /** * 线程集合 */ private arraylist<downloadthread> threadlist = new arraylist<>(); /** * 构造器 */ public downutil(context context) { this.context = context; finaldb = dbutil.getfinaldb(context); judgedownstate(); } /** * 初始化时判断下载状态 */ public void judgedownstate() { //取出数据库中的下载信息,存储到内存中 list<threaddownloadinfobean> list = finaldb.findall(threaddownloadinfobean.class); if (list != null && list.size() == downloadactivity.threadnum) { for (int i = 0; i < list.size(); i++) { maputil.map.put(i, list.get(i)); } } //查询sp中是否存储过要下载的文件大小 long spfilesize = sputil.getinstance(context).getlong(downloadactivity.filename, 0l); long downloadedsize = getfinishedsize(); //sp中或者数据库中没有查询到说明从没有进行过下载 if (spfilesize == 0 || downloadedsize == 0) { downloadstate = state_ready; } else if (downloadedsize >= spfilesize) { downloadstate = state_finish; } else { downloadstate = state_pausing; filesize = spfilesize; } } /** * 点击了开始按钮 */ public void clickdownloadbtn() { if (downloadstate == state_ready) { startdownload(); } else if (downloadstate == state_pausing) { continuedownload(); } } /** * 进入应用第一次开始下载 */ private void startdownload() { //开启新线程,得到要下载的文件大小 new thread() { @override public void run() { try { httpurlconnection conn; conn = (httpurlconnection) new url(downloadactivity.url).openconnection(); conn.setconnecttimeout(5000); conn.setreadtimeout(5000); conn.setrequestmethod("get"); conn.connect(); if (conn.getresponsecode() == 200) { utils.print("downutil连接成功"); //得到要下载的文件大小 filesize = conn.getcontentlength(); //得到文件名后缀名 string contentdisposition = new string(conn.getheaderfield("content-disposition").getbytes("iso-8859-1"), "utf-8"); string filename = "下载测试" + contentdisposition.substring(contentdisposition.lastindexof("."), contentdisposition.lastindexof("\"")); //得到存储路径 string sdcardpath = context.getexternalfilesdir(null).getpath(); downloadactivity.filename = sdcardpath + "/" + filename; sputil.getinstance(context).savestring(downloadactivity.file_name, downloadactivity.filename); sputil.getinstance(context).savelong(downloadactivity.filename, filesize); /* * 计算一下每个线程需要分担的下载文件大小 比如 总下载量为100 一共有三个线程 * 那么 线程1负责0-32,线程2负责33-65,线程3负责66-99和100, * 也就是说下载总量除以线程数如果有余数,那么最后一个线程多下载一个余数部分 */ //每个线程均分的大小 long threaddownsize = filesize / downloadactivity.threadnum; //线程均分完毕剩余的大小 long leftdownsize = filesize % downloadactivity.threadnum; //创建要写入的文件 randomaccessfile file = new randomaccessfile(downloadactivity.filename, "rw"); //设置文件大小 file.setlength(filesize); //关闭 file.close(); for (int i = 0; i < downloadactivity.threadnum; i++) { utils.print("开启线程" + i); //指定每个线程开始下载的位置 long startposition = i * threaddownsize; //指定每个线程负责下载的大小,当现场是集合里面最后一个线程的时候,它要增加leftdownsize的大小 threaddownsize = i == downloadactivity.threadnum - 1 ? threaddownsize + leftdownsize : threaddownsize; //存储線程信息 threaddownloadinfobean bean = new threaddownloadinfobean(downloadthread.state_ready, 0, threaddownsize, startposition + threaddownsize, startposition, i); finaldb.save(bean); randomaccessfile threadfile = new randomaccessfile(downloadactivity.filename, "rw"); threadlist.add(new downloadthread(context, i, downloadactivity.url, threadfile)); threadlist.get(i).start(); } downloadstate = state_loading; downloadinfolistener.connectsuccess(); } else { utils.print("downutil-连接失败"); downloadinfolistener.connectfail(); } conn.disconnect(); } catch (ioexception e) { utils.print("downutil-io异常"); downloadinfolistener.ioexception(); e.printstacktrace(); } } }.start(); } /** * 继续下载 */ private void continuedownload() { list<threaddownloadinfobean> list = finaldb.findall(threaddownloadinfobean.class); for (int i = 0; i < downloadactivity.threadnum; i++) { //当前线程已经下载完了就不再开启 if (list.get(i).downloadstate != downloadthread.state_finish) { utils.print("重新开启线程" + i); randomaccessfile threadfile = null; try { threadfile = new randomaccessfile(downloadactivity.filename, "rw"); } catch (filenotfoundexception e) { e.printstacktrace(); } downloadthread downloadthread = new downloadthread(context, i, downloadactivity.url, threadfile); threadlist.add(downloadthread); downloadthread.start(); } } downloadstate = state_loading; downloadinfolistener.connectsuccess(); } /** * 点击了暂停的按钮 */ public void clickpausebtn() { if (downloadstate == state_loading) { stopdownload(); } } /** * 暂停下载 */ private void stopdownload() { for (int i = 0; i < threadlist.size(); i++) { if (threadlist.get(i).downloadstate == downloadthread.state_loading) { threadlist.get(i).downloadstate = downloadthread.state_pausing; } } downloadstate = state_pausing; threadlist.clear(); } /** * 返回此刻所有线程完成的下载大小 */ public long getfinishedsize() { long totalsize = 0; for (int i = 0; i < downloadactivity.threadnum; i++) { threaddownloadinfobean bean = maputil.map.get(i); if (bean != null) { //如果该线程已经下载的部分大于分配给它的部分多余部分不予计算 long addsize = bean.downloadedsize > bean.downloadtotalsize ? bean.downloadtotalsize : bean.downloadedsize; totalsize += addsize; } } return totalsize; } /** * 下载信息监听器 */ private downloadinfolistener downloadinfolistener; /** * 下载信息监听器 */ public interface downloadinfolistener { void connectsuccess(); void connectfail(); void ioexception(); } /** * 设置下载信息监听器 */ public void setdownloadinfolistener(downloadinfolistener downloadinfolistener) { this.downloadinfolistener = downloadinfolistener; } }
页面activity:负责展示下载进度等信息,提供操作页面
public class downloadactivity extends baseactivity { /** * 下载地址输入框 */ private edittext et_download_url; /** * 确定输入地址的按钮 */ private button btn_download_geturl; /** * 进度条 */ private numberprogressview np_download; /** * 开始下载的按钮 */ private button btn_download_start; /** * 暂停下载的按钮 */ private button btn_download_pause; /** * 取消下载的按钮 */ private button btn_download_cancel; /** * 文件信息显示 */ private textview tv_download_file_info; /** * 下载速度显示 */ private textview tv_download_speed; /** * 显示下载信息 */ private textview tv_download_speed_info; /** * 每隔一段时间刷新下载进度显示 */ private final static int what_increace = 1; /** * 得到了文件名称 */ private final static int what_get_filename = 2; /** * downutil连接失败 */ private final static int whai_connect_fail = 3; /** * downutilio异常 */ private final static int what_io_exception = 4; /** * 下载工具 */ private downutil downutil; /** * 需要开启的线程数量 */ public static final int threadnum = 5; /** * 存放文件路径名称的sp键名 */ public static final string file_name = "filename"; /** * 要下载的文件的url地址 */ public static string url = ""; /** * 文件下载路径和文件名称 */ public static string filename; /** * 上次统计已经完成下载的文件大小 */ private long lastfinishedsize; private handler handler = new handler() { @override public void handlemessage(message msg) { super.handlemessage(msg); switch (msg.what) { case what_increace: updateview(); break; case what_get_filename: tv_download_file_info.settext("下载路径:" + filename); break; case whai_connect_fail: tv_download_file_info.settext("连接失败"); break; case what_io_exception: tv_download_file_info.settext("io异常"); break; } } }; /** * 更新视图 */ private void updateview() { //当前已经完成下载的文件大小 long currentfinishedsize = downutil.getfinishedsize(); //要显示的下载信息的文字 stringbuilder downloadinfo = new stringbuilder(); tv_download_speed.settext("当前下载速度:" + formatesize(currentfinishedsize - lastfinishedsize) + "/s"); //本次统计比上次统计增加了,更新视图,本次统计较上次统计没有变化,不做视图更新 if (currentfinishedsize > lastfinishedsize) { lastfinishedsize = currentfinishedsize; downloadinfo.append("下载进度:" + formatesize(currentfinishedsize) + "/" + formatesize(downutil.filesize) + "\n"); for (int i = 0; i < threadnum; i++) { threaddownloadinfobean bean = maputil.map.get(i); if (bean.downloadtotalsize != 0) { downloadinfo.append("线程" + i + "下载进度:" + bean.downloadedsize * 100 / bean.downloadtotalsize + "% " + formatesize(bean.downloadedsize) + "/" + formatesize(bean.downloadtotalsize) + "\n"); } } np_download.setprogress((int) (currentfinishedsize * 100 / downutil.filesize)); tv_download_speed_info.settext(downloadinfo); //下载完成后 if (currentfinishedsize >= downutil.filesize) { tv_download_speed.settext("下载完毕"); handler.removemessages(what_increace); return; } } handler.sendemptymessagedelayed(what_increace, 1000); } /** * 返回子类要显示的布局 r.layout...... */ @override protected int childview() { return r.layout.activity_download; } /** * 强制子类实现该抽象方法,初始化各自的view */ @override protected void findchildview() { et_download_url = (edittext) findviewbyid(r.id.et_download_url); btn_download_geturl = (button) findviewbyid(r.id.btn_download_geturl); np_download = (numberprogressview) findviewbyid(r.id.np_download); btn_download_start = (button) findviewbyid(r.id.btn_download_start); btn_download_pause = (button) findviewbyid(r.id.btn_download_pause); btn_download_cancel = (button) findviewbyid(r.id.btn_download_cancel); tv_download_file_info = (textview) findviewbyid(r.id.tv_download_file_info); tv_download_speed = (textview) findviewbyid(r.id.tv_download_speed); tv_download_speed_info = (textview) findviewbyid(r.id.tv_download_speed_info); } /** * 强制子类实现该抽象方法,初始化各自数据 */ @override protected void initchilddata() { downutil = new downutil(this); lastfinishedsize = downutil.getfinishedsize(); stringbuilder downloadinfo = new stringbuilder(); filename = sputil.getinstance(this).getstring(file_name, null); if (filename != null) { downloadinfo.append("下载路径:" + filename); if (downutil.downloadstate == downutil.state_finish) { np_download.setprogress(100); } else if (downutil.downloadstate == downutil.state_pausing) { np_download.setprogress((int) (downutil.getfinishedsize() * 100 / downutil.filesize)); } tv_download_file_info.settext(downloadinfo); } } /** * 强制子类实现该抽象方法,设置自己的监听器 */ @override protected view[] setchildlistener() { downutil.setdownloadinfolistener(new downutil.downloadinfolistener() { @override public void connectsuccess() { //不能在此更新,需要到主线程刷新ui handler.sendemptymessage(what_get_filename); handler.sendemptymessage(what_increace); } @override public void connectfail() { handler.sendemptymessage(whai_connect_fail); } @override public void ioexception() { handler.sendemptymessage(what_io_exception); } }); return new view[]{btn_download_start, btn_download_pause, btn_download_geturl, btn_download_cancel}; } /** * 子类在这个方法里面实现自己view的点击监听事件的相应 * * @param v 父类传递到子类的点击的view */ @override protected void clickchildview(view v) { switch (v.getid()) { case r.id.btn_download_start: downutil.clickdownloadbtn(); break; case r.id.btn_download_pause: downutil.clickpausebtn(); handler.removemessages(what_increace); break; case r.id.btn_download_cancel: //停止发送消息 handler.removemessages(what_increace); //暂停下载 downutil.clickpausebtn(); //重置状态 downutil.downloadstate = downutil.state_ready; //统计下载的大小归零 lastfinishedsize = 0; //重置文本信息 tv_download_speed.settext(""); tv_download_file_info.settext(""); tv_download_speed_info.settext(""); //进度条归零 np_download.setprogress(0); //清空内存数据 maputil.map.clear(); //sp清理 sputil.getinstance(this).removeall(); //清空数据库 downutil.finaldb.deleteall(threaddownloadinfobean.class); //删除文件 if (filename != null) { file file = new file(filename); if (file.exists()) { boolean delete = file.delete(); if (delete) { utils.toasts(this, "删除" + filename + "成功"); } else { utils.toasts(this, "删除失败"); } } else { utils.toasts(this, "文件不存在"); } } else { utils.toasts(this, "文件不存在"); } break; case r.id.btn_download_geturl: string edittexturl = et_download_url.gettext().tostring(); if (edittexturl.trim().equals("")) { utils.toasts(this, "请输入地址"); } else { url = edittexturl; } break; } } @override protected void ondestroy() { handler.removecallbacksandmessages(null); downutil.clickpausebtn(); super.ondestroy(); } /** * 格式化数据大小 */ private string formatesize(long size) { return formatter.formatfilesize(this, size); } }
布局
<?xml version="1.0" encoding="utf-8"?> <linearlayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context="com.example.testdemo.activity.downloadactivity"> <linearlayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <edittext android:id="@+id/et_download_url" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:hint="输入下载地址"/> <button android:id="@+id/btn_download_geturl" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="确定"/> </linearlayout> <com.example.testdemo.view.numberprogressview android:id="@+id/np_download" android:layout_width="match_parent" android:layout_height="100dp"> </com.example.testdemo.view.numberprogressview> <linearlayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <button android:id="@+id/btn_download_start" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="开始下载"/> <button android:id="@+id/btn_download_pause" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="暂停下载"/> <button android:id="@+id/btn_download_cancel" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="取消下载"/> </linearlayout> <textview android:id="@+id/tv_download_file_info" android:layout_width="match_parent" android:layout_height="wrap_content"/> <textview android:id="@+id/tv_download_speed" android:layout_width="match_parent" android:layout_height="wrap_content"/> <textview android:id="@+id/tv_download_speed_info" android:layout_width="match_parent" android:layout_height="wrap_content"/> </linearlayout>
下载activity的父类
public abstract class baseactivity extends activity { /** * 点击监听器,子类也可以使用 */ protected baseonclicklistener onclicklistener = new baseonclicklistener(); /** * 在oncreate方法里面,找到视图,初始化数据,设置点击监听器 */ @override protected void oncreate(bundle savedinstancestate) { super.oncreate(savedinstancestate); setcontentview(childview()); findview(); initdata(); setlistener(); } /** * 返回子类要显示的布局 r.layout...... */ protected abstract int childview(); /** * 找到需要的视图---网址:https://www.buzzingandroid.com/tools/android-layout-finder/ */ private void findview() { findchildview(); } /** * 初始化数据 */ private void initdata() { // 初始化子类的数据 initchilddata(); } /** * 对需要设置监听 的视图设置监听 */ private void setlistener() { // 子类实现setchildlistene()方法,返回一个view数组,里面包含所有需要设置点击监听的view,然后进行for循环,对里面所有view设置监听器 if (setchildlistener() == null || setchildlistener().length == 0) { return; } view[] viewarray = setchildlistener(); for (view view : viewarray) { view.setonclicklistener(onclicklistener); } } /** * 自定义的点击监听类 */ protected class baseonclicklistener implements view.onclicklistener { @override public void onclick(view v) { clickchildview(v); } } /** * 强制子类实现该抽象方法,初始化各自的view */ protected abstract void findchildview(); /** * 强制子类实现该抽象方法,初始化各自数据 */ protected abstract void initchilddata(); /** * 强制子类实现该抽象方法,设置自己的监听器 */ protected abstract view[] setchildlistener(); /** * 子类在这个方法里面实现自己view的点击监听事件的相应 * * @param v 父类传递到子类的点击的view */ protected abstract void clickchildview(view v); }
得到数据库的操作类
public class dbutil { public static finaldb getfinaldb(final context context) { finaldb finaldb = finaldb.create(context, "remuxing.db", false, 1, new finaldb.dbupdatelistener() { @override public void onupgrade(sqlitedatabase db, int oldvirsion, int newvirsion) { } }); return finaldb; } }
内存中存储下载信息的类
public class maputil { public static hashmap<integer, threaddownloadinfobean> map = new hashmap<>(); }
sp存储的工具类
public enum sputil { sputil; public static final string notificationbar_height = "notificationbar_height"; private static sharedpreferences sp; public static sputil getinstance(context context) { if (sp == null) { sp = context.getsharedpreferences("remuxing", context.mode_private); } return sputil; } public void savelong(string key, long value) { sp.edit().putlong(key, value).apply(); } public long getlong(string key, long defvalue) { return sp.getlong(key, defvalue); } public void savestring(string key, string value) { sp.edit().putstring(key, value).apply(); } public string getstring(string key, string defvalue) { return sp.getstring(key, defvalue); } public void removeall() { sp.edit().clear().apply(); } }
工具类
public class utils { /** * 打印 */ public static void print(string message) { log.e("tag", "----- " + message + " ------") ; } /** * 短吐司 */ public static void toasts(context context, string message) { toast.maketext(context, message, toast.length_short).show(); } }
进度条:这个可以忽略,用安卓原生的,代码看我的另一篇博客:android自定义view实现水平带数字百分比进度条
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。