有没有办法让View自己去控制自己的动画逻辑,放上去就能自己动?

当然是可以的。

但是怎么优雅的实现呢?
我们来试着分析一下Shimmer-android的源码。

##shimmer-android初步

###介绍

Shimmer-android是前一段时间由Facebook的工程师开源到GitHub里的库。提到Facebook是不是感觉逼格瞬间高了呢?
它可以让View呈现动态的闪光效果。先看看效果:
Shimmer

###用法
The following snippet shows how you can use ShimmerFrameLayout

1
2
3
4
5
6
7
<com.facebook.shimmer.ShimmerFrameLayout
android:id="@+id/shimmer_view_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
>

...(your complex view here)...
</com.facebook.shimmer.ShimmerFrameLayout>

In your code, you can then start the animation:

1
2
3
4
ShimmerFrameLayout container = 
(ShimmerFrameLayout) findViewById(R.id.shimmer_view_container);
container.startShimmerAnimation();
container.stopShimmerAnimation();

Setting

1
2
3
4
5
6
7
8
9
10
11
12
// Reset all parameters of the shimmer animation
container = container.useDefault();
// Slow and reverse
mShimmerViewContainer.setDuration(5000);
mShimmerViewContainer.setRepeatMode(ObjectAnimator.REVERSE);
// Thin, straight and transparent
mShimmerViewContainer.setBaseAlpha(0.1f);
mShimmerViewContainer.setDropoff(0.1f);
mShimmerViewContainer.setTilt(0);

// Sweep angle 90
mShimmerViewContainer.setAngle(ShimmerFrameLayout.MaskAngle.CW_90);

##源码分析
这个Module只有一个类,也就是ShimmerFrameLayout。留给外部使用的借口上面已经说明了,那么我们就从接口开始探究一下Facebook的工程师是怎样实现闪光效果的。

###StartAnimation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* Start the shimmer animation. If the 'auto start' property is set, this
* method is called automatically when the
* layout is attached to the current window. Calling this method has no effect
* if the animation is already playing.
*/

public void startShimmerAnimation() {
if (mAnimationStarted) {
return;
}
Animator animator = getShimmerAnimation();
animator.start();
mAnimationStarted = true;
}

在前面留一个mAnimationStarted标志位是很好的思路。
注释里写说Attached to the current window, 似乎有点小意思啊,不会是像SurfaceView拥有自己的Window吧,感觉要炸裂啊。

接下来,getShimmerAnimation()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// Get the shimmer <a href="http://developer.android.com/reference/android/animation/Animator.html">Animator</a>
// object, which is responsible for driving the highlight mask animation.
private Animator getShimmerAnimation() {
if (mAnimator != null) {
return mAnimator;
}
int width = getWidth();
int height = getHeight();
switch (mMask.shape) {
default:
case LINEAR:
switch (mMask.angle) {
default:
case CW_0:
mMaskTranslation.set(-width, 0, width, 0);
break;
case CW_90:
mMaskTranslation.set(0, -height, 0, height);
break;
case CW_180:
mMaskTranslation.set(width, 0, -width, 0);
break;
case CW_270:
mMaskTranslation.set(0, height, 0, -height);
break;
}
}
mAnimator = ValueAnimator.ofFloat(0.0f, 1.0f + (float) mRepeatDelay / mDuration);
mAnimator.setDuration(mDuration + mRepeatDelay);
mAnimator.setRepeatCount(mRepeatCount);
mAnimator.setRepeatMode(mRepeatMode);
mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float value = Math.max(0.0f, Math.min(1.0f, (Float) animation.getAnimatedValue()));
setMaskOffsetX((int) (mMaskTranslation.fromX * (1 - value) + mMaskTranslation.toX * value));
setMaskOffsetY((int) (mMaskTranslation.fromY * (1 - value) + mMaskTranslation.toY * value));
}
});
return mAnimator;
}

以上都是定义了一个Animator,这个Animator会控制View的移动方式频率幅度等等参数按照一定规律变化。详情请看属性动画

###DispatchDraw

ViewGroup.dispatchDraw(Canvas canvas)

1
2
3
4
5
6
/**
* Called by draw to draw the child views. This may be overridden
* by derived classes to gain control just before its children are drawn
* (but after its own view has been drawn).
* @param canvas the canvas on which to draw the view
*/

所以真正绘制出闪光效果还是在这个类中进行的。该方法中调用了dispatchDrawUsingBitmap(Canvas canvas)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* Draws and masks the children using a Bitmap.
*
* @param canvas Canvas that the masked children will end up being drawn to.
*/

