Android okhttp+rxjava实现多文件下载和断点续传

来源:互联网 发布:php接口开发视频 编辑:程序博客网 时间:2024/06/06 21:45
    先说下我的需求。我的需求是PC端先进行更新数据的管理,然后移动端登录时候会自动访问服务,传入mac值,获取需更新数据的信息。如下图所示:


      

        从服务返回到的是json格式的字符串,我解析后获得一个list<bean>,bean的结构为:

public class OfflineDataBean {    private String dataId;    private String dataName;    private String organizationName;    private String mac;    private int dataType;    private String dataAddtime;    private String dataUpdatetime;    private String dataPath;    private String dataStatus;    private String remark;    ...}
      

      接下来就是将这个list展示在一个RecyclerView里。在这里我首先将RecyclerView的Adapter和Holder进行了一次封装:

public abstract class BaseRecyclerAdapter<T> extends RecyclerView.Adapter<RecyclerViewHolder> {    //list集合    protected final List<T> mData;    protected final Context mContext;    //上下文    protected LayoutInflater mInflater;    //点击item监听    private OnItemClickListener mClickListener;    //长按item监听    private OnItemLongClickListener mLongClickListener;    /**     * 构造方法     *     * @param ctx     * @param list     */    public BaseRecyclerAdapter(Context ctx, List<T> list) {        mData = (list != null) ? list : new ArrayList<T>();        mContext = ctx;        mInflater = LayoutInflater.from(ctx);    }    public void clear() {        this.mData.clear();    }    /**     * 方法中主要是引入xml布局文件,并且给item点击事件和item长按事件赋值     *     * @param parent     * @param viewType     * @return     */    @Override    public RecyclerViewHolder onCreateViewHolder(ViewGroup parent, final int viewType) {        final RecyclerViewHolder holder = new RecyclerViewHolder(mContext,                mInflater.inflate(getItemLayoutId(viewType), parent, false));        if (mClickListener != null) {            holder.itemView.setOnClickListener(new View.OnClickListener() {                @Override                public void onClick(View v) {                    mClickListener.onItemClick(holder.itemView, holder.getPosition());                }            });        }        if (mLongClickListener != null) {            holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {                @Override                public boolean onLongClick(View v) {                    mLongClickListener.onItemLongClick(holder.itemView, holder.getPosition());                    return true;                }            });        }        return holder;    }    /**     * onBindViewHolder这个方法主要是给子项赋值数据的     *     * @param holder     * @param position     */    @Override    public void onBindViewHolder(RecyclerViewHolder holder, int position) {        bindData(holder, position, mData.get(position));    }    @Override    public int getItemCount() {        return mData.size();    }    /**     * add方法是添加item方法     *     * @param pos     * @param item     */    public void add(int pos, T item) {        mData.add(pos, item);        notifyItemInserted(pos);    }    /**     * delete方法是删除item方法     *     * @param pos     */    public void delete(int pos) {        mData.remove(pos);        notifyItemRemoved(pos);    }    /**     * item点击事件set方法     *     * @param listener     */    public void setOnItemClickListener(OnItemClickListener listener) {        mClickListener = listener;    }    /**     * item长安事件set方法     *     * @param listener     */    public void setOnItemLongClickListener(OnItemLongClickListener listener) {        mLongClickListener = listener;    }    /**     * item中xml布局文件方法     *     * @param viewType     * @return     */    abstract public int getItemLayoutId(int viewType);    /**     * 赋值数据方法     *     * @param holder     * @param position     * @param item     */    abstract public void bindData(RecyclerViewHolder holder, int position, T item);    /**     * item点击事件接口     */    public interface OnItemClickListener {        public void onItemClick(View itemView, int pos);    }    /**     * item长按事件接口     */    public interface OnItemLongClickListener {        public void onItemLongClick(View itemView, int pos);    }}
public class RecyclerViewHolder extends RecyclerView.ViewHolder {    /**     * 集合类,layout里包含的View,以view的id作为key,value是view对象     */    private SparseArray<View> mViews;    /**     * 上下文对象     */    private Context mContext;    /**     * 构造方法     *     * @param ctx     * @param itemView     */    public RecyclerViewHolder(Context ctx, View itemView) {        super(itemView);        mContext = ctx;        mViews = new SparseArray<View>();    }    /**     * 存放xml页面方法     *     * @param viewId     * @param <T>     * @return     */    private <T extends View> T findViewById(int viewId) {        View view = mViews.get(viewId);        if (view == null) {            view = itemView.findViewById(viewId);            mViews.put(viewId, view);        }        return (T) view;    }    public View getView(int viewId) {        return findViewById(viewId);    }    /**     * 存放文本的id     *     * @param viewId     * @return     */    public TextView getTextView(int viewId) {        return (TextView) getView(viewId);    }    /**     * 存放button的id     *     * @param viewId     * @return     */    public Button getButton(int viewId) {        return (Button) getView(viewId);    }    /**     * 存放图片的id     *     * @param viewId     * @return     */    public ImageView getImageView(int viewId) {        return (ImageView) getView(viewId);    }    public LinearLayout getLinearLayout(int viewId) {        return (LinearLayout) getView(viewId);    }    public ProgressBar getProgressBar(int viewId)  {        return (ProgressBar) getView(viewId);    }    /**     * 存放图片按钮的id     *     * @param viewId     * @return     */    public ImageButton getImageButton(int viewId) {        return (ImageButton) getView(viewId);    }    /**     * 存放输入框的id     *     * @param viewId     * @return     */    public EditText getEditText(int viewId) {        return (EditText) getView(viewId);    }    /**     * 存放文本xml中的id并且可以赋值数据的方法     *     * @param viewId     * @param value     * @return     */    public RecyclerViewHolder setText(int viewId, String value) {        TextView view = findViewById(viewId);        view.setText(value);        return null;    }    /**     * 存放图片xml中的id并且可以赋值数据的方法     *     * @param viewId     * @param resId     * @return     */    public RecyclerViewHolder setBackground(int viewId, int resId) {        View view = findViewById(viewId);        view.setBackgroundColor(resId);        return null;    }    /**     * 存放点击事件监听     *     * @param viewId     * @param listener     * @return     */    public RecyclerViewHolder setClickListener(int viewId, View.OnClickListener listener) {        View view = findViewById(viewId);        view.setOnClickListener(listener);        return null;    }}
   

