美文网首页优秀案例Android源码分析
Glide源码分析(二) 如何动态测量获取ImageView大小

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

作者: 蛋西 | 来源:发表于2017-01-24 22:33 被阅读654次

    我们都知道,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绘制前回调监听获取视图大小,在加载确定大小的图片

    相关文章

      网友评论

        本文标题:Glide源码分析(二) 如何动态测量获取ImageView大小

        本文链接:https://www.haomeiwen.com/subject/xhzibttx.html