android-downloader_一个带进度条的下载开源框架源码解析(雷惊风)

原创 2015年07月06日 21:08:23

在我们的开发过程中,经常会用到下载功能,也经常伴有显示下载进度的需求,什么下载文件啊,更新版本啊等等吧,今天给大家介绍一个带进度更新下载的开源框架—-android-downloader(下载地址,并跟大家一起看一下它的源码,熟悉一下它的实现过程。

一、涉及到的类及作用。

1. DownloadTask.java

  封装单个下载任务包含的信息,包括要下载文件的名称、文件的下载地址、要下载到本地的路径、开始结束时间、文件类型、文件大小、下载监听(DownloadListener)等等,还有就是当前下载任务的开始,停止等方法。

2.DownloadListener.java

下载任务回调类,包括onAdd(DownloadTask task)、onDelete(DownloadTask task)、onStop(DownloadTask task)、onStart()、onProgressUpdate(Integer... values)、onSuccess(DownloadTask task)、onCancelled()、onError(Throwable thr)、onFinish()方法。

3.AsycDownloadTask.java

真正实现下载的类,继承自AsyncTask类。

4.DownloadException.java

继承自Exception类的自定义错误提示类。

5.DownloadStatus.java

下载中的各种状态,包括STATUS_PENDING、STATUS_RUUUING、STATUS_STOPPED、STATUS_FINISHED、STATUS_FAILED、STATUS_DETELED几种状态。

6.DownloadType.java

下载文件的类型,包括Type_unknown、type_text、type_image、type_music、type_video、type_app几种类型。

7.Isql.java、ISqlmpl.java、DatabaseConfigUtil.java、DatabaseHelper.java

OrmLite数据库操作相关类,保存下载任务信息,这里先不讲,知道有这么回事就行了,还是主要将下载更新相关实现。

二、源码分析。

   至于怎么用大家可以网上去查一下,可以结合Listview一起用,我在这里只跟大家分析一下它的实现过程,,实现过程明白了,用起来也就得心应手了。很简单,大致的过程就是我们需要自己去new一个DownloadListener并实现里边的方法(在这里实现具体的更新操作),实例化DownLoadManager对象,当要建立下载任务时,每一个下载任务创建一个DownLoadTask对象并赋值(name、path、url等等),将当前对象与DownloadListener对象一起添加到DownLoadMagager中如下代码:

DownloadTask task = new DownloadTask(KnowledgeDocListActivity.this);
                                task.setUrl(Settings.DOMAINNAME + docEntity.getUrl());
                                task.setName(docEntity.getDocName());
                                task.setPath(Settings.WORDFILE + docEntity.getDocName());
                                task.setId(docEntity.getId());
                                mDownloadManager.add(task, listener);
那么,让我们看一看DownloadManager的add方法中做了什么:

/**
     * Add Task
     *
     * @param task DownloadTask
     * @return
     */
    public void add(DownloadTask task, DownloadListener listener) {
        Log.i("Add Task");
	//判断task的可用性;
        if (task == null || !task.isValid()) {
            OnResult(POST_MESSAGE.ERROR, task, listener, DownloadException.DOWNLOAD_TASK_NOT_VALID);
            return;
        }

        if (task.getContext() == null) {
	//如果我们没有对task的Context属性赋值,在这里赋值;
	//可以通过上边代码得知,在new DownloadTask(KnowledgeDocListActivity.this)的时候将Context传过来的;
            task.setContext(context);
        }

        ISql iSql = new ISqlImpl(context);
        DownloadTask temptask = null;

        try {
		//根据传入的task到OrmLite数据库查找;
            temptask = iSql.queryDownloadTask(task);

            if (temptask == null || !temptask.isValid() || !temptask.isComplete()) {
		//判断task的name、path、url属性是否都存在;
                if (task.isComplete()) {
                    iSql.addDownloadTask(task);
			//发送添加task事件;
                    OnResult(POST_MESSAGE.ADD, task, listener, -1);
                    Log.i("The Task is stored in the sqlite.");
                } else {
		//如果没有name等属性,开启异步任务去网上获取,这里只是获取,没有下载;
                    task.start(context, listener, true);
                }
            } else {
		//数据库中已经存在这个下载任务;
                task.setDownloadTask(temptask);
                OnResult(POST_MESSAGE.ADD, task, listener, -1);
                Log.i("The Task is already stored in the sqlite.");
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
看他的OnResult(Post_Message.*,task,listener,Integer)方法:

 /**
     * deal with the result
     *
     * @param message  POST_MESSAGE
     * @param listener DownloadListener
     * @param code     code
     */
    @SuppressWarnings("rawtypes")
    private void OnResult(final POST_MESSAGE message, final DownloadTask task, final DownloadListener listener, final Integer code) {
        if (context == null || !(context instanceof Activity)) {
            Log.w("The context is null or invalid!");
            return;
        }

        ((Activity) context).runOnUiThread(new Runnable() {

            public void run() {
                if (listener == null) {
                    return;
                }

                switch (message) {
                    case ADD:
                        listener.onAdd(task);
                        break;
                    case DELETE:
                        listener.onDelete(task);
                        break;
                    case START:
                        listener.onStart();
                        break;
                    case FINISH:
                        listener.onFinish();
                        break;
                    case STOP:
                        listener.onStop(task);
                        break;
                    case ERROR:
                        listener.onError(new DownloadException(code));
                        break;
                }
            }
        });
    }

可见他是通过message不同的状态,调用了DownloadListener的不同方法,正好,也看一下我们这个最终操作UI的类吧:

private DownloadListener listener = new DownloadListener<Integer, DownloadTask>() {
        
        @Override
        public void onAdd(DownloadTask downloadTask) {
            super.onAdd(downloadTask);
            LogUtils.d("onAdd()");
            mDownloadTasklist.add(downloadTask);
            LogUtils.d("" + downloadTask);
            mDocListAdapter.notifyDataSetChanged();
        }

       
        @Override
        public void onDelete(DownloadTask downloadTask) {
            super.onDelete(downloadTask);
            LogUtils.d("onDelete()");
        }

      
        @Override
        public void onStop(DownloadTask downloadTask) {
            super.onStop(downloadTask);
            LogUtils.d("onStop()");
        }

        /**
         * Runs on the UI thread before doInBackground(Params...).
         */
        @Override
        public void onStart() {
            super.onStart();
            UIUtils.makeToast(KnowledgeDocListActivity.this, getResources().getString(R.string.start_download), AppMsg.STYLE_ALERT).show();
        }

        /**
         * Runs on the UI thread after publishProgress(Progress...) is invoked. The
         * specified values are the values passed to publishProgress(Progress...).
         *
         * @param values The values indicating progress.
         */
        @Override
        public void onProgressUpdate(Integer... values) {
            super.onProgressUpdate(values);
            mDocListAdapter.notifyDataSetChanged();
            LogUtils.d("onProgressUpdate");
        }

        /**
         * Runs on the UI thread after doInBackground(Params...). The specified
         * result is the value returned by doInBackground(Params...). This method
         * won't be invoked if the task was cancelled.
         *
         * @param downloadTask The result of the operation computed by
         *                     doInBackground(Params...).
         */
        @Override
        public void onSuccess(DownloadTask downloadTask) {
            super.onSuccess(downloadTask);
            LogUtils.d("异步URL:" + downloadTask.getUrl());
            for (int i = 0; i < mDocList.size(); i++) {
                LogUtils.d("docListURL:" + i + ":" + mDocList.get(i).getUrl());
                if (downloadTask.getUrl().contains(mDocList.get(i).getUrl())) {
                    mDocList.get(i).setIsLocalHave(true);
                    break;
                }
            }
            UIUtils.makeToast(KnowledgeDocListActivity.this, getResources().getString(R.string.download_success), AppMsg.STYLE_ALERT).show();
            mDocListAdapter.notifyDataSetChanged();
            LogUtils.d("onSuccess()");
        }

        /**
         * Applications should preferably override onCancelled(Object). This method
         * is invoked by the default implementation of onCancelled(Object). Runs on
         * the UI thread after cancel(boolean) is invoked and
         * doInBackground(Object[]) has finished.
         */
        @Override
        public void onCancelled() {
            super.onCancelled();
            LogUtils.d("onCancelled()");
        }

        @Override
        public void onError(Throwable thr) {
            super.onError(thr);
            LogUtils.d("onError():" + thr.getMessage());
            UIUtils.makeToast(KnowledgeDocListActivity.this, getResources().getString(R.string.down_fail), AppMsg.STYLE_ALERT).show();
        }

        /**
         * Runs on the UI thread after doInBackground(Params...) when the task is
         * finished or cancelled.
         */
        @Override
        public void onFinish() {
            super.onFinish();
            LogUtils.d("onFinish()");
        }
    };
里边有一些我的操作,就不删除了,可能会对大家理解有帮助,好,再看看我们DownLoadManager的Add方法中的其他操作:task.isComplete():

/**
     * url, name and path is not empty.
     *
     * @return true is valid,otherwise not.
     */
    public boolean isComplete() {
        return !TextUtils.isEmpty(url) && !TextUtils.isEmpty(name) && !TextUtils.isEmpty(path);
    }
task.start(context, listener, true):

/**
     * Start the Task
     *
     * @param context  Context
     * @param listener DownloadListener
     */
    @SuppressWarnings({
            "rawtypes", "unchecked"
    })
    protected void start(Context context, DownloadListener listener, boolean isOnlyGetHead) {
        //Get context,which will be used to communicate with sqlite.
        if (this.context == null && context != null) {
            this.context = context;
        }

        if (task != null) {
            task.cancel(false);
        }
<span style="white-space:pre">	</span>//AsycDownloadTask是继承了AsyncTask的一个类,
<span style="white-space:pre">	</span>//所以在这里他开启了异步去获取信息;
        task = new AsycDownloadTask(listener, isOnlyGetHead);
        task.execute(this);
    }
看一下AsycDownloadTask的doInBackground(DownloadTask ... tasks)方法,也是最主要的一个方法:

/**
     * TODO if error occurs,carry it out. if (listener != null) {
     * listener.onError(new Throwable()); }
     */
    protected DownloadTask doInBackground(DownloadTask... tasks) {
        if (tasks.length <= 0) {
            Log.e("There is no DownloadTask.");
            return null;
        }

        DownloadTask task = tasks[0];

        if (task == null || !task.isValid()) {
		//发送task异常错误信息;
            SendError(task, DownloadException.DOWNLOAD_TASK_NOT_VALID);

            Log.e("The task is not valid,or the url of the task is not valid.");
            return null;
        }

        String path = task.getPath();
        File file = new File(path);

        InputStream in = null;
        RandomAccessFile out = null;
        HttpURLConnection connection = null;

        try {
            long range = file.length();
            long size = task.getSize();
            long curSize = range;
            String filename = task.getName();
            String contentType = task.getMimeType();
		//本地文件信息与数据库保存task信息一致,说明下载了这个任务;
            if (task.getStatus() == DownloadStatus.STATUS_FINISHED && size == range) {
                Log.i("The DownloadTask has already been downloaded.");
                return task;
            }
<span style="white-space:pre">	</span>	//根据url获取网络文件相关信息;
            String urlString = task.getUrl();
            String cookies = null;
            while (true) {
                URL url = new URL(urlString);
                connection = (HttpURLConnection) url.openConnection();
                connection.setRequestProperty("User-Agent", "Snowdream Mobile");
                connection.setRequestProperty("Connection", "Keep-Alive");
                if (cookies != null && cookies != "") {
                    connection.setRequestProperty("Cookie", cookies);
                }
                connection.setRequestMethod("GET");

                if (range > 0) {
                    connection.setRequestProperty("Range", "bytes=" + range +
                            "-");
                }

                //http auto redirection
                //see: http://www.mkyong.com/java/java-httpurlconnection-follow-redirect-example/
                boolean redirect = false;
                boolean success = false;

                // normally, 3xx is redirect
                int status = connection.getResponseCode();
                Log.i("HTTP STATUS CODE: " + status);

                switch (status) {
                    case HttpURLConnection.HTTP_OK:
                    case HttpURLConnection.HTTP_PARTIAL:
                        success = true;

                        String transfer_encoding = connection.getHeaderField("Transfer-Encoding");
                        if (!TextUtils.isEmpty(transfer_encoding)
                                && transfer_encoding.equalsIgnoreCase("chunked")) {
                            mode = MODE_TRUNKED;
                            Log.i("HTTP MODE: TRUNKED");
                        } else {
                            mode = MODE_DEFAULT;
                            Log.i("HTTP MODE: DEFAULT");
                        }

                        String accept_ranges = connection.getHeaderField("Accept-Ranges");
                        if (!TextUtils.isEmpty(accept_ranges)
                                && accept_ranges.equalsIgnoreCase("bytes")) {
                            Log.i("Accept-Ranges: bytes");
                        } else {
                            range = 0;
                            Log.i("Accept-Ranges: none");
                        }
                        break;
                    case HttpURLConnection.HTTP_MOVED_TEMP:
                    case HttpURLConnection.HTTP_MOVED_PERM:
                    case HttpURLConnection.HTTP_SEE_OTHER:
                        redirect = true;
                        // get redirect url from "location" header field
                        urlString = connection.getHeaderField("Location");

                        // get the cookie if need, for login
                        cookies = connection.getHeaderField("Set-Cookie");

                        Log.i("Redirect Url : " + urlString);
                        break;
                    default:
                        success = false;
                        break;
                }

                if (!redirect) {
                    if (!success) {
                        SendError(task, DownloadException.DOWNLOAD_TASK_FAILED);

                        Log.e("Http Connection error. ");
                        return null;
                    }
                    Log.i("Successed to establish the http connection.Ready to download...");
                    break;
                }
            }

            if (range == 0) {
                //set the whole file size
                size = connection.getContentLength();
		//赋值文件大小字段;
                task.setSize(size);

                if (contentType != connection.getContentType()) {
                    contentType = connection.getContentType();
			//赋值类型;
                    task.setMimeType(contentType);
                }

                //auto get filename
                if (TextUtils.isEmpty(filename)) {
                    String disposition = connection.getHeaderField("Content-Disposition");
                    if (disposition != null) {
                        // extracts file name from header field
                        final String FILENAME = "filename=";
                        final int startIdx = disposition.indexOf(FILENAME);
                        final int endIdx = disposition.indexOf(';', startIdx);
                        filename = disposition.substring(startIdx + FILENAME.length(), endIdx > 0 ? endIdx : disposition.length());
                    } else {
                        // extracts file name from URL
                        filename = urlString.substring(urlString.lastIndexOf("/") + 1,
                                urlString.length());
                    }
			//赋值文件名称;
                    task.setName(filename);
                }

                //auto get filepath
                if (TextUtils.isEmpty(path)) {
                    path = STORE_PATH + filename;

                    file = new File(path);
<span style="white-space:pre">			</span>//赋值path;
                    task.setPath(path);
                }

                task.setStartTime(System.currentTimeMillis());
<span style="white-space:pre">		</span>//下载任务添加到数据库;
                SaveDownloadTask(task, task.getStatus());
                Log.i("The Task is stored in the sqlite.");
<span style="white-space:pre">		</span>//如果是只获取信息,不下载文件,则发送调用DownLoadLisener的add方法,
<span style="white-space:pre">		</span>//并不进行后续下载操作;
                if (isOnlyGetHead) {
                    SendAdd(task);
                    return null;
                }
            }

            File dir = file.getParentFile();

            if (!dir.exists() && !dir.mkdirs()) {
                SendError(task, DownloadException.DOWNLOAD_TASK_FAILED);
                Log.e("The directory of the file can not be created!");
                return null;
            }
            task.setStatus(DownloadStatus.STATUS_RUNNING);
            SaveDownloadTask(task, task.getStatus());

            Log.i("DownloadTask " + task);

            out = new RandomAccessFile(file, "rw");
            out.seek(range);

            in = new BufferedInputStream(connection.getInputStream());

            byte[] buffer = new byte[1024];
            int nRead = 0;
            int progress = -1;
            boolean isFinishDownloading = true;
		//下载操作;
            while ((nRead = in.read(buffer, 0, 1024)) > 0) {
                out.write(buffer, 0, nRead);

                curSize += nRead;

                if (size != 0) {
                    progress = (int) ((curSize * 100) / size);
                }
		//这里更新了进度条,注意注意... ...
                publishProgress(progress);

                Log.i("cur size:" + (curSize) + "    total size:" + (size) + "    cur progress:" + (progress));

                if (isCancelled()) {
                    task.setStatus(DownloadStatus.STATUS_STOPPED);
                    isFinishDownloading = false;
                    break;
                }

                if (task.getStatus() != DownloadStatus.STATUS_RUNNING) {
                    isFinishDownloading = false;
                    break;
                }
            }

            if (!isFinishDownloading) {
                Log.w("The DownloadTask has not been completely downloaded.");
                SaveDownloadTask(task, task.getStatus());
                return null;
            }

            //when the mode is MODE_TRUNKED,set the latest size.
            if (size == 0 && curSize != 0) {
                task.setSize(curSize);
            }

            range = file.length();
            size = task.getSize();
            Log.i("range: " + range + " size: " + size);

            if (range != 0 && range == size) {
                Log.i("The DownloadTask has been successfully downloaded.");
                task.setFinishTime(System.currentTimeMillis());
                SaveDownloadTask(task, DownloadStatus.STATUS_FINISHED);
                return task;
            } else {
                Log.i("The DownloadTask failed to downloaded.");
                SendError(task, DownloadException.DOWNLOAD_TASK_FAILED);
                return null;
            }
        } catch (MalformedURLException e) {
            SendError(task, DownloadException.DOWNLOAD_TASK_FAILED);

            e.printStackTrace();
        } catch (ProtocolException e) {
            SendError(task, DownloadException.DOWNLOAD_TASK_FAILED);

            e.printStackTrace();
        } catch (FileNotFoundException e) {
            SendError(task, DownloadException.DOWNLOAD_TASK_FAILED);

            e.printStackTrace();
        } catch (IOException e) {
            SendError(task, DownloadException.DOWNLOAD_TASK_FAILED);

            e.printStackTrace();
        } finally {
            try {
                if (in != null) {
                    in.close();
                }

                if (out != null) {
                    out.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }

            if (connection != null) {
                connection.disconnect();
            }
        }
        return null;
    }
看一下当前类里边的SendError与SendAdd方法:

private void SendError(DownloadTask task, Integer code) {
        Log.e("Errors happen while downloading.");
        SaveDownloadTask(task, DownloadStatus.STATUS_FAILED);
<span style="white-space:pre">	</span>//调用了当前类里边的一个sHandler对象;
        sHandler.obtainMessage(
                MESSAGE_POST_ERROR,
                new AsyncTaskResult(this, task,
                        code)).sendToTarget();
    }

    private void SendAdd(DownloadTask task) {
        sHandler.obtainMessage(
                MESSAGE_POST_ADD,
                new AsyncTaskResult(this, task,
                        -1)).sendToTarget();
    }
看一下sHandler是个什么东东:

private static class InternalHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            AsyncTaskResult result = (AsyncTaskResult) msg.obj;

            if (result == null || result.mTask == null || result.mTask.isCancelled()) {
                Log.i("The asyncTask is not valid or cancelled!");
                return;
            }

            switch (msg.what) {
                case MESSAGE_POST_ERROR:
		<span style="white-space:pre">	</span>//调用当前类的OnError方法;
                    ((AsycDownloadTask) result.mTask).OnError(result.mDownloadTask, result.mData);
                    break;
                case MESSAGE_POST_ADD:
			//调用当前类的OnAdd方法;
                    ((AsycDownloadTask) result.mTask).OnAdd(result.mDownloadTask);
                    break;
                default:
                    break;
            }
        }
    }
继续向下看对应的方法:

 /**
     * throw error
     *
     * @param task task
     * @param code The code of the exception
     */
    private void OnError(DownloadTask task, Integer code) {
        if (listener != null) {
            listener.onError(new DownloadException(code));
        }
    }

    /**
     * inform Add
     *
     * @param task task
     */
    private void OnAdd(DownloadTask task) {
        if (listener != null && listener instanceof DownloadListener) {
            ((DownloadListener) listener).onAdd(task);
        }
    }
可以发现到现在还是调用了DownloadListener对应的方法去进行相关操作;下面看一下下载过程中的更新方法:
publishProgress(progress);
protected final void publishProgress(Progress... values) {
        if (!isCancelled()) {
            getHandler().obtainMessage(MESSAGE_POST_PROGRESS,
                    new AsyncTaskResult<Progress>(this, values)).sendToTarget();
        }
    }
得到Handler对象发送消息,看getHandler方法:

 private static Handler getHandler() {
        synchronized (AsyncTask.class) {
            if (sHandler == null) {
                sHandler = new InternalHandler();
            }
            return sHandler;
        }
    }
很简单的单例,下边看看这个Handler在哪:

 public InternalHandler() {
            super(Looper.getMainLooper());
        }
主线程啊,看看他发送消息后的操作吧:

private static class InternalHandler extends Handler {
        public InternalHandler() {
            super(Looper.getMainLooper());
        }

        @SuppressWarnings({"unchecked", "RawUseOfParameterizedType"})
        @Override
        public void handleMessage(Message msg) {
            AsyncTaskResult<?> result = (AsyncTaskResult<?>) msg.obj;
            switch (msg.what) {
                case MESSAGE_POST_RESULT:
                    // There is only one result
                    result.mTask.finish(result.mData[0]);
                    break;
                case MESSAGE_POST_PROGRESS:
		   //最终在这里调用了更新操作;
                    result.mTask.onProgressUpdate(result.mData);
                    break;
            }
        }
    }
好了,到这里添加向DownLoadMagager中添加task就说完了,其实下载的时候是调用了DownLoadMagager的start方法,在start方法中也是调用了DownloadTask的start方法跟DownLoadMagager的添加方法中调用DownloadTask的start方法差不多,只不过这次最后一个参数传的是false,最后一个参数的含义就是是不是只获取文件信息,下边看一下实现:

**
     * Start Task
     *
     * @param task
     * @param listener
     * @return
     */
    @SuppressWarnings("rawtypes")
    public boolean start(DownloadTask task, DownloadListener listener) {
        Log.i("Start Task");

        boolean ret = false;

        if (task == null) {
            OnResult(POST_MESSAGE.ERROR, task, listener, DownloadException.DOWNLOAD_TASK_NOT_VALID);
            return ret;
        }

        if (task.getContext() == null) {
            task.setContext(context);
        }

        ISql iSql = new ISqlImpl(context);

        DownloadTask temptask = null;

        try {
            temptask = iSql.queryDownloadTask(task);

            if (temptask == null) {
                add(task, listener);
            } else if (!temptask.equals(task)) {
                task.setDownloadTask(temptask);
            }

            switch (task.getStatus()) {
                case DownloadStatus.STATUS_RUNNING:
                    OnResult(POST_MESSAGE.START, task, listener, -1);
                    OnResult(POST_MESSAGE.FINISH, task, listener, -1);
                    Log.i("The Task is already Running.");
                    break;
                default:
                    if (listener != null) {
			//这里就跟添加task时一样了,注意最后一个参数;
                        task.start(context, listener, false);
                    }
                    break;
            }

            ret = true;
        } catch (SQLException e) {
            e.printStackTrace();
        }

        return ret;
    }
好,到现在为止添加跟下载就说完了,还有stop方法等等,也不难,感兴趣的可以自己看一下,我这里就不再说了。上边的实现代码很简单,我也加了注释,希望这篇文章对大家了解android-downloader有帮助。




刚刚创建了132079212,希望能共同学习,非诚勿扰!

版权声明:本文为博主原创文章,转载请注明出处。

相关文章推荐

Android DownloadManager 的使用

从Android 2.3(API level 9)开始Android用系统服务(Service)的方式提供了Download Manager来优化处理长时间的下载操作。Download Manager...
  • sir_zeng
  • sir_zeng
  • 2013年05月28日 10:32
  • 40786

开源项目之android-downloader

android,下载文件: demo: https://github.com/snowdream/android-downloader android-downloader 一个用于下载的androi...
  • xiongmc
  • xiongmc
  • 2014年05月15日 18:08
  • 1423

android文件下载器(轻量级)——EasyFileDownload

EasyFileDownloaderA lightweight for use in the android file downloader Download the APK is especiall...

Android 文件下载引擎 FileDownloader

FileDownloader  Android 文件下载引擎,稳定、高效、灵活、简单易用 特点 简单易用高并发灵活可选择性支持: 独立/非独立进程自动断点续传 需要注意 当下载的文件大小可能...

Android 文件下载引擎,稳定、高效、简单易用:FileDownloader

Android 文件下载引擎,稳定、高效、简单易用:FileDownloade 来源:http://www.open-open.com/lib/view/open1451397722511....

Android 打造形形色色的进度条 实现可以如此简单

转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/43371299 ,本文出自:【张鸿洋的博客】1、概述 最近需要用进度条,秉着不重复...

Delphi7高级应用开发随书源码

  • 2003年04月30日 00:00
  • 676KB
  • 下载

Delphi7高级应用开发随书源码

  • 2003年04月30日 00:00
  • 676KB
  • 下载

FileDownloader-Android 文件下载引擎,稳定、高效、简单易用

FileDownloader Android 文件下载引擎,稳定、高效、简单易用     README DOC 本引擎依赖okhttp 3.4.1 版本迭代日志: C...
  • Alpha58
  • Alpha58
  • 2016年11月30日 15:03
  • 4331

Android酷炫实用的开源框架(UI框架)

Android酷炫实用的开源框架(UI框架)前言忙碌的工作终于可以停息一段时间了,最近突然有一个想法,就是自己写一个app,所以找了一些合适开源控件,这样更加省时,再此分享给大家,希望能对大家有帮助,...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:android-downloader_一个带进度条的下载开源框架源码解析(雷惊风)
举报原因:
原因补充:

(最多只允许输入30个字)