Glide源码分析(二) 如何动态测量获取ImageView大小

介绍Glide如何动态测量ImageView大小

我们都知道,Glide是在Picasso的基础上进行的改善,相比于Picasso,Glide会根据ImageView的大小来生成图片大小,这样可以减少图片占用的内存大小,我们来看看Glide是怎么动态测量获取ImageView的大小,并且设置根据大小设置图片的大小

我们先来简单回顾一下Activity中加载视图的过程:
我们在onCreate(...)方法中调用了setContentView(int layoutId)来设置了布局文件,其实这里并没有马上进行视图的测量,布局,绘制,一切都是等到onResume之后才进行界面视图的渲染。至于如何渲染,这里有两篇文章,如果你不熟悉或者还是小白,可以先参考下:
Android View的绘制流程
Android应用程序启动过程源代码分析

那么在这个生命周期过程中,如果使用了Glide的加载图片方法,那么其实这时候图片是无法确定大小和位置的(因为这时候整个页面视图还没有进行测量布局绘制,哪来的确定大小和位置),这时候Glide该怎么办呢?

还是以我们上文Glide源码分析(一) 图片加载的生命周期中的例子来分析一下整个过程,重点讲解一下动态获取ImageView大小

例子

  1. Glide.with(StartActivity.this).load(R.mipmap.pizza).into(mIvShow);

分析

本例中,Glide前面的方法最终要生成Bitmap对象写入ImageView对象中显示出来,我们看下into()方法
GenericRequestBuilder.java

  1. // 设置了将要加载图片到哪个视图,取消已经加载到ImageView中的资源,并且释放资源用于后面可能的复用
  2. public Target<TranscodeType> into(ImageView view) {
  3. Util.assertMainThread();
  4. if (view == null) {
  5. throw new IllegalArgumentException("You must pass in a non null View");
  6. }
  7. // 如果之前没有定义过Transformation,并且ImageView设置了scaleType
  8. if (!isTransformationSet && view.getScaleType() != null) {
  9. switch (view.getScaleType()) {
  10. case CENTER_CROP:
  11. // 生成一个CENTER_CROP的Transformation,用于后面生成图片时转换
  12. applyCenterCrop();
  13. break;
  14. case FIT_CENTER:
  15. case FIT_START:
  16. case FIT_END:
  17. // 生成一个FIT_END的Transformation对象,用于后面生成图片时转换
  18. applyFitCenter();
  19. break;
  20. //$CASES-OMITTED$
  21. default:
  22. // Do nothing.
  23. }
  24. }
  25. // 统一构建ImageViewTarget
  26. return into(glide.buildImageViewTarget(view, transcodeClass));
  27. }

这里最终会构建一个ImageViewTarget对象,接下来看下是如何构建这个类
关于自定义Transformation的例子,具体看我的例子工程GlideSampleTransformationActivity.java

Glide.java

  1. <R> Target<R> buildImageViewTarget(ImageView imageView, Class<R> transcodedClass) {
  2. return imageViewTargetFactory.buildTarget(imageView, transcodedClass);
  3. }

ImageViewTargetFactory.java

  1. @SuppressWarnings("unchecked")
  2. public <Z> Target<Z> buildTarget(ImageView view, Class<Z> clazz) {
  3. if (GlideDrawable.class.isAssignableFrom(clazz)) {
  4. return (Target<Z>) new GlideDrawableImageViewTarget(view);
  5. } else if (Bitmap.class.equals(clazz)) {
  6. ......
  7. }
  8. ......
  9. }

这里我们最终会返回GlideDrawableImageViewTarget对象,接下来就是request任务
GenericRequestBuilder.java

  1. public <Y extends Target<TranscodeType>> Y into(Y target) {
  2. Util.assertMainThread();
  3. if (target == null) {
  4. throw new IllegalArgumentException("You must pass in a non null Target");
  5. }
  6. if (!isModelSet) {
  7. throw new IllegalArgumentException("You must first set a model (try #load())");
  8. }
  9. // 这个target之前是否有存在Request任务
  10. Request previous = target.getRequest();
  11. if (previous != null) {
  12. // 如果存在,那么清除之前的Request
  13. previous.clear();
  14. requestTracker.removeRequest(previous);
  15. previous.recycle();
  16. }
  17. // 构建新的Request,下面会讲解到
  18. Request request = buildRequest(target);
  19. target.setRequest(request);
  20. lifecycle.addListener(target);
  21. // 运行Request,本文重点在这里
  22. requestTracker.runRequest(request);
  23. return target;
  24. }