      然后RecyclerView里的item布局文件为:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"    android:layout_width="match_parent"    android:orientation="horizontal"    android:paddingLeft="10dp"    android:layout_height="60dp">    <TextView        android:id="@+id/tv_name"        android:layout_width="100dp"        android:layout_height="wrap_content"        android:textSize="16sp"/>    <ProgressBar        android:id="@+id/main_progress"        android:layout_width="0dp"        android:layout_weight="1"        android:layout_height="match_parent"        style="@style/Widget.AppCompat.ProgressBar.Horizontal" />    <TextView        android:id="@+id/tv_percent"        android:layout_marginLeft="10dp"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:text="00"        android:textSize="18sp" />    <TextView        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:text="/100"        android:textSize="18sp"/>    <Button        android:id="@+id/btn_down"        android:layout_marginLeft="10dp"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:text="开始下载"/></LinearLayout>

      开始布局RecyclerView,思路为点击开始下载按钮,开始下载文件,再次点击暂停下载并可以续传下载。下载完毕后提示下载完毕。

baseRecyclerAdapterOfflineData=new BaseRecyclerAdapter<OfflineDataBean>(this,offlineDataBeenList) {    @Override    public int getItemLayoutId(int viewType) {        return R.layout.item_offlinedata;    }    @Override    public void bindData(RecyclerViewHolder holder, int position, OfflineDataBean item) {        TextView tvName=holder.getTextView(R.id.tv_name);        TextView tvpercent=holder.getTextView(R.id.tv_percent);        Button btnDown=holder.getButton(R.id.btn_down);        ProgressBar progressBar=holder.getProgressBar(R.id.main_progress);        tvName.setText(offlineDataBeenList.get(position).getDataName());        btnDown.setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View v) {                if(btnDown.getText().equals("开始下载")||btnDown.getText().equals("继续下载")) {                    DownloadManager.getInstance().download(offlineDataBeenList.get(position).getDataPath(), new DownLoadObserver() {                        @Override                        public void onNext(DownloadInfo value) {                            super.onNext(value);                            tvpercent.setText(String.valueOf((int)(((double)value.getProgress()/(double)value.getTotal())*100.00)));                            progressBar.setMax((int) value.getTotal());                            progressBar.setProgress((int) value.getProgress());                            btnDown.setText("暂停下载");                        }                        @Override                        public void onComplete() {                            if (downloadInfo != null) {                                btnDown.setText("下载结束");                            }                        }                    });                }else if(btnDown.getText().toString().equals("暂停下载")) {                    DownloadManager.getInstance().cancel(offlineDataBeenList.get(position).getDataPath());                    btnDown.setText("开始下载");                }            }        });    }};rvDownload.setAdapter(baseRecyclerAdapterOfflineData);rvDownload.setLayoutManager(new LinearLayoutManager(this));rvDownload.setItemAnimator(new DefaultItemAnimator());

      重点还是在DownloadManager类,再次感谢下丰神。这个类里最重要的是download方法,如下所示:

