前言
RxJava是一个实现java响应式编程的库。个人觉得它的优势是能够简化异步处理的逻辑,从而使整个业务流程能够更好的被读者理解,在处理异步业务的时候有处理同步业务的类似体验。个人理解可能不是很准确,对RxJava理解较浅的可以看看扔物线大牛的牛文给 Android 开发者的 RxJava 详解。
Volley这个库作为Android开发者肯定是知道的,是Google开源的一个网络框架,适用于数据小网络请求频繁的场景。
阅读本文前你应该先了解过RxJava和Volley。
实现思路
我这里需要解决的问题是使用TextView
加载一段HTML,HTML中有部分是图片,需要能够点击进入一个可缩放图片的页面查看图片的细节。
用TextView
加载HTML很简单textView.setText(Html.fromHtml(htmlString))
即可。加载HTML中的图片也很简单,只要实现一个ImageGetter
,然后在Html.fromHtml(htmlString, imageGetter, tagHandler)
使用即可。而实现TextView
中部分内容的点击使用ClickableSapn
可以实现。
解决问题的难点是,HTML中的图片在加载HTML的时候需要先从网络下载,我们需要在所有图片下载完成后再去刷新TextView
,达到显示图片的目的。之前使用Volley的时候都是使用CallBack<T>
的方式,这样在处理单个异步任务的时候还没有问题,但是在处理一组任务的时候就有些不足。之前RxJava就是一个很火很先进的库,自己也进行过了解,知道使用RxJava在处理一组异步任务的时候相当方便,所以就想尝试一下能否使用RxJava和Volley来解决遇到的问题。功夫不负有心人,在查过不少资料之后,经过尝试终于找到了一个可行的方案。
Volley除了提供Response.Listener<T>
,和Response.ErrorListener
的CallBack<T>
方式,还提供了RequestFuture<T>
的方式。这样我们就可以实现同步方式处理异步的请求。
举个栗子:
private RequestQueue requestQueue = Volley.newRequestQueue(context);
// response listener
public void getImageFromNet(String url, int maxWidth, int maxHeight, Bitmap.Config config, Response.Listener<Bitmap> listener,
Response.ErrorListener errorListener) {
ImageRequest request = new ImageRequest(url, listener, maxWidth, maxHeight, config, errorListener);
request.setShouldCache(true);
request.setTag(url);
requestQueue.add(request);
}
// request future
public Bitmap getImageFromNet(String url, int maxWidth, int maxHeight, Bitmap.Config config)
throws ExecutionException, InterruptedException {
RequestFuture<Bitmap> requestFuture = RequestFuture.newFuture();
ImageRequest request = new ImageRequest(url, requestFuture, maxWidth, maxHeight, config, requestFuture);
request.setShouldCache(true);
request.setTag(url);
requestQueue.add(request);
return requestFuture.get();
}
设计思路:
- 因为使用了图片的缓存,所以在textView.setText(spanned)
中先显示已经缓存过的部分图片。
- 收集需要下载的图片地址之后,使用RxJava和Volley下载所有图片。
- 再次textView.setText(spanned)
,刷新TextView。
主要代码:
private OnImageClickListener listener;
private CompositeSubscription compositeSubscription;
public void setHtmlString(String htmlString) {
// 要响应点击,这句必须
setMovementMethod(LinkMovementMethod.getInstance());
this.htmlString = htmlString;
Spanned spanned = Html.fromHtml(htmlString, VolleyImageGetter.from(this), null);
// 先显示文字和部分图片
setText(spanned);
// 获取需要下载的图片url
SpannableStringBuilder spannableStringBuilder;
if (spanned instanceof SpannableStringBuilder) {
spannableStringBuilder = (SpannableStringBuilder) spanned;
} else {
spannableStringBuilder = new SpannableStringBuilder(spanned);
}
ImageSpan[] imageSpans =
spannableStringBuilder.getSpans(0, spannableStringBuilder.length(), ImageSpan.class);
if (imageSpans != null && imageSpans.length > 0) {
List<String> sources = new ArrayList<>();
for (ImageSpan span : imageSpans) {
final String url = span.getSource();
if (!AndroidHelper.getImageDiskCache().hasBitMap(VolleyHelper.getCacheKey(url))) {
LogHelper.e("add url:" + url);
sources.add(url);
}
}
if (sources.size() > 0) {
getImageInRxWay(sources);
} else {
// 如果不需要下载图片则处理图片的点击
resetTextAndClickable();
}
}
}
private void resetTextAndClickable() {
Spanned spanned =
Html.fromHtml(htmlString, VolleyImageGetter.from(ImageClickableTextView.this), null);
SpannableStringBuilder spannableStringBuilder;
if (spanned instanceof SpannableStringBuilder) {
spannableStringBuilder = (SpannableStringBuilder) spanned;
} else {
spannableStringBuilder = new SpannableStringBuilder(spanned);
}
ImageSpan[] imageSpans =
spannableStringBuilder.getSpans(0, spannableStringBuilder.length(), ImageSpan.class);
if (imageSpans != null && imageSpans.length > 0) {
for (ImageSpan span : imageSpans) {
final String url = span.getSource();
int start = spannableStringBuilder.getSpanStart(span);
int end = spannableStringBuilder.getSpanEnd(span);
ClickableSpan clickableSpan = new ClickableSpan() {
@Override public void onClick(View widget) {
if (listener != null) {
listener.imageClicked(url);
}
}
};
// remove other clickable span
ClickableSpan[] clickableSpans =
spannableStringBuilder.getSpans(start, end, ClickableSpan.class);
if (clickableSpans != null && clickableSpans.length > 0) {
for (ClickableSpan span1 : clickableSpans) {
spannableStringBuilder.removeSpan(span1);
}
}
// add image clickable span
spannableStringBuilder.setSpan(clickableSpan, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
setText(spanned);
}
// get image from net in rxjava way
private void getImageInRxWay(List<String> sources) {
if (compositeSubscription == null) {
compositeSubscription = new CompositeSubscription();
}
Subscription subscription =
Observable.from(sources)
.flatMap(new Func1<String, Observable<BitmapEntry>>() {
@Override public Observable<BitmapEntry> call(String s) {
return getBitmapEntry(s);
}
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Subscriber<BitmapEntry>() {
@Override public void onCompleted() {
LogHelper.e("onCompleted");
resetTextAndClickable();
}
@Override public void onError(Throwable e) {
if (e instanceof VolleyError) {
VolleyError cause = (VolleyError) e.getCause();
String s = new String(cause.networkResponse.data, Charset.forName("UTF-8"));
LogHelper.e(s);
} else {
LogHelper.e(e.getMessage());
}
}
@Override public void onNext(BitmapEntry entry) {
// add bitmap to cache
if (entry.getBitmap() != null) {
AndroidHelper.getImageDiskCache().putBitmap(entry.getKey(), entry.getBitmap());
}
}
});
compositeSubscription.add(subscription);
}
// 使用defer来提高性能
// defer操作符是直到有订阅者订阅时,才通过Observable的工厂方法创建Observable并执行,
// 在使用rxjava中我们应该使用defer来包装慢的操作
private Observable<BitmapEntry> getBitmapEntry(final String url) {
return Observable.defer(new Func0<Observable<BitmapEntry>>() {
@Override public Observable<BitmapEntry> call() {
try {
return Observable.just(getImageBitmap(url));
} catch (ExecutionException | InterruptedException e) {
LogHelper.e(e.getMessage());
return Observable.error(e);
}
}
});
}
private BitmapEntry getImageBitmap(String url) throws ExecutionException, InterruptedException {
RequestFuture<Bitmap> future = RequestFuture.newFuture();
ImageRequest request = new ImageRequest(url, future, 0, 0, Bitmap.Config.RGB_565, future);
VolleyHelper.addRequest2Queue(request);
String key = VolleyHelper.getCacheKey(url);
Bitmap bitmap = future.get();
return new BitmapEntry(key, bitmap);
}
public interface OnImageClickListener {
public void imageClicked(String imageUrl);
}
个人对于RxJava的理解也是比较浅,相信多看多练也能掌握这个新思想。