美文网首页
Glide遇坑记---Glide与CircleImageView

Glide遇坑记---Glide与CircleImageView

作者: Mr槑 | 来源:发表于2017-11-18 21:32 被阅读1070次

    Glide遇坑记之决战

    上述三种解决方法都避免了与真正BOSS---TransitionDrawable的正面交锋。TransitionDrawable继承自LayerDrawableLayerDrawable是一个特殊的Drawable,它内部保持着一个Drawable数组,其中每一个Drawable都是视图中的一层。通过多个Drawable的叠加、渐变、旋转等组合显示出与单一Drawable不同的效果。

    @Override
    public boolean animate(T current, ViewAdapter adapter) {
        Drawable previous = adapter.getCurrentDrawable();
        if (previous != null) {
            TransitionDrawable transitionDrawable = new TransitionDrawable(new Drawable[] { previous, current });
            transitionDrawable.setCrossFadeEnabled(true);
            transitionDrawable.startTransition(duration);
            adapter.setDrawable(transitionDrawable);
            return true;
        } else {
            defaultAnimation.animate(current, adapter);
            return false;
        }
    }
    
    public void startTransition(int durationMillis) {
        mFrom = 0;
        mTo = 255;
        mAlpha = 0;
        mDuration = mOriginalDuration = durationMillis;
        mReverse = false;
        mTransitionState = TRANSITION_STARTING;
        invalidateSelf();
    }
    

    DrawableCrossFadeViewAnimation.animate()方法先是获取TransitionDrawable对象实例,接着调用setCrossFadeEnabled()startTransition()方法对TransitionDrawable的成员变量进行设置。并在startTransition()方法的最后一行,调用invalidateSelf()方法尝试进行视图重绘。

    public void invalidateSelf() {
        final Callback callback = getCallback();
        if (callback != null) {
            callback.invalidateDrawable(this);
        }
    }
    

    invalidateSelf()方法先是获取TransitionDrawable注册的Callback实例,如果无则返回null。通过Callback接口,一个Drawable实例可以回调其客户端来执行动画。为了动画可以被执行,所有的客户端都应该支持这个Callback接口。 View类正是实现了Callback接口,所以callback.invalidateDrawable()其实调用的就是View中的invalidateDrawable()方法。 但此时TransitionDrawable实例未注册任何Callback接口,invalidateSelf()方法直接返回。


    紧接着animation()中执行adapter.setDrawable()方法,方法内部通过view.setImageDrawable(drawable)来更新Drawable

    public void setImageDrawable(@Nullable Drawable drawable) {
        if (mDrawable != drawable) {
            mResource = 0;
            mUri = null;
    
            final int oldWidth = mDrawableWidth;
            final int oldHeight = mDrawableHeight;
    
            updateDrawable(drawable);
    
            if (oldWidth != mDrawableWidth || oldHeight != mDrawableHeight) {
                requestLayout();
            }
            invalidate();
        }
    }
    

    view.setImageDrawable(drawable)方法内部先是通过updateDrawable(drawable)更新成员变量mDrawable,同时修改其属性。 接着调用invalidate()方法正式开始视图重绘。

    private void updateDrawable(Drawable d) {
        if (mDrawable != null) {
            sameDrawable = mDrawable == d;
            mDrawable.setCallback(null);
            unscheduleDrawable(mDrawable);
            if (!sCompatDrawableVisibilityDispatch && !sameDrawable && isAttachedToWindow()) {
                    mDrawable.setVisible(false, false);
            }
        }
    
        mDrawable = d;
    
        if (d != null) {
            d.setCallback(this);
            d.setLayoutDirection(getLayoutDirection());
            d.setLevel(mLevel);
            configureBounds();
        } else {
            mDrawableWidth = mDrawableHeight = -1;
        }
    }
    

    updateDrawble()首先对mDrawable做了一些检查,并将与ImageView关联的Drawable实例mDrawableCallback置空。接着把传进来的Drawable对象赋给成员变量mDrawable。如果参数d不为空的话,那么设置dCallbackImageView实例。通过d.getIntrinsicWidth()获取drawablewidth赋值全局变量mDrawableWidth

    Android视图重绘机制

    View的源码中会有数个invalidate()方法的重载和一个invalidateDrawable()方法,最终都是通过invalidateInternal()方法来实现视图重制。

    void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache, boolean fullInvalidate) {
        if (mGhostView != null) {
            mGhostView.invalidate(true);
            return;
        }
    
        if (skipInvalidate()) {
            return;
        }
    
        if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)) == (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)
                    || (invalidateCache && (mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID)
                    || (mPrivateFlags & PFLAG_INVALIDATED) != PFLAG_INVALIDATED|| (fullInvalidate && isOpaque() != mLastIsOpaque)) {
            if (fullInvalidate) {
                mLastIsOpaque = isOpaque();
                mPrivateFlags &= ~PFLAG_DRAWN;
            }
    
            mPrivateFlags |= PFLAG_DIRTY;
    
            if (invalidateCache) {
                mPrivateFlags |= PFLAG_INVALIDATED;
                mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
            }
    
            final AttachInfo ai = mAttachInfo;
            final ViewParent p = mParent;
            if (p != null && ai != null && l < r && t < b) {
                final Rect damage = ai.mTmpInvalRect;
                damage.set(l, t, r, b);
                p.invalidateChild(this, damage);
            }
        }
    }
    

    在这个方法中首先会调用skipInvalidate()方法来判断当前 View是否需要重绘,判断的逻辑也比较简单,如果View是不可见的且没有执行任何动画,就认为不需要重绘了。之后会进行透明度的判断,并给View添加一些标记位,然后调用ViewParent的invalidateChild()方法,这里的ViewParent其实就是当前视图的父视图,因此会调用到ViewGroupinvalidateChild()方法中。省略若干循环调用。最终经过多次辗转的调用,最终会走到视图绘制的入口方法performTraversals()中,然后重新执行绘制流程。

    invalidate()方法虽然最终会调用到performTraversals()方法中,但这时measurelayout流程是不会重新执行的,因为视图没有强制重新测量的标志位,而且大小也没有发生过变化,所以这时只有draw流程可以得到执行。而如果你希望视图的绘制流程可以完完整整地重新走一遍,就不能使用invalidate()方法,而应该调用requestLayout()

    绘制流程始于ViewRootImplperformDraw()方法,里面又调用了ViewRootImpldraw()方法,经过一系列调用,然后实例化Canvas对象,锁定该canvas的区域并进行一系列的属性赋值,最后调用了mView.draw(canvas)方法,这个mView就是DecorView,也就是说从DecorView开始绘制。由于ViewGroup没有重写draw方法,因此所有的View都是通过调用Viewdraw()方法实现绘制。

    public void draw(Canvas canvas) {
        final int privateFlags = mPrivateFlags;
        final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
                (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
        mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
    
        /*
         * Draw traversal performs several drawing steps which must be executed
         * in the appropriate order:
         *
         *      1. Draw the background
         *      2. If necessary, save the canvas' layers to prepare for fading
         *      3. Draw view's content
         *      4. Draw children
         *      5. If necessary, draw the fading edges and restore layers
         *      6. Draw decorations (scrollbars for instance)
         */
    
        // Step 1, draw the background, if needed
        int saveCount;
    
        if (!dirtyOpaque) {
            drawBackground(canvas);
        }
    
        // skip step 2 & 5 if possible (common case)
        final int viewFlags = mViewFlags;
        boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
        boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
        if (!verticalEdges && !horizontalEdges) {
            // Step 3, draw the content
            if (!dirtyOpaque) onDraw(canvas);
    
            // Step 4, draw the children
            dispatchDraw(canvas);
    
            // Overlay is part of the content and draws beneath Foreground
            if (mOverlay != null && !mOverlay.isEmpty()) {
                mOverlay.getOverlayView().dispatchDraw(canvas);
            }
    
            // Step 6, draw decorations (foreground, scrollbars)
            onDrawForeground(canvas);
    
            // we're done...
            return;
        }
        ...
    }
    

    draw过程比较复杂,但是逻辑十分清晰,一般是遵循下面几个步骤:

    • 绘制背景 -- drawBackground()
    • 绘制内容 -- onDraw()
    • 绘制孩子 -- dispatchDraw()
    • 绘制装饰 -- onDrawScrollbars()

    View中的onDraw()方法是一个空实现,不同的View有着不同的内容,这需要我们自己去实现,即在自定义View中重写该方法来实现。

    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
    
        if (mDrawable == null) {
            return; // couldn't resolve the URI
        }
    
        if (mDrawableWidth == 0 || mDrawableHeight == 0) {
            return;     // nothing to draw (empty bounds)
        }
    
        if (mDrawMatrix == null && mPaddingTop == 0 && mPaddingLeft == 0) {
            mDrawable.draw(canvas);
        } else {
            final int saveCount = canvas.getSaveCount();
            canvas.save();
    
            canvas.translate(mPaddingLeft, mPaddingTop);
    
            if (mDrawMatrix != null) {
                canvas.concat(mDrawMatrix);
            }
            mDrawable.draw(canvas);
            canvas.restoreToCount(saveCount);
        }
    }
    

    ImageViewonDraw()方法首先对mDrawable进行检查,mDrawable是否为空、宽高是否具有意义。在设置了mDrawMatrix的一系列方法后,onDraw()在绘制前会根据mDrawMatrix设置的值对图片资源进行相应的变换操作。无论Drawable缩放与否在满足mDrawable != null && mDrawableWidth != 0 && mDrawableHeight != 0的绘制条件下,最终都是通过mDrawable.draw(canvas)方法对mDrawable进行绘制。这里的mDrawableTransitionDrawable对象实例。

    public void draw(Canvas canvas) {
        boolean done = true;
    
        switch (mTransitionState) {
            case TRANSITION_STARTING:
                mStartTimeMillis = SystemClock.uptimeMillis();
                done = false;
                mTransitionState = TRANSITION_RUNNING;
                break;
    
            case TRANSITION_RUNNING:
                if (mStartTimeMillis >= 0) {
                    float normalized = (float)(SystemClock.uptimeMillis() - mStartTimeMillis) / mDuration;
                    done = normalized >= 1.0f;
                    normalized = Math.min(normalized, 1.0f);
                    mAlpha = (int) (mFrom  + (mTo - mFrom) * normalized);                                                
                }
                break;
        }
    
        final int alpha = mAlpha;
        final boolean crossFade = mCrossFade;
        final ChildDrawable[] array = mLayerState.mChildren;
    
        if (done) {
            if (!crossFade || alpha == 0) {
                array[0].mDrawable.draw(canvas);
            }
            if (alpha == 0xFF) {
                array[1].mDrawable.draw(canvas);
            }
            return;
        }
    
        Drawable d;
        d = array[0].mDrawable;
        if (crossFade) {
            d.setAlpha(255 - alpha);
        }
        d.draw(canvas);
        if (crossFade) {
            d.setAlpha(0xFF);
        }
    
        if (alpha > 0) {
            d = array[1].mDrawable;
            d.setAlpha(alpha);
            d.draw(canvas);
            d.setAlpha(0xFF);
        }
    
        if (!done) {
            invalidateSelf();
        }
    }
    

    调用adapter.setDrawable(transitionDrawable),进行视图重绘的流程中,实质还是调用TransitionDrawable.draw()方法完成自身绘制。TransitionDrawable.draw()方法的逻辑也是简单明了,d.setAlpha(alpha)d.draw(canvas),在不同阶段为两张Drawable设置对应透明度以此实现两个Drawable之间的淡入淡出效果。Drawable.draw()本身是个抽象方法,绘制具体逻辑由其子类实现。这里的drawable分别为GlideBitmapDrawableBitmapDrawable对象实例,具体为什么,在了解Drawable源码之后你就清楚了。(GlideBitmapDrawable则隐藏在Glide网络请求部分的源码之中)

    Drawable源码分析

    Drawable是一个用于处理各种可绘制资源的抽象类。我们使用Drawable最常见的情况就是将获取到的资源绘制到屏幕上。

    Drawable实例可能存在以下多种形式

    • Bitmap:最简单的Drawable形式,PNG或者JPEG图片。
    • .9图PNG的一个扩展,可以支持设置其如何填充内容,如何被拉伸。
    • Shape:包含简单的绘制指令,用于替代Bitmap,某些情况下对大小调整有更好表现。
    • Layers:一个复合的Drawable,按照层级进行绘制,单个Drawable实例绘制于其下层Drawable实例集合之上。
    • States:一个复合的Drawable,根据它的state选择一个Drawable集合。
    • Levels:一个复合的Drawable,根据它的level选择一个Drawable集合。
    • Scale:一个复合的Drawable和单个Drawable实例构成,它的总体尺寸由它的当前level值决定。

    Drawable常见使用步骤

    • 通过Resource获取Drawable实例
    • 将获取的Drawable实例当做背景设置给View或者作为ImageViewsrc进行显示:

    getResources().getDrawable()方法经过多次辗转的调用最终会通过ResourcesImpl实例的drawableFromBitmap()方法加载资源图片。

    private static Drawable drawableFromBitmap(Resources res, Bitmap bm, byte[] np, Rect pad, Rect layoutBounds, String srcName) {
    
        if (np != null) {
            return new NinePatchDrawable(res, bm, np, pad, layoutBounds, srcName);
        }
    
        return new BitmapDrawable(res, bm);
    }
    

    drawableFromBitmap()方法对于.9图返回1个NinePatchDrawable实例,普通图片返回1个BitmapDrawable实例。


    public boolean animate(T current, ViewAdapter adapter) {
        Drawable previous = adapter.getCurrentDrawable();
        if (previous != null) {
            TransitionDrawable transitionDrawable = new TransitionDrawable(new Drawable[] { previous, current });
            return true;
        } 
    } 
    

    TransitionDrawable在实例化时传入的previouscurrent分别来自adapter.getCurrentDrawable()方法和animate()方法传入的current参数。adapter.getCurrentDrawable()方法内部通过view.getDrawable()来获取与ImageView关联的Drawable实例。这个Drawable则来自getPlaceholderDrawable()方法。

    private Drawable getPlaceholderDrawable() {
            if (placeholderDrawable == null && placeholderResourceId > 0) {
                placeholderDrawable = context.getResources().getDrawable(placeholderResourceId);
            }
            return placeholderDrawable;
    }
    

    getPlaceholderDrawable()方法,通过Resource实例加载占位符placeHolder图片资源。没错,getPlaceholderDrawable()方法就像👆Drawable常见使用步骤,最终会通过ResourcesImpl.drawableFromBitmap()加载资源图片,返回1个BitmapDrawable实例。通过target.onLoadStarted(getPlaceholderDrawable())将获取的Drawable实例当做背景设置给ImageView

    LayerDrawable&Callback

    Android LayerDrawable and Drawable.Callback

    文章中之所以提到Callback是因为为ImageView设置占位符时ImageView的Callback指向当前的Drawable。
    当使用占位符作为子图层创建LayerDrawable实例时

        LayerDrawable(@NonNull Drawable[] layers, @Nullable LayerState state) {
            this(state, null);
    
            final int length = layers.length;
            final ChildDrawable[] r = new ChildDrawable[length];
            for (int i = 0; i < length; i++) {
                r[i] = new ChildDrawable(mLayerState.mDensity);
                r[i].mDrawable = layers[i];
                layers[i].setCallback(this);
                mLayerState.mChildrenChangingConfigurations |= layers[i].getChangingConfigurations();
            }
        }
    
    BitmapDrawable&GlideBitmapDrawable

    Drawable.draw()本身是个抽象方法,绘制具体逻辑由其子类实现。TransitionDrawable.draw() 方法最终还是通过d.setAlpha(alpha)d.draw(canvas),在不同阶段为Drawable设置对应透明度以此实现两个Drawable之间的淡入淡出效果。

    public void setAlpha(int alpha) {
        final int oldAlpha = mBitmapState.mPaint.getAlpha();
        if (alpha != oldAlpha) {
            mBitmapState.mPaint.setAlpha(alpha);
            invalidateSelf();
        }
    }
    

    LayerDrawable中,每层视图(Drawable)都会将LayerDrawable注册为它的Drawable.Callback。从而允许Drawable在需要重绘自己的时候能够告知LayerDrawable重绘它。LayerDrawable最终调用到View中的invalidateDrawable()方法,之后就会按照我们前面分析的流程执行重绘逻辑,以此改变视图背景。

    public void draw(Canvas canvas) {
        final Bitmap bitmap = mBitmapState.mBitmap;
        if (bitmap == null) {
            return;
        }
    
        final BitmapState state = mBitmapState;
        final Paint paint = state.mPaint;
    
        final Shader shader = paint.getShader();
        if (shader == null) {
            if (needMirroring) {
                canvas.save();
                canvas.translate(mDstRect.right - mDstRect.left, 0);
                canvas.scale(-1.0f, 1.0f);
            }
    
            canvas.drawBitmap(bitmap, null, mDstRect, paint);
    
        } else {
            updateShaderMatrix(bitmap, paint, shader, needMirroring);
            canvas.drawRect(mDstRect, paint);
        }
    }
    

    BitmapDrawable.draw()方法先是对画笔Paint和画布Canvas进行相应设置,接着将Drawable实例中的Bitmap绘制到View实例关联的画布上。

    GlideBitmapDrawable绘制逻辑与BitmapDrawable基本相同,便不再赘述。


    至此,我们已经了解了TransitionDrawable实现渐变的原理,及与相关知识(Android视图重绘机制、Drawable及其实现类源码)。是不是觉得头昏脑胀,不知所云~~~ 不不不,应该是虽然学到了很多知识,并没有发现问题的存在~~~

    还记得吗,这个坑只会在使用CircleImageView的情况下出现,对于ImageView,即便是在使用占位符和默认动画的情况下Glide仍可以正常工作。那CircleImageViewImageView两者之间又是存在何种差异导致了问题的出现?

    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
    
        if (mDrawable == null) {
            return; // couldn't resolve the URI
        }
    
        if (mDrawableWidth == 0 || mDrawableHeight == 0) {
            return;     // nothing to draw (empty bounds)
        }
    
        if (mDrawMatrix == null && mPaddingTop == 0 && mPaddingLeft == 0) {
            mDrawable.draw(canvas);
        } else {
            final int saveCount = canvas.getSaveCount();
            canvas.save();
    
            canvas.translate(mPaddingLeft, mPaddingTop);
    
            if (mDrawMatrix != null) {
                canvas.concat(mDrawMatrix);
            }
            mDrawable.draw(canvas);
            canvas.restoreToCount(saveCount);
        }
    }
    

    ImageView.onDraw()方法通过mDrawable.draw()方法对mDrawable进行绘制并实现淡入淡出效果。👆有对ImageView.onDraw()方法更为详细的分析过程,没错就是Android视图重绘机制哪里~

    protected void onDraw(Canvas canvas) {
        if (mDisableCircularTransformation) {
            onDraw(canvas);
            return;
        }
    
        if (mBitmap == null) {
            return;
        }
    
        if (mFillColor != Color.TRANSPARENT) {
            canvas.drawCircle(mDrawableRect.centerX(), mDrawableRect.centerY(), mDrawableRadius, mFillPaint);
        }
        canvas.drawCircle(mDrawableRect.centerX(), mDrawableRect.centerY(), mDrawableRadius, mBitmapPaint);
        if (mBorderWidth > 0) {
            canvas.drawCircle(mBorderRect.centerX(), mBorderRect.centerY(), mBorderRadius, mBorderPaint);
        }
    }
    

    通过上面的分析,我们终于可以得出只显示占位符placeHolder的原因。Glide在网络中获取图片并经过解码、逻辑操作(包括对图片的压缩,甚至还有旋转、圆角等逻辑处理)之后,会最终回调到GlideDrawableImageViewTarget.onResourceReady()方法来设置ImageView。一番处理之后,会调用父类ImageViewTargetonResourceReady()方法。

    onResourceReady()方法通过对glideAnimation进行判空,对glideAnimation.animate()返回值进行分析,来决定是否执行setResource()方法。根据animationFactory引用工厂对象的不同,onResourceReady()方法可能传入DrawableCrossFadeViewAnimationNoAnimation对象实例。

    DrawableCrossFadeViewAnimation.animate()方法内部先是获取先前通过placeHolder()设置占位符占位符previous。如previous不为空,则通过TransitionDrawable设置动画并添加图片至ImageView。否则通过defaultAnimation展示图片。

    CircleImageView.onDraw()方法仅是通过canvas.drawCircle()方法将Drawable实例中的 Bitmap经过裁剪之后绘制到CircleImageView实例关联的画布上。没错,与ImageView.onDraw()方法相比缺少了对mDrawable.draw()方法的调用,而mDrawable.draw()方法则会不断调用invalidateSelf()方法获取其关联的View进行重复的视图重绘操作,通过不断调用TransitionDrawable.draw()方法,设置两个Drawable透明度从而实现渐入渐出效果的实现

    除上述我个人的结论之外,网上也有一些分析的文章说:根本原因就是你的placeholder图片和你要加载显示的图片宽高比不一样,而Android的TransitionDrawable无法很好地处理不同宽高比的过渡问题,这的确是个Bug,是Android的也是Glide的。 文章实在是太长了😂,对此就先挖个坑,回头再填~

    至此Glide遇坑记之Glide与CircleImageView的分析就告一段落~~
    以上分析均是个人见解。如果错误或疏忽请及时指出,O(∩_∩)O谢谢!

    参考文章

    Glide v4快速高效的Android图片加载库

    Android图片加载框架最全解析,从源码的角度理解Glide的执行流程

    详谈高大上的图片加载框架Glide -源码篇

    Android Drawable完全解析

    Android LayerDrawable and Drawable.Callback

    相关文章

      网友评论

          本文标题:Glide遇坑记---Glide与CircleImageView

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