private boolean dispatchDrawUsingBitmap(Canvas canvas) {
Bitmap unmaskBitmap = tryObtainRenderUnmaskBitmap();
Bitmap maskBitmap = tryObtainRenderMaskBitmap();
if (unmaskBitmap == null || maskBitmap == null) {
return false;
}
// First draw a desaturated version
drawUnmasked(new Canvas(unmaskBitmap));
canvas.drawBitmap(unmaskBitmap, 0, 0, mAlphaPaint);

// Then draw the masked version
drawMasked(new Canvas(maskBitmap));
canvas.drawBitmap(maskBitmap, 0, 0, null);

return true;
}

首先,分别Create了两个Bitmap,让我们看看创建过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
private Bitmap tryCreateRenderBitmap() {
int width = getWidth();
int height = getHeight();
try {
return createBitmapAndGcIfNecessary(width, height);
} catch (OutOfMemoryError e) {
String logMessage = "ShimmerFrameLayout failed to create working bitmap";
//...
Log.d(TAG, logMessage);
}
return null;
}

/**
* Creates a bitmap with the given width and height.
* <p/>
* If it fails with an OutOfMemory error, it will force a GC and then try to create the bitmap
* one more time.
*
* @param width width of the bitmap
* @param height height of the bitmap
*/

protected static Bitmap createBitmapAndGcIfNecessary(int width, int height) {
try {
return Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
} catch (OutOfMemoryError e) {
System.gc();
return Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
}
}

其次,分两次画出了想要的效果,第一次UnMask调用了super.dispatchDraw绘出了子View们,然后在Mask中绘除了闪光高亮区。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
// Draws the children and masks them on the given Canvas.
private void drawMasked(Canvas renderCanvas) {
Bitmap maskBitmap = getMaskBitmap();
if (maskBitmap == null) {
return;
}

renderCanvas.clipRect(
mMaskOffsetX,
mMaskOffsetY,
mMaskOffsetX + maskBitmap.getWidth(),
mMaskOffsetY + maskBitmap.getHeight());
super.dispatchDraw(renderCanvas);

renderCanvas.drawBitmap(maskBitmap, mMaskOffsetX, mMaskOffsetY, mMaskPaint);
}

// Return the mask bitmap, creating it if necessary.
private Bitmap getMaskBitmap() {
if (mMaskBitmap != null) {
return mMaskBitmap;
}

int width = mMask.maskWidth(getWidth());
int height = mMask.maskHeight(getHeight());

mMaskBitmap = createBitmapAndGcIfNecessary(width, height);
Canvas canvas = new Canvas(mMaskBitmap);
Shader gradient;
switch (mMask.shape) {
default:
case LINEAR: {
int x1, y1;
int x2, y2;
switch (mMask.angle) {
default:
case CW_0:
x1 = 0;
y1 = 0;
x2 = width;
y2 = 0;
break;
case CW_90:
x1 = 0;
y1 = 0;
x2 = 0;
y2 = height;
break;
case CW_180:
x1 = width;
y1 = 0;
x2 = 0;
y2 = 0;
break;
case CW_270:
x1 = 0;
y1 = height;
x2 = 0;
y2 = 0;
break;
}
gradient =
new LinearGradient(
x1, y1,
x2, y2,
mMask.getGradientColors(),
mMask.getGradientPositions(),
Shader.TileMode.REPEAT);
break;
}
case RADIAL: {
int x = width / 2;
int y = height / 2;
gradient =
new RadialGradient(
x,
y,
(float) (Math.max(width, height) / Math.sqrt(2)),
mMask.getGradientColors(),
mMask.getGradientPositions(),
Shader.TileMode.REPEAT);
break;
}
}
canvas.rotate(mMask.tilt, width / 2, height / 2);
Paint paint = new Paint();
paint.setShader(gradient);
// We need to increase the rect size to account for the tilt
int padding = (int) (Math.sqrt(2) * Math.max(width, height)) / 2;
canvas.drawRect(-padding, -padding, width + padding, height + padding, paint);

return mMaskBitmap;
}

Shader的意思就是做一个纹理处理,使得画到上面的物体都成为定义的格式(这里的渐变色)。
于是一部分Bitmap就被高亮了。
至于其它部分则是0.3f的透明度产生的效果。
具体的东西我将在下一篇博客中介绍。
其实很简单,对吧。如果不懂得可以联系我进一步交流。
danxionglei@foxmail.com