public void download(String url, DownLoadObserver downLoadObserver) {    Observable.just(url)            .filter(s -> !downCalls.containsKey(s))//call的map已经有了,就证明正在下载,则这次不下载            .flatMap(s -> Observable.just(createDownInfo(s)))            .map(this::getRealFileName)//检测本地文件夹,生成新的文件名            .flatMap(downloadInfo -> Observable.create(new DownloadSubscribe(downloadInfo)))//下载            .observeOn(AndroidSchedulers.mainThread())//在主线程回调            .subscribeOn(Schedulers.io())//在子线程执行            .subscribe(downLoadObserver);//添加观察者}

      其中url是文件下载地址,downloadObserver是用来回调的接口,监听下载情况。简要说明下这个rxjava的方法,从上往下每行的意思分别是:

      传入url参数;

      判断是否正在这个url下载文件,如果存在,则这次不下载(防止多次点击同一个下载按钮);

      获取并传入下载信息;

      检测本地文件(文件是否存在,如果存在已下载多少);

      根据下载信息创建下载的观察者方法;

      设置在主线程回调;

      观察者方法在子线程执行;

      添加观察者方法,开始执行。

      

      附上完整代码:

public class DownloadManager {    private static final AtomicReference<DownloadManager> INSTANCE = new AtomicReference<>();    private HashMap<String, Call> downCalls;//用来存放各个下载的请求    private OkHttpClient mClient;//OKHttpClient;    //获得一个单例类    public static DownloadManager getInstance() {        for (; ; ) {            DownloadManager current = INSTANCE.get();            if (current != null) {                return current;            }            current = new DownloadManager();            if (INSTANCE.compareAndSet(null, current)) {                return current;            }        }    }    private DownloadManager() {        downCalls = new HashMap<>();        mClient = new OkHttpClient.Builder().build();    }    /**     * 开始下载     *     * @param url              下载请求的网址     * @param downLoadObserver 用来回调的接口     */    public void download(String url, DownLoadObserver downLoadObserver) {        Observable.just(url)                .filter(s -> !downCalls.containsKey(s))//call的map已经有了,就证明正在下载,则这次不下载                .flatMap(s -> Observable.just(createDownInfo(s)))                .map(this::getRealFileName)//检测本地文件夹,生成新的文件名                .flatMap(downloadInfo -> Observable.create(new DownloadSubscribe(downloadInfo)))//下载                .observeOn(AndroidSchedulers.mainThread())//在主线程回调                .subscribeOn(Schedulers.io())//在子线程执行                .subscribe(downLoadObserver);//添加观察者    }    public void cancel(String url) {        Call call = downCalls.get(url);        if (call != null) {            call.cancel();//取消        }        downCalls.remove(url);    }    /**     * 创建DownInfo     *     * @param url 请求网址     * @return DownInfo     */    private DownloadInfo createDownInfo(String url) {        DownloadInfo downloadInfo = new DownloadInfo(url);        long contentLength = getContentLength(url);//获得文件大小        downloadInfo.setTotal(contentLength);        String fileName = url.substring(url.lastIndexOf("/"));        downloadInfo.setFileName(fileName);        return downloadInfo;    }    private DownloadInfo getRealFileName(DownloadInfo downloadInfo) {        String fileName = downloadInfo.getFileName();        long downloadLength = 0, contentLength = downloadInfo.getTotal();        File file = new File(MyApp.sContext.getFilesDir(), fileName);        if (file.exists()) {            //找到了文件,代表已经下载过,则获取其长度            downloadLength = file.length();        }        //之前下载过,需要重新来一个文件        int i = 1;        while (downloadLength >= contentLength) {            int dotIndex = fileName.lastIndexOf(".");            String fileNameOther;            if (dotIndex == -1) {                fileNameOther = fileName + "(" + i + ")";            } else {                fileNameOther = fileName.substring(0, dotIndex)                        + "(" + i + ")" + fileName.substring(dotIndex);            }            File newFile = new File(MyApp.sContext.getFilesDir(), fileNameOther);            file = newFile;            downloadLength = newFile.length();            i++;        }        //设置改变过的文件名/大小        downloadInfo.setProgress(downloadLength);        downloadInfo.setFileName(file.getName());        return downloadInfo;    }    private class DownloadSubscribe implements ObservableOnSubscribe<DownloadInfo> {        private DownloadInfo downloadInfo;        public DownloadSubscribe(DownloadInfo downloadInfo) {            this.downloadInfo = downloadInfo;        }        @Override        public void subscribe(ObservableEmitter<DownloadInfo> e) throws Exception {            String url = downloadInfo.getUrl();            long downloadLength = downloadInfo.getProgress();//已经下载好的长度            long contentLength = downloadInfo.getTotal();//文件的总长度            //初始进度信息            e.onNext(downloadInfo);            Request request = new Request.Builder()                    //确定下载的范围,添加此头,则服务器就可以跳过已经下载好的部分                    .addHeader("RANGE", "bytes=" + downloadLength + "-" + contentLength)                    .url(url)                    .build();            Call call = mClient.newCall(request);            downCalls.put(url, call);//把这个添加到call里,方便取消            Response response = call.execute();            File file = new File(MyApp.sContext.getFilesDir(), downloadInfo.getFileName());            InputStream is = null;            FileOutputStream fileOutputStream = null;            try {                is = response.body().byteStream();                fileOutputStream = new FileOutputStream(file, true);                byte[] buffer = new byte[2048];//缓冲数组2kB                int len;                while ((len = is.read(buffer)) != -1) {                    fileOutputStream.write(buffer, 0, len);                    downloadLength += len;                    downloadInfo.setProgress(downloadLength);                    e.onNext(downloadInfo);                }                fileOutputStream.flush();                downCalls.remove(url);            } finally {                //关闭IO流                IOUtil.closeAll(is, fileOutputStream);            }            e.onComplete();//完成        }    }    /**     * 获取下载长度     *     * @param downloadUrl     * @return     */    private long getContentLength(String downloadUrl) {        Request request = new Request.Builder()                .url(downloadUrl)                .build();        try {            Response response = mClient.newCall(request).execute();            if (response != null && response.isSuccessful()) {                long contentLength = response.body().contentLength();                response.close();                return contentLength == 0 ? DownloadInfo.TOTAL_ERROR : contentLength;            }        } catch (IOException e) {            e.printStackTrace();        }        return DownloadInfo.TOTAL_ERROR;    }}

      其他地方不用多说,最核心一句代码是:

                    .addHeader("RANGE", "bytes=" + downloadLength + "-" + contentLength)
      通过这行代码确定下载的范围,从已下载的地方下载到结束。前面在创建被观察者时候执行的两个方法createDownInfo和getRealFileName就是为了分别获取总长度和已下载长度。引用丰神博文原话来说就是:


当要断点续传的话必须添加这个头,让输入流跳过多少字节的形式是不行的,所以我们要想能成功的添加这条信息那么就必须对这个url请求2次,一次拿到总长度,来方便判断本地是否有下载一半的数据,第二次才开始真正的读流进行网络请求,我还想了一种思路,当文件没有下载完成的时候添加一个自定义的后缀,当下载完成再把这个后缀取消了,应该就不需要请求两次了


      对应的下载信息DownloadInfo为:

public class DownloadInfo {    public static final long TOTAL_ERROR = -1;//获取进度失败    private String url;    private long total;    private long progress;    private String fileName;    public DownloadInfo(String url) {        this.url = url;    }    public String getUrl() {        return url;    }    public String getFileName() {        return fileName;    }    public void setFileName(String fileName) {        this.fileName = fileName;    }    public long getTotal() {        return total;    }    public void setTotal(long total) {        this.total = total;    }    public long getProgress() {        return progress;    }    public void setProgress(long progress) {        this.progress = progress;    }}
      

      回调接口为:

public  abstract class DownLoadObserver implements Observer<DownloadInfo> {    protected Disposable d;//可以用于取消注册的监听者    protected DownloadInfo downloadInfo;    @Override    public void onSubscribe(Disposable d) {        this.d = d;    }    @Override    public void onNext(DownloadInfo downloadInfo) {        this.downloadInfo = downloadInfo;    }    @Override    public void onError(Throwable e) {        e.printStackTrace();    }}

      主要代码已经贴上,让我们来看看效果为:


      可以断点续传,可以监听到实时下载情况,可以同时多个下传。

      需求达成。



                    .addHeader("RANGE", "bytes=" + downloadLength + "-" + contentLength)
阅读全文
0 0
原创粉丝点击