RxJava+Volley实现图片可点击的TextView

前言

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.ErrorListenerCallBack<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);
}

完整的代码:https://github.com/Zhaoyy/ikanxue/blob/2.x/app/src/main/java/com/mislead/ikanxue/app/view/ImageClickableTextView.java

个人对于RxJava的理解也是比较浅,相信多看多练也能掌握这个新思想。

参考
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值