TransitionDrawable实现图片淡入淡出效果如下:
基本使用
使用TransitionDrawable方案实现切换图片时淡入淡出效果,代码一般如下:
val drawable: Drawable = BitmapDrawable(ContextProxy.getAppContext().resources, res)
val current = img!!.background
if (current is TransitionDrawable) {
val currentTransition = current
showLayer = currentTransition.getDrawable(currentTransition.numberOfLayers - 1)
transitionDrawable = TransitionDrawable(arrayOf(showLayer, drawable))
transitionDrawable!!.isCrossFadeEnabled = true
img!!.background = transitionDrawable
transitionDrawable!!.startTransition(600)
}
关键代码分析
- TransitionDrawable对象创建
transitionDrawable = TransitionDrawable(arrayOf(showLayer, drawable))
TransitionDrawable继承于LayerDrawable,在创建对象实例时,会把layers层的Drawable的Callback设置为自己:
LayerDrawable(@NonNull Drawable[] layers, @Nullable LayerState state) {
this(state, null);
if (layers == null) {
throw new IllegalArgumentException("layers must be non-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();
}
mLayerState.mNumChildren = length;
mLayerState.mChildren = r;
ensurePadding();
refreshPadding();
}
2.背景设置
img!!.background = transitionDrawable
ImageView继承于View,设置背景处理如下:
public void setBackground(Drawable background) {
//noinspection deprecation
setBackgroundDrawable(background);
}
public void setBackgroundDrawable(Drawable background) {
computeOpaqueFlags();
if (background == mBackground) {
return;
}
...
if (requestLayout) {
requestLayout();
}
mBackgroundSizeChanged = true;
invalidate(true);
invalidateOutline();
}
方法内部最后调用了invalidate,通过主线程Handler的消息分发最后会触发performTraversals流程(具体流程在View绘制流程中有介绍),最终会调用当前View的draw方法,在回顾下View绘制流程中draw方法:
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);
}
...
draw函数的第一步就是绘制背景:
private void drawBackground(Canvas canvas) {
final Drawable background = mBackground;
if (background == null) {
return;
}
...
final int scrollX = mScrollX;
final int scrollY = mScrollY;
if ((scrollX | scrollY) == 0) {
background.draw(canvas);
} else {
canvas.translate(scrollX, scrollY);
background.draw(canvas);
canvas.translate(-scrollX, -scrollY);
}
}
从函数流程看到了background.draw,这个background就是之前我们设置的TransitionDrawable
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) {
// the setAlpha() calls below trigger invalidation and redraw. If we're done, just draw
// the appropriate drawable[s] and return
if (!crossFade || alpha == 0) {
array[0].mDrawable.draw(canvas);
}
if (alpha == 0xFF) {
array[1].mDrawable.draw(canvas);
}
return;
}
...
}
从流程可以看到TransitionDrawable在被设置为背景,未执行startTransition前,仅仅是绘制了两个图层,TransitionDrawable的layer是BitmapDrawable, 那么看下BitmapDrawable的draw函数:
public void draw(Canvas canvas) {
final Bitmap bitmap = mBitmapState.mBitmap;
if (bitmap == null) {
return;
}
...
if (shader == null) {
if (needMirroring) {
canvas.save();
// Mirror the bitmap
canvas.translate(mDstRect.right - mDstRect.left, 0);
canvas.scale(-1.0f, 1.0f);
}
canvas.drawBitmap(bitmap, null, mDstRect, paint);
if (needMirroring) {
canvas.restore();
}
} else {
updateShaderMatrix(bitmap, paint, shader, needMirroring);
canvas.drawRect(mDstRect, paint);
}
...
}
可以看到,最终会canvas.drawBitmap绘制出图片。
大致流程如下:
3.执行淡入淡出动画
public void startTransition(int durationMillis) {
mFrom = 0;
mTo = 255;
mAlpha = 0;
mDuration = mOriginalDuration = durationMillis;
mReverse = false;
mTransitionState = TRANSITION_STARTING;
invalidateSelf();
}
设置淡入淡出动画配置参数和状态,最后调用了invalidateSelf
public void invalidateSelf() {
final Callback callback = getCallback();
if (callback != null) {
callback.invalidateDrawable(this);
}
}
public Callback getCallback() {
return mCallback != null ? mCallback.get() : null;
}
可以看到这里会调用Callback的invalidateDrawable,还记得之前说过的嘛?ImageView在调用setBackground函数时,会设置Drawable的Callback为自己,那么invalidateSelf就会调用到ImageView的invalidateDrawable:
@Override
public void invalidateDrawable(@NonNull Drawable dr) {
if (dr == mDrawable) {
...
invalidate();
} else {
super.invalidateDrawable(dr);
}
}
mDrawable未发生变化时,会调用到父类View的invalidateDrawable方法,触发invalidate
public void invalidateDrawable(@NonNull Drawable drawable) {
if (verifyDrawable(drawable)) {
final Rect dirty = drawable.getDirtyBounds();
final int scrollX = mScrollX;
final int scrollY = mScrollY;
invalidate(dirty.left + scrollX, dirty.top + scrollY,
dirty.right + scrollX, dirty.bottom + scrollY);
rebuildOutline();
}
}
分析到这一步,基本已经了解了TransitionDrawable的实现原理了。那么淡入淡出的核心逻辑处理在哪呢?
@Override
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) {
// the setAlpha() calls below trigger invalidation and redraw. If we're done, just draw
// the appropriate drawable[s] and return
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();
}
}
从以上流程上可以看到,通过mTransitionState的状态来计算当前的透明度值mAlpha,再对两个Drawable图层分别做alpha动画,其中两个Drawable的alpha值总和是255。
float normalized = (float) (SystemClock.uptimeMillis() - mStartTimeMillis) / mDuration;
done = normalized >= 1.0f;
TransitionDrawable通过不断判断当前时间是否超过动画时间,如果未超过执行时间,会不断的通过invalidateSelf来触发ImageView的invalidate,间接触发自身两个layer图层的透明度绘制,从而实现图片淡入淡出效果。