Glide遇坑记---Glide与CircleImageView

2017-11-18  本文已影响1070人  Mr槑

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过程比较复杂,但是逻辑十分清晰,一般是遵循下面几个步骤:

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实例可能存在以下多种形式

Drawable常见使用步骤

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

上一篇下一篇

猜你喜欢

热点阅读