上面代码我们主要关注下buildRequest(target)方法和requestTracker.runRequest(request)方法(本文重点)
GenericRequestBuilder.java

  1. private Request buildRequest(Target<TranscodeType> target) {
  2. if (priority == null) {
  3. priority = Priority.NORMAL;
  4. }
  5. return buildRequestRecursive(target, null);
  6. }
  7. private Request buildRequestRecursive(Target<TranscodeType> target, ThumbnailRequestCoordinator parentCoordinator) {
  8. if (thumbnailRequestBuilder != null) { // 是否有指定自定义缩略图的请求,本例中没有
  9. ......
  10. } else if (thumbSizeMultiplier != null) { // 是否有等比例缩放的请求,本例中没有
  11. ......
  12. } else {
  13. // 没有缩略图,构建一个GenericRequest对象
  14. return obtainRequest(target, sizeMultiplier, priority, parentCoordinator);
  15. }
  16. }

关于缩略图,具体看我的例子工程GlideSampleThumbnailActivity.java类,里面有关于缩略图的用法

好了,前面铺垫了这么多,其实还没有涉及到如何获取ImageView的大小,别急,马上就来~~
我们看下requestTracker.runRequest(target)方法
RequestTracker.java

  1. public void runRequest(Request request) {
  2. requests.add(request);
  3. if (!isPaused) {
  4. request.begin();
  5. } else {
  6. pendingRequests.add(request);
  7. }
  8. }

这个方法运行了封装好的GenericRequest.begin()类方法
GenericRequest.java

  1. public void begin() {
  2. // 记录Request运行起始时间
  3. startTime = LogTime.getLogTime();
  4. if (model == null) {
  5. onException(null);
  6. return;
  7. }
  8. // 设置为等待测量ImageView大小
  9. status = Status.WAITING_FOR_SIZE;
  10. // overrideWidth和overrideHeight是用户可能通过Glide.override(x,y)
  11. // 在显示图片前重新剪裁图片大小
  12. if (Util.isValidDimensions(overrideWidth, overrideHeight)) {
  13. 用户已经指定图片大小,无需测量ImageView大小
  14. onSizeReady(overrideWidth, overrideHeight);
  15. } else {
  16. // 这里的target是我们上文中得到的GlideDrawableImageViewTarget,
  17. // 通过target类设置了一个监听进去,来监听ImageView的图片固定
  18. target.getSize(this);
  19. }
  20. if (!isComplete() && !isFailed() && canNotifyStatusChanged()) {
  21. // 开始加载占位符图片(如果用户有设置占位符的情况下)
  22. target.onLoadStarted(getPlaceholderDrawable());
  23. }
  24. // 记录本次run的时间
  25. if (Log.isLoggable(TAG, Log.VERBOSE)) {
  26. logV("finished run method in " + LogTime.getElapsedMillis(startTime));
  27. }
  28. }

重点在这里target.getSize(this),这个类设置了
GlideDrawableImageViewTarget.java

  1. public void getSize(SizeReadyCallback cb) {
  2. sizeDeterminer.getSize(cb);
  3. }

GlideDrawableImageViewTarget.java

  1. public void getSize(SizeReadyCallback cb) {
  2. // 获取ImageView的准确宽度或者LayoutParams的常量值(例如:LayoutParams.WRAP_CONTENT)
  3. int currentWidth = getViewWidthOrParam();
  4. // 同上面获取宽度
  5. int currentHeight = getViewHeightOrParam();
  6. // 如果宽度是准确值,或者是LayoutParams.WRAP_CONTENT属性
  7. // 如果高度是准确值,或者是LayoutParams.WRAP_CONTENT属性
  8. if (isSizeValid(currentWidth) && isSizeValid(currentHeight)) {
  9. // 直接回调已获取到当前的宽高
  10. cb.onSizeReady(currentWidth, currentHeight);
  11. } else {
  12. // 加入到回调队列中
  13. if (!cbs.contains(cb)) {
  14. cbs.add(cb);
  15. }
  16. if (layoutListener == null) {
  17. // 获取ImageView的视图树监听
  18. final ViewTreeObserver observer = view.getViewTreeObserver();
  19. layoutListener = new SizeDeterminerLayoutListener(this);
  20. /*
  21. 重点来咯,添加一个OnPreDrawListener这个监听,关于这个监听的含义大体
  22. 是在视图绘制之前进行回调,绘制的时候,视图的肯定是经过测量过宽高了,别问我为什么,我会打人的
  23. */
  24. observer.addOnPreDrawListener(layoutListener);
  25. }
  26. }
  27. }

