一、回顾
hello,这节接着上一节介绍RippleDrawable
的水波实现效果,顺便带着大家自己动手实现一款带水波的自定义view。好了废话不多说,还是像往常一样,先用一个demo来回顾水波的使用:
定义一个水波的xml:
然后在view上可以这么使用:
这里没用foreground属性是因为在前面介绍了foreground是前置背景,因此用了background属性来代替,在android中drawable显示到view上的过程里说过
background
属性和foreground
属性的如果有点击效果,需要设置view.setClickable(true)
或者view.setOnClickListener
。下面正式进入正片:
代码都是在android-27下分析,在android-28下的点击波纹效果还不太一样,这里先申明下
二、概述
-
RippleDrawable
里面通过RippleForeground
和RippleBackground
两个类的动画来控制水波画圆的半径和圆心的位置,以及画圆的透明度 -
RippleForeground
和RippleBackground
是RippleComponent的子类,在RippleDrawble的绘制部分会先去画RippleDrawale的item部分,并且该item部分的id不是mask。紧接着绘制RippleBackground部分,如果RippleBackground是isVisible才会去绘制,后面会讲到什么时候是isVisible;紧接着绘制exit的时候没有绘制完的rippleForeground动画,所以在连续点得很快的时候,会有一层一层波纹的效果。 -
RippleForeground
创建了softWare
和hardWare
的动画,默认情况下,如果rippleDrawable
是isBound,RippleForeground
的enterSoftWare
动画是不创建的(注意:enter不创建该动画是在27上面的,也就是手按下的时候),我在28上面看到的动画效果在按下的时候就有波纹效果,因此可以猜测28上面在按下的时候是创建了enterSoftWare动画的。 -
RippleBackground
中也是创建了softWare
和hardWare
动画,而RippleBackground
中创建动画的前提是view中的canvas.isHardwareAccelerated(),才能去绘制drawHardWare动画,默认情况下是没开启硬件加速的情况,因此drawHardWare动画是不会绘制的。 -
RippleForeground#createSoftwareEnter
融合了三个动画,有水波半径的增大、圆心渐变、透明度渐变的动画。 -
RippleForeground#createSoftwareExit
融合了三个动画,有水波半径的增大、圆心渐变、透明度渐变的动画。和enter的区别就是enter的透明度是0到1,而exit的透明度是1到0的过程。 -
RippleForeground#drawSoftware
该处是绘制的关键,主要在绘制的时候改变画笔的透明度、绘制圆的圆心、改变圆的半径大小。 -
RippleBackground#drawSoftware
在它的绘制里面就是画的一个固定的圆,圆心始终是(0,0),半径大小不变。 - 在手按下view和抬起view的时候,绘制流程是首先触发
RippleDrawable
的onStateChange
方法,会调用RippleForeground
的enter
和setup
方法,随后创建了softWare
的动画,在动画里面不断地调用了RippleDrawable
的invalidateSelf
方法,然后会触发RippleForeground
和RippleBackground
的draw
方法,随即到父类RippleComponent
的draw方法,而RippleComponent
方法会触发drawSoftWare
方法,最终到RippleForeground
的drawSoftWare
方法。
三、RippleDrawable的初始化
3.1 RippleDrawable#inflate
还记得在第一篇介绍drawable的时候,说过drawable初始化是从inflate方法开始的不,知道这个直接看RippleDrawable的初始化,在inflate方法中调用了父类的inflate方法和updateStateFromTypedArray
方法:
private void updateStateFromTypedArray(@NonNull TypedArray a) throws XmlPullParserException {
//RippleState是RippleDrawable的子类,继承自父类LayerDrawable的LayerState
final RippleState state = mState;
//看到了没,上面例子中为什么要定义一个ripple_color.xml,这里就是获取到一个ColorStateList
final ColorStateList color = a.getColorStateList(R.styleable.RippleDrawable_color);
//获取到的ColorStateList交给了RippleState.mColor
if (color != null) {
mState.mColor = color;
}
//获取一个半径的属性,在demo里面没设置,所以这里用默认的mState.mMaxRadius的值
mState.mMaxRadius = a.getDimensionPixelSize(
R.styleable.RippleDrawable_radius, mState.mMaxRadius);
}
初始化中将获取到ripple
标签的color属性和radius属性,赋值给了RippleState。
3.2 RippleDrawable#inflateLayers
再来看下父类的inflate方法,这个得去LayerDrawable的inflate方法,该方法中调用了inflateLayers
方法,用来初始化里面的item:
private void inflateLayers(@NonNull Resources r, @NonNull XmlPullParser parser,
@NonNull AttributeSet attrs, @Nullable Theme theme)
throws XmlPullParserException, IOException {
final LayerState state = mLayerState;
final int innerDepth = parser.getDepth() + 1;
int type;
int depth;
while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
&& ((depth = parser.getDepth()) >= innerDepth || type != XmlPullParser.END_TAG)) {
if (type != XmlPullParser.START_TAG) {
continue;
}
if (depth > innerDepth || !parser.getName().equals("item")) {
continue;
}
final ChildDrawable layer = new ChildDrawable(state.mDensity);
final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.LayerDrawableItem);
//此处是解析item属性的地方
updateLayerFromTypedArray(layer, a);
a.recycle();
if (layer.mDrawable == null && (layer.mThemeAttrs == null ||
layer.mThemeAttrs[R.styleable.LayerDrawableItem_drawable] == 0)) {
//如果item标签定义的是drawable的xml文件调走这里
layer.mDrawable = Drawable.createFromXmlInner(r, parser, attrs, theme);
layer.mDrawable.setCallback(this);
state.mChildrenChangingConfigurations |=
layer.mDrawable.getChangingConfigurations();
}
//将每一个ChildDrawable添加到LayerState中
addLayer(layer);
}
}
3.3 RippleDrawable#addLayer
可以看到如果标签是item生成一个ChildDrawable对象,解析item在updateLayerFromTypedArray
方法里:
private void updateLayerFromTypedArray(@NonNull ChildDrawable layer, @NonNull TypedArray a) {
final LayerState state = mLayerState;
final int N = a.getIndexCount();
for (int i = 0; i < N; i++) {
final int attr = a.getIndex(i);
switch (attr) {
//获取id,省略了其他属性的获取,这里就不介绍了,大家自己尝试
case R.styleable.LayerDrawableItem_id:
layer.mId = a.getResourceId(attr, layer.mId);
break;
}
}
//获取drawable属性
final Drawable dr = a.getDrawable(R.styleable.LayerDrawableItem_drawable);
if (dr != null) {
if (layer.mDrawable != null) {
layer.mDrawable.setCallback(null);
}
//将获取到的drawable值放到ChildDrawable中
layer.mDrawable = dr;
layer.mDrawable.setCallback(this);
state.mChildrenChangingConfigurations |=
layer.mDrawable.getChangingConfigurations();
}
}
该方法里面先是遍历除了drawable值以外,其他的属性都获取了,比如id属性,还有其他的比如width、gravity属性等就不说了,大家自己尝试。
紧接着就是获取到drawable属性值,将drawable值放到ChildDrawable中。updateLayerFromTypedArray
完事了后,紧接着最后就是addLayer
了,这个其实跟上一节介绍StateListDrawable
的addState
类似:
int addLayer(@NonNull ChildDrawable layer) {
final LayerState st = mLayerState;
final int N = st.mChildren != null ? st.mChildren.length : 0;
final int i = st.mNumChildren;
if (i >= N) {
final ChildDrawable[] nu = new ChildDrawable[N + 10];
if (i > 0) {
//数组扩容到10个元素的大小
System.arraycopy(st.mChildren, 0, nu, 0, i);
}
st.mChildren = nu;
}
将上面生成的ChildDrawable放到了LayerState的mChildren数组中
st.mChildren[i] = layer;
st.mNumChildren++;
st.invalidateCache();
return i;
}
在addLayer方法中也是将LayerState
中的mChildren数组扩容到10个元素的大小,然后将传过来的ChildDrawable
放到了LayerState
的mChildren
数组中。到此,RippleDrawable的初始化讲解完了,我们来回顾下:
- 在
inflate
方法中首先调用了父类LayerDrawable
的inflate
方法,在inflate
方法中解析每一个item
标签,每一个item
标签对应一个ChildDrawable
,其中解析完了id等属性之后,紧接着解析drawable属性的值,将属性值依次放到ChildDrawable
中。- 将上面解析好的
ChildDrawable
依次添加到LayerDrawable
中的LayerState
数组mChildren
里。- 在
RippleDrawable
中的inflate方法中,初始化了ripple
标签中的color和radius属性值,然后放到RippleState
中。
3.5 初始化mask部分
初始化mask需要到ppleDrawable.updateLocalState
法看下:
private void updateLocalState() {
// Initialize from constant state.
mMask = findDrawableByLayerId(R.id.mask);
}
public Drawable findDrawableByLayerId(int id) {
final ChildDrawable[] layers = mLayerState.mChildren;
for (int i = mLayerState.mNumChildren - 1; i >= 0; i--) {
if (layers[i].mId == id) {
return layers[i].mDrawable;
}
}
return null;
}
上面两个方法不用解释了吧,获取id=R.id.mask的layer,讲获取到的drawable放到mMask全局drawale里面,后面绘制会用到。
四、RippleDrawable的绘制
4.1 RippleDrawable#draw
关于drawable的绘制,直接看RippleDrawable的draw方法:
@Override
public void draw(@NonNull Canvas canvas) {
pruneRipples();
// Clip to the dirty bounds, which will be the drawable bounds if we
// have a mask or content and the ripple bounds if we're projecting.
final Rect bounds = getDirtyBounds();
//先保存canvas的状态
final int saveCount = canvas.save(Canvas.CLIP_SAVE_FLAG);
//裁剪drawable的区域
canvas.clipRect(bounds);
//绘制content部分
drawContent(canvas);
//绘制波纹部分
drawBackgroundAndRipples(canvas);
还原canvas的状态
canvas.restoreToCount(saveCount);
}
4.2 RippleDrawable#drawContent
private void drawContent(Canvas canvas) {
// Draw everything except the mask.
final ChildDrawable[] array = mLayerState.mChildren;
final int count = mLayerState.mNumChildren;
for (int i = 0; i < count; i++) {
if (array[i].mId != R.id.mask) {
array[i].mDrawable.draw(canvas);
}
}
}
很清晰,直接绘制item的id不是mask的drawable。在开篇的事例中,不带id=mask的drawable="#cccccc",此处是一个colorDrawable。
4.3 绘制background、Ripples部分
这部分是波纹效果的关键,看下drawBackgroundAndRipples方法:
private void drawBackgroundAndRipples(Canvas canvas) {
//绘制水波的动画类
final RippleForeground active = mRipple;
//绘制背景的动画类
final RippleBackground background = mBackground;
//抬起的次数
final int count = mExitingRipplesCount;
if (active == null && count <= 0 && (background == null || !background.isVisible())) {
return;
}
//获取到点击时的坐标
final float x = mHotspotBounds.exactCenterX();
final float y = mHotspotBounds.exactCenterY();
//将画布偏移到点击的坐标位置
canvas.translate(x, y);
//绘制mask部分
updateMaskShaderIfNeeded();
// Position the shader to account for canvas translation.
if (mMaskShader != null) {
final Rect bounds = getBounds();
mMaskMatrix.setTranslate(bounds.left - x, bounds.top - y);
mMaskShader.setLocalMatrix(mMaskMatrix);
}
//如果在ripple标签的color属性值的颜色没有透明度,默认透明度是255/2
//得到alpha值后的一半,再往左移24位正好是得到透明度的16进制值
//11111111 11111111 11111111 11111111
// alpha值左移24位跑到最前面去了
final int color = mState.mColor.getColorForState(getState(), Color.BLACK);
final int halfAlpha = (Color.alpha(color) / 2) << 24;
final Paint p = getRipplePaint();
//默认为空
if (mMaskColorFilter != null) {
final int fullAlphaColor = color | (0xFF << 24);
mMaskColorFilter.setColor(fullAlphaColor);
p.setColor(halfAlpha);
p.setColorFilter(mMaskColorFilter);
p.setShader(mMaskShader);
} else {
//color值位与之后再与alpha值进行或运算
final int halfAlphaColor = (color & 0xFFFFFF) | halfAlpha;
p.setColor(halfAlphaColor);
p.setColorFilter(null);
p.setShader(null);
}
//如果background不为空,并且isVisible才去绘制background
if (background != null && background.isVisible()) {
background.draw(canvas, p);
}
//将每一次exit的ripple依次绘制出来,可以看出来该处是绘制波纹效果的关键,
if (count > 0) {
final RippleForeground[] ripples = mExitingRipples;
for (int i = 0; i < count; i++) {
ripples[i].draw(canvas, p);
}
}
//当前次的rippleForeground绘制
if (active != null) {
active.draw(canvas, p);
}
//还原画布的偏移量
canvas.translate(-x, -y);
}
上面在绘制ripple和background:
- 获取到点击时候的坐标
- 偏移画布的坐标到点击的坐标
- 绘制mask部分
- 获取ripple的color属性的值,并将color的alpha值减小一半
- 如果background不为空,并且background.isVisible才绘制background
- 将每一次exit的ripple依次绘制出来,如果连续点击的话,会出现水波一层一层的效果,该处就是绘制一层一层的效果
- 绘制当前次的rippleForeground
- 还原画布的偏移量
4.3.1 绘制mask部分
private void updateMaskShaderIfNeeded() {
//省略一些空判断
//获取maskType
final int maskType = getMaskType();
if (mMaskBuffer == null
|| mMaskBuffer.getWidth() != bounds.width()
|| mMaskBuffer.getHeight() != bounds.height()) {
if (mMaskBuffer != null) {
mMaskBuffer.recycle();
}
//创建mask部分画布需要的bitmap
mMaskBuffer = Bitmap.createBitmap(
bounds.width(), bounds.height(), Bitmap.Config.ALPHA_8);
//将mask部分的bitmap放到bitmapShader上面,后面会用到ripple上面
mMaskShader = new BitmapShader(mMaskBuffer,
Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
//创建mask部分的画布
mMaskCanvas = new Canvas(mMaskBuffer);
} else {
mMaskBuffer.eraseColor(Color.TRANSPARENT);
}
if (mMaskMatrix == null) {
mMaskMatrix = new Matrix();
} else {
mMaskMatrix.reset();
}
//创建了PorterDuffColorFilter,后面绘制riiple的时候会用到
if (mMaskColorFilter == null) {
mMaskColorFilter = new PorterDuffColorFilter(0, PorterDuff.Mode.SRC_IN);
}
final int top = bounds.top;
mMaskCanvas.translate(-left, -top);
//默认情况下maskType=MASK_NONE,大家可以看下getMaskType怎么获取的
if (maskType == MASK_EXPLICIT) {
drawMask(mMaskCanvas);
} else if (maskType == MASK_CONTENT) {
drawContent(mMaskCanvas);
}
mMaskCanvas.translate(left, top);
}
- 获取到mask部分的maskType,如果mask部分的drawable颜色值透明度是255,获取到的maskType=MASK_NONE,否则maskType=MASK_EXPLICIT
- 生成
mMaskBuffer
、mMaskShader
、mMaskCanvas
,创建了mMaskColorFilter
,关于PorterDuffColorFilter
的应用,在StateListDrawable
部分有提到过,此处使用SRC_IN模式,说明mask部分在要绘制的下面。 - 由于我们分析过maskType=MASK_NONE,所以不会绘制mask部分,直接将
mMaskShader
传给ripple部分。
从上面看我们绘制background的条件是不为空,并且是isVisible,此处可不是view中的visible的意思:
public boolean isVisible() {
return mOpacity > 0 || isHardwareAnimating();
}
mOpacity在点击的时候绘制透明度变化的一个变量,从0到1和1到0变化的过程,isHardwareAnimating
也很简单:
protected final boolean isHardwareAnimating() {
return mHardwareAnimator != null && mHardwareAnimator.isRunning()
|| mHasPendingHardwareAnimator;
}
表示mHardwareAnimator正在进行中,先姑且不管,后面我们再看该动画是什么意思。
我们看下mExitingRipples
是在什么付的值:
//该方法是在手抬起的时候绘制的,实际是在exit的时候,将mRipple赋值给mExitingRipples数组,并且将数组自增1。调用完了exit后,将mRipple至为空
private void tryRippleExit() {
if (mRipple != null) {
if (mExitingRipples == null) {
mExitingRipples = new RippleForeground[MAX_RIPPLES];
}
mExitingRipples[mExitingRipplesCount++] = mRipple;
mRipple.exit();
mRipple = null;
}
}
关于rippleDrawable静态绘制部分就先说到这里,下面到rippleDrawable动态绘制部分。
4.4 触摸绘制
在第一节view的ontouchEvent触发后,紧接着会触发drawable的setState方法,在setState中会触发drawable的onStateChange方法,直接看RippleDrawable
的onStateChange
方法:
@Override
protected boolean onStateChange(int[] stateSet) {
final boolean changed = super.onStateChange(stateSet);
boolean enabled = false;
boolean pressed = false;
boolean focused = false;
boolean hovered = false;
for (int state : stateSet) {
if (state == R.attr.state_enabled) {
enabled = true;
} else if (state == R.attr.state_focused) {
focused = true;
} else if (state == R.attr.state_pressed) {
pressed = true;
} else if (state == R.attr.state_hovered) {
hovered = true;
}
}
//既按下了又是enable状态
setRippleActive(enabled && pressed);
setBackgroundActive(hovered || focused || (enabled && pressed), focused || hovered);
return changed;
}
onStateChange
逻辑很清晰,在enable并且pressed状态下会触发setRippleActive
和setBackgroundActive
方法,先来看下setRippleActive
方法是干嘛的:
private void setRippleActive(boolean active) {
if (mRippleActive != active) {
mRippleActive = active;
if (active) {
//按下的时候调用该方法
tryRippleEnter();
} else {
//抬起的时候调用该方法
tryRippleExit();
}
}
}
按下的时候调用了tryRippleEnter
方法,抬起的时候调用了tryRippleExit
方法:
private void tryRippleEnter() {
//限制了ripple最大的次数
if (mExitingRipplesCount >= MAX_RIPPLES) {
return;
}
if (mRipple == null) {
final float x;
final float y;
//mHasPending在按下的时候为true,
if (mHasPending) {
mHasPending = false;
//按下时候的坐标
x = mPendingX;
y = mPendingY;
} else {
//后面的坐标用mHotspotBounds里面的坐标
x = mHotspotBounds.exactCenterX();
y = mHotspotBounds.exactCenterY();
}
final boolean isBounded = isBounded();
//生成了一个RippleForeground
mRipple = new RippleForeground(this, mHotspotBounds, x, y, isBounded, mForceSoftware);
}
//紧接着调用了setUp和enter方法
mRipple.setup(mState.mMaxRadius, mDensity);
mRipple.enter(false);
}
rippleEnter里面的逻辑还是挺清晰的,先是判断RippleForeground
是否为空,将按下时候的x、y的坐标传给RippleForeground
,紧接着调用了setUp和enter方法,RippleForeground
是继承自RippleComponent
,setUp和enter方法都是父类中定义的,看下这两个方法的定义:
public final void setup(float maxRadius, int densityDpi) {
//默认maxRadius=-1,因此走else里面的逻辑
if (maxRadius >= 0) {
mHasMaxRadius = true;
mTargetRadius = maxRadius;
} else {
mTargetRadius = getTargetRadius(mBounds);
}
//缩放的单位密度
mDensityScale = densityDpi * DisplayMetrics.DENSITY_DEFAULT_SCALE;
onTargetRadiusChanged(mTargetRadius);
}
五、动画部分
5.1 RippleForeground的动画
默认传过来的maxRadius=-1,因此通过getTargetRadius得到mTargetRadius,getTargetRadius里面通过勾股定理得到view大小的对角线的一半。最后调用了onTargetRadiusChanged
方法,该方法是个空方法,可以想到是交给子类自己去处理mTargetRadius
的问题,紧接着看下enter方法做了些什么:
public final void enter(boolean fast) {
cancel();
mSoftwareAnimator = createSoftwareEnter(fast);
if (mSoftwareAnimator != null) {
mSoftwareAnimator.start();
}
}
先是取消之前的动画,紧接着在通过createSoftwareEnter
方法创建了mSoftwareAnimator
动画,最后是启动动画。createSoftwareEnter
是一个抽象的方法,来到RippleForeground
看下该方法:
@Override
protected Animator createSoftwareEnter(boolean fast) {
// Bounded ripples don't have enter animations.
//注释说得很清楚,如果当前rippleDrawable是bounded直接返回null,也就是按下的时候没有动画
if (mIsBounded) {
return null;
}
//动画时间会根据mTargetRadius成正比
final int duration = (int)
(1000 * Math.sqrt(mTargetRadius / WAVE_TOUCH_DOWN_ACCELERATION * mDensityScale) + 0.5);
//radius动画
final ObjectAnimator tweenRadius = ObjectAnimator.ofFloat(this, TWEEN_RADIUS, 1);
tweenRadius.setAutoCancel(true);
tweenRadius.setDuration(duration);
tweenRadius.setInterpolator(LINEAR_INTERPOLATOR);
tweenRadius.setStartDelay(RIPPLE_ENTER_DELAY);
//水波画圆的时候圆心动画,从点击的点到rippleDrawable中心位置一直到点击的点到rippleDrawable中心位置的0.7的圆心渐变动画
final ObjectAnimator tweenOrigin = ObjectAnimator.ofFloat(this, TWEEN_ORIGIN, 1);
tweenOrigin.setAutoCancel(true);
tweenOrigin.setDuration(duration);
tweenOrigin.setInterpolator(LINEAR_INTERPOLATOR);
tweenOrigin.setStartDelay(RIPPLE_ENTER_DELAY);
//透明度的动画
final ObjectAnimator opacity = ObjectAnimator.ofFloat(this, OPACITY, 1);
opacity.setAutoCancel(true);
opacity.setDuration(OPACITY_ENTER_DURATION_FAST);
opacity.setInterpolator(LINEAR_INTERPOLATOR);
final AnimatorSet set = new AnimatorSet();
set.play(tweenOrigin).with(tweenRadius).with(opacity);
return set;
}
在enterSoftware动画里面,先是判断是不是bounds,此处的isBound是从rippleDrawable中传过来的:
private boolean isBounded() {
return getNumberOfLayers() > 0;
}
也就是通过RippleState中的mNumChildren个数大于0来判断的,在上面初始化过程中已经分析过了,addLayer方法添加的个数实际是通过xml中的item个数来添加的,因此一般情况下都是isBounded的,除非在ripple标签里面不定义item标签。
虽然在softWareEnter里面一般都是return null,但是后面的动画,还是分析下,因为在softWareExit中还是定义这三个动画:
- tweenRadius定义水波画圆的时候半径的动画
- tweenOrigin定义水波画圆的时候圆心的动画
- opacity定义水波透明度的动画、
上面三个动画都用到了动画的Property
形式实现当前类值的改变,都是从0到1的过程,在tweenRadius
动画中不断改变RippleForeground
中的mTweenRadius
变量,在tweenOrigin
动画中不断改变mTweenX
和mTweenX
全局变量,opacity
动画中不断改变mOpacity
全局变量。并且在动画的setValue方法中都会调用invalidateSelf
方法,最终会重新调用到rippleDrawable的invalidateSelf
方法,在第一节中简单提过invalidateSelf
方法,最终会触发drawable的draw方法,因此可以想到实际上rippleForeground中的动画会不断调用到RippleComponent
的draw方法:
public boolean draw(Canvas c, Paint p) {
//如果canvas是hardwareAccelerated模式才会走hardWare的动画,默认直接跳过
final boolean hasDisplayListCanvas = !mForceSoftware && c.isHardwareAccelerated()
&& c instance DisplayListCanvas;
if (mHasDisplayListCanvas != hasDisplayListCanvas) {
mHasDisplayListCanvas = hasDisplayListCanvas;
if (!hasDisplayListCanvas) {
// We've switched from hardware to non-hardware mode. Panic.
endHardwareAnimations();
}
}
if (hasDisplayListCanvas) {
final DisplayListCanvas hw = (DisplayListCanvas) c;
startPendingAnimation(hw, p);
if (mHardwareAnimator != null) {
return drawHardware(hw);
}
}
//默认会去绘制softWare部分
return drawSoftware(c, p);
}
在RippleComponent的draw方法里面,如果没开启硬件加速,hardWare动画是没有打开的,因此直接看drawSoftware部分,drawSoftware在RippleComponent里面是抽象方法,因此还是得需要到子类RippleForeground里面看下:
@Override
protected boolean drawSoftware(Canvas c, Paint p) {
boolean hasContent = false;
//获取到画笔最开始的透明度,透明度是ripple标签color颜色值透明度的一半,这个在rippleDrawable静态绘制部分已经讲过
final int origAlpha = p.getAlpha();
final int alpha = (int) (origAlpha * mOpacity + 0.5f);
//获取到当前的圆的半径
final float radius = getCurrentRadius();
if (alpha > 0 && radius > 0) {
//获取圆心的位置
final float x = getCurrentX();
final float y = getCurrentY();
p.setAlpha(alpha);
c.drawCircle(x, y, radius, p);
p.setAlpha(origAlpha);
hasContent = true;
}
return hasContent;
}
上面通过mOpacity算出当前画笔的透明度,这里用了一个+0.5f转成int类型,这个是很常用的float转int类型的计算方式吧,通常在现有基础上+0.5f。mOpacity
变量是在opacity
动画中通过它的property改变全局属性的方式,关于动画大家可以看看property
的使用,这里用到的是FloatProperty
的类型:
/**
* Property for animating opacity between 0 and its target value.
*/
private static final FloatProperty OPACITY =
new FloatProperty("opacity") {
@Override
public void setValue(RippleForeground object, float value) {
object.mOpacity = value;
object.invalidateSelf();
}
@Override
public Float get(RippleForeground object) {
return object.mOpacity;
}
};
关于动画网上的用法很多,大家可以自己尝试写些动画,在上面动画中setValue中,调用了object.invalidateSelf方法,这个就是不断递归调用到RippleDrawable的draw方法的原因,其实说白了最终会调用view的draw方法。
getCurrentRadius
方法是获取当前radius:
private float getCurrentRadius() {
return MathUtils.lerp(0, mTargetRadius, mTweenRadius);
}
这里是android的MathUtils工具类,差值器的利用,前面两个参数起始值和终止值,第三个三处是百分比。
getCurrentX和getCurrentY方法也是和圆心的获取是类似的,说完了enter部分的softWare部分,我们来看下exit部分,上面已经分析了exit得从tryRippleExit
方法说起:
private void tryRippleExit() {
if (mRipple != null) {
if (mExitingRipples == null) {
mExitingRipples = new RippleForeground[MAX_RIPPLES];
}
//将每一次的rippleForground存起来,在draw方法中绘制完未绘制完的rippleForground
mExitingRipples[mExitingRipplesCount++] = mRipple;
mRipple.exit();
mRipple = null;
}
}
mRipple.exit()
会触发到rippleForground的createSoftwareExit的动画,这里就不贴出创建动画的代码,简单说下:
看到了没,这里跟enter的动画区别是,如果isBounded会往下走创建动画的,而上面分析enter的时候,默认是isbounded直接return了,因此看不到enter的动画效果的,而我在
android-28
的手机上看到按下才有波纹效果,所以还得看下android-28
是不是改了enter的逻辑。
5.2 RippleBackground的动画
说完了RippleForeground的绘制和动画部分,其实到了Rippleground部分就简单多了,因为他只有透明度的动画:
@Override
protected Animator createSoftwareEnter(boolean fast) {
// Linear enter based on current opacity.
final int maxDuration = fast ? OPACITY_ENTER_DURATION_FAST : OPACITY_ENTER_DURATION;
final int duration = (int) ((1 - mOpacity) * maxDuration);
final ObjectAnimator opacity = ObjectAnimator.ofFloat(this, OPACITY, 1);
opacity.setAutoCancel(true);
opacity.setDuration(duration);
opacity.setInterpolator(LINEAR_INTERPOLATOR);
return opacity;
}
我去,这里不解释,直接一个opacity的动画,好吧,太直观了点,说完了enter部分的动画,下面接着看下exit部分的动画:
@Override
protected Animator createSoftwareExit() {
final AnimatorSet set = new AnimatorSet();
//透明度显示从1到0
final ObjectAnimator exit = ObjectAnimator.ofFloat(this, RippleBackground.OPACITY, 0);
exit.setInterpolator(LINEAR_INTERPOLATOR);
exit.setDuration(OPACITY_EXIT_DURATION);
exit.setAutoCancel(true);
final AnimatorSet.Builder builder = set.play(exit);
final int fastEnterDuration = mIsBounded ?
(int) ((1 - mOpacity) * OPACITY_ENTER_DURATION_FAST) : 0;
if (fastEnterDuration > 0) {
//这里又从0到1的过程
final ObjectAnimator enter = ObjectAnimator.ofFloat(this, RippleBackground.OPACITY, 1);
enter.setInterpolator(LINEAR_INTERPOLATOR);
enter.setDuration(fastEnterDuration);
enter.setAutoCancel(true);
builder.after(enter);
}
return set;
}
exit动画分为两部分,一个透明度从1到0,然后又从0到1的过程,这个分析下来,就是抬起的时候先从不透明到完全透明再到不完全透明的过程。上面用到了动画集合AnimatorSet.Builder
的after方法,这个我也没用过,从字面意思理解是在上面的exit动画结束后再执行透明度从0到1的enter动画。
好了,关于RippleForground的绘制、动画以及RippleBackground绘制和动画都讲完了,RippleForground负责水波的绘制,RippleBackground负责绘制透明度渐变的动画。
5.3 取消动画
关于RippleDrawable中的水波动画,还得需要了解view的销毁时机,不知道大家平时有没有重写一个view的onDetachViewFromWindow
方法没,view上的background和foreground都是在detach的时候进行销毁,所以RippleDrawable也不例外,先顺着view往下看:
void dispatchDetachedFromWindow() {
//一般自定义view的时候重写该方法,比如释放动画等等
onDetachedFromWindow();
//销毁drawable的地方
onDetachedFromWindowInternal();
}
注释写得很清楚,大家在自定义view的时候,是不是有用过onDetachedFromWindow
方法,就是由这而来,接着看onDetachedFromWindowInternal
方法:
protected void onDetachedFromWindowInternal() {
jumpDrawablesToCurrentState();
}
为了方便大家看代码,我把代码精简到一行代码,接着往下看:
public void jumpDrawablesToCurrentState() {
if (mBackground != null) {
mBackground.jumpToCurrentState();
}
if (mStateListAnimator != null) {
mStateListAnimator.jumpToCurrentState();
}
if (mDefaultFocusHighlight != null) {
mDefaultFocusHighlight.jumpToCurrentState();
}
if (mForegroundInfo != null && mForegroundInfo.mDrawable != null) {
mForegroundInfo.mDrawable.jumpToCurrentState();
}
}
看到了没,都是调用了drawable的jumpToCurrentState
方法,直接来到RippleDrawable下面的该方法:
@Override
public void jumpToCurrentState() {
super.jumpToCurrentState();
if (mRipple != null) {
mRipple.end();
}
if (mBackground != null) {
mBackground.end();
}
cancelExitingRipples();
}
private void cancelExitingRipples() {
final int count = mExitingRipplesCount;
final RippleForeground[] ripples = mExitingRipples;
for (int i = 0; i < count; i++) {
ripples[i].end();
}
if (ripples != null) {
Arrays.fill(ripples, 0, count, null);
}
mExitingRipplesCount = 0;
// Always draw an additional "clean" frame after canceling animations.
invalidateSelf(false);
}
很一目了然吧,调用了RippleForeground
的end
、RippleBackground
的end
以及在cancelExitingRipples
方法里面调用了每次exit未完成的RippleForeground的end方法,所以归根到最后,其实是调用了父类RippleComponent
中end
方法:
public void end() {
endSoftwareAnimations();
endHardwareAnimations();
}
看到了吧,方法名都摆出来了:
private void endSoftwareAnimations() {
if (mSoftwareAnimator != null) {
mSoftwareAnimator.end();
mSoftwareAnimator = null;
}
}
private void endHardwareAnimations() {
if (mHardwareAnimator != null) {
mHardwareAnimator.end();
mHardwareAnimator = null;
}
}
直接不解释,关于view从window上detach后到RippleDrawable
中动画停止后就到这里了。
六、总结
我们再来梳理下绘制流程:
-
RippleDrawable
在inflate
过程初始化了一层层的layer
,添加到LayerState
里面,初始化mask部分的drawable,放到了mMask全局drawable里面,初始化了ripple
标签里面的color
属性。 -
在RippleDrawable
静态绘制部分先是绘制了非id=mask的item - mask部分color属性值alpha=255是不会绘制的,因此颜色值的alpha值需要在[0,255)这个区间,mask绘制是在rippleForeground和RippleBackground的绘制下层。
- 接着绘制
RippleBackground
部分,如果RippleBackground.isVisible才绘制。 - 接着绘制每次
exit
未完成的RippleForeground
部分,注意这里是个集合遍历绘制RippleForeground
。 - 接着才是绘制当前次的
RippleForeground
。 - 在动画部分,先是触发了
RippleDrawable
的onStateChange
方法,接着创建了RippleForeground
,调用了RippleForeground
的enter
和setup``方法,在enter里面创建了
softWare动画,其中
hardWare动画是要开启了硬件加速功能才能创建,所以默认不会创建
softWare`动画。 -
RippleForeground
中的softWare
创建的动画有三个,一个是半径、圆心、透明度变化的三个动画,在enter
的时候RippleForeground
在RippleDrawable.isBounded
的时候不创建动画;在exit
的时候不会限制创建动画,这个是在android-27
下面的源码。在android-28
的手机上面我看下了效果是在enter
的时候有水波动画,exit
的时候没有动画,大家可以用android-28
的手机尝试下。 -
RippleBackground
中就一个动画,改变画笔的透明底,enter
情况下画笔从0到1的过程;在exit
的时候画笔的透明度先是从1到0,然后又从0到1的过程。 - 上面提到的
enter
和exit
中的动画,都是不断地调用到RippleDrawable
的invalidateSelf
方法,而invalidateSelf
会触发view
的draw
方法,最后触发了RippleDrawable
的draw
方法,最终会触发到RippleForeground
的drawSoftware
和RippleBackground
的drawSoftware
。 - RippleDrawable中动画销毁是在
view#dispatchdetachedFromWindow
到RippleDrawable
的jumpToCurrentState
方法。