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

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

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

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

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

例子

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

分析

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

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

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

Glide.java

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

ImageViewTargetFactory.java

    @SuppressWarnings("unchecked")
    public <Z> Target<Z> buildTarget(ImageView view, Class<Z> clazz) {
        if (GlideDrawable.class.isAssignableFrom(clazz)) {
            return (Target<Z>) new GlideDrawableImageViewTarget(view);
        } else if (Bitmap.class.equals(clazz)) {
            ......
        }
        ......
    }

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

public <Y extends Target<TranscodeType>> Y into(Y target) {
        Util.assertMainThread();
        if (target == null) {
            throw new IllegalArgumentException("You must pass in a non null Target");
        }
        if (!isModelSet) {
            throw new IllegalArgumentException("You must first set a model (try #load())");
        }
        
        // 这个target之前是否有存在Request任务
        Request previous = target.getRequest();

        if (previous != null) {
            // 如果存在,那么清除之前的Request
            previous.clear();
            requestTracker.removeRequest(previous);
            previous.recycle();
        }
        // 构建新的Request,下面会讲解到
        Request request = buildRequest(target);
        target.setRequest(request);
        lifecycle.addListener(target);
        // 运行Request,本文重点在这里
        requestTracker.runRequest(request);

        return target;
    }

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

private Request buildRequest(Target<TranscodeType> target) {
        if (priority == null) {
            priority = Priority.NORMAL;
        }
        return buildRequestRecursive(target, null);
    }

    private Request buildRequestRecursive(Target<TranscodeType> target, ThumbnailRequestCoordinator parentCoordinator) {
        if (thumbnailRequestBuilder != null) {  // 是否有指定自定义缩略图的请求,本例中没有
            ......
        } else if (thumbSizeMultiplier != null) {  // 是否有等比例缩放的请求,本例中没有
           ......
        } else {
            // 没有缩略图,构建一个GenericRequest对象
            return obtainRequest(target, sizeMultiplier, priority, parentCoordinator);
        }
    }

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

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

public void runRequest(Request request) {
        requests.add(request);
        if (!isPaused) {
            request.begin();
        } else {
            pendingRequests.add(request);
        }
    }

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

public void begin() {
       // 记录Request运行起始时间
        startTime = LogTime.getLogTime();
        if (model == null) {
            onException(null);
            return;
        }
        // 设置为等待测量ImageView大小
        status = Status.WAITING_FOR_SIZE;
       // overrideWidth和overrideHeight是用户可能通过Glide.override(x,y)
       // 在显示图片前重新剪裁图片大小
        if (Util.isValidDimensions(overrideWidth, overrideHeight)) {
            用户已经指定图片大小,无需测量ImageView大小
            onSizeReady(overrideWidth, overrideHeight);
        } else {
            // 这里的target是我们上文中得到的GlideDrawableImageViewTarget,
            // 通过target类设置了一个监听进去,来监听ImageView的图片固定
            target.getSize(this);
        }

        if (!isComplete() && !isFailed() && canNotifyStatusChanged()) {
            // 开始加载占位符图片(如果用户有设置占位符的情况下)
            target.onLoadStarted(getPlaceholderDrawable());
        }
        // 记录本次run的时间
        if (Log.isLoggable(TAG, Log.VERBOSE)) {
            logV("finished run method in " + LogTime.getElapsedMillis(startTime));
        }
    }

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

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

GlideDrawableImageViewTarget.java

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

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

private static class SizeDeterminerLayoutListener implements ViewTreeObserver.OnPreDrawListener {
            /* 这里为什么用弱引用,我想是因为我们的ViewTarget可能在测量ImageView前
               有可能进行多次的Glide.into(),还记得我们在GenericRequestBuilder.into()方法吗?
               里面有对之前的Request进行clear,recycle等操作,忘记的同学可以回头去看看代码
            */
            private final WeakReference<SizeDeterminer> sizeDeterminerRef;

            public SizeDeterminerLayoutListener(SizeDeterminer sizeDeterminer) {
                sizeDeterminerRef = new WeakReference<SizeDeterminer>(sizeDeterminer);
            }

            @Override
            public boolean onPreDraw() {
                if (Log.isLoggable(TAG, Log.VERBOSE)) {
                    Log.v(TAG, "OnGlobalLayoutListener called listener=" + this);
                }
                SizeDeterminer sizeDeterminer = sizeDeterminerRef.get();
                if (sizeDeterminer != null) {
                    // 统一回调,可以获取当前ImageView的宽高
                    sizeDeterminer.checkCurrentDimens();
                }
                return true;
            }
        }

SizeDeterminer.java

private void checkCurrentDimens() {
            if (cbs.isEmpty()) {
                return;
            }
            // 上面已经解释过了
            int currentWidth = getViewWidthOrParam();
            int currentHeight = getViewHeightOrParam();
            if (!isSizeValid(currentWidth) || !isSizeValid(currentHeight)) {
                return;
            }
            // 统一回调监听队列
            notifyCbs(currentWidth, currentHeight);
            // Keep a reference to the layout listener and remove it here
            // rather than having the observer remove itself because the observer
            // we add the listener to will be almost immediately merged into
            // another observer and will therefore never be alive. If we instead
            // keep a reference to the listener and remove it here, we get the
            // current view tree observer and should succeed.
            ViewTreeObserver observer = view.getViewTreeObserver();
            if (observer.isAlive()) {
                // 测量得到了结果,移除监听
                observer.removeOnPreDrawListener(layoutListener);
            }
            layoutListener = null;
        }

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

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

总结

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容