我们看下SizeDeterminerLayoutListener这个类,实现了ViewTreeObserver.OnPreDrawListener接口,通过onPreDraw方法,执行回调队列进行统一的回调

  1. private static class SizeDeterminerLayoutListener implements ViewTreeObserver.OnPreDrawListener {
  2. /* 这里为什么用弱引用,我想是因为我们的ViewTarget可能在测量ImageView前
  3. 有可能进行多次的Glide.into(),还记得我们在GenericRequestBuilder.into()方法吗?
  4. 里面有对之前的Request进行clear,recycle等操作,忘记的同学可以回头去看看代码
  5. */
  6. private final WeakReference<SizeDeterminer> sizeDeterminerRef;
  7. public SizeDeterminerLayoutListener(SizeDeterminer sizeDeterminer) {
  8. sizeDeterminerRef = new WeakReference<SizeDeterminer>(sizeDeterminer);
  9. }
  10. @Override
  11. public boolean onPreDraw() {
  12. if (Log.isLoggable(TAG, Log.VERBOSE)) {
  13. Log.v(TAG, "OnGlobalLayoutListener called listener=" + this);
  14. }
  15. SizeDeterminer sizeDeterminer = sizeDeterminerRef.get();
  16. if (sizeDeterminer != null) {
  17. // 统一回调,可以获取当前ImageView的宽高
  18. sizeDeterminer.checkCurrentDimens();
  19. }
  20. return true;
  21. }
  22. }

SizeDeterminer.java

  1. private void checkCurrentDimens() {
  2. if (cbs.isEmpty()) {
  3. return;
  4. }
  5. // 上面已经解释过了
  6. int currentWidth = getViewWidthOrParam();
  7. int currentHeight = getViewHeightOrParam();
  8. if (!isSizeValid(currentWidth) || !isSizeValid(currentHeight)) {
  9. return;
  10. }
  11. // 统一回调监听队列
  12. notifyCbs(currentWidth, currentHeight);
  13. // Keep a reference to the layout listener and remove it here
  14. // rather than having the observer remove itself because the observer
  15. // we add the listener to will be almost immediately merged into
  16. // another observer and will therefore never be alive. If we instead
  17. // keep a reference to the listener and remove it here, we get the
  18. // current view tree observer and should succeed.
  19. ViewTreeObserver observer = view.getViewTreeObserver();
  20. if (observer.isAlive()) {
  21. // 测量得到了结果,移除监听
  22. observer.removeOnPreDrawListener(layoutListener);
  23. }
  24. layoutListener = null;
  25. }

回调方法notifyCbs(currentWidth, currentHeight)最后回调GenericRequest.onSizeReady()方法对图片进行加载显示,本节暂时不对图片的加载源码进行分析
GenericRequest.java

  1. public void onSizeReady(int width, int height) {
  2. if (Log.isLoggable(TAG, Log.VERBOSE)) {
  3. logV("Got onSizeReady in " + LogTime.getElapsedMillis(startTime));
  4. }
  5. if (status != Status.WAITING_FOR_SIZE) {
  6. return;
  7. }
  8. status = Status.RUNNING;
  9. // 图片缩放参数,如果用户有设置thumbnail参数时`Glide.with(...).thumbnail(0.2f).into(...)`进行比例缩放
  10. // 默认值是1,不进行图片大小缩放
  11. width = Math.round(sizeMultiplier * width);
  12. height = Math.round(sizeMultiplier * height);
  13. // 图片加载
  14. ......
  15. ......
  16. ......
  17. }

总结

通过本节的源码分析,我们可以发现Glide将加载图片到ImageVIew封装成Request对象,在run Request的时候进行判断,如果ImageView有固定大小,那么就直接回调加载图片,如果图片还没有进行测量过,那么就设置监听ViewTreeObserver.OnPreDrawListener,等待ImageView绘制前回调监听获取视图大小,在加载确定大小的图片