从服务返回到的是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)