前言
在之前的博客android如何给整个视图view圆角显示中有提到过如何实现对View显示进行圆角裁剪,其原理其实也比较简单。这里先看看动画效果。
View负责绘制显示的draw方法
因为View的draw方法是负责View绘制显示的,并且它是负责整体显示的,包括View的背景,内容,以及子View的递归显示等,因此要使当前View以及它包含的子View也实现裁剪的效果,就需要重写draw方法,而不是onDraw方法,onDraw方法只是负责View自身内容的显示的。下面是View的draw方法绘制流程描述
public void draw(Canvas canvas) {
/*
* 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)
*/
...
}
那么如何对View的显示进行裁剪呢?
对View绘制显示进行裁剪可以使用Canvas提供的clipXXX方法。像我们这里要显示水波纹效果,我选择的是clipPath方法,创建一个Path对象,然后addCircle得到一个圆形的Path。为了防止canvas.clipPath之后会影响其他View的绘制效果,这里需要先对当前Canvas的状态进行save保存,clipPath和draw绘制之后,再restore还原。因此draw方法实现如下
@Override public void draw(Canvas canvas) {
int saveCount = canvas.save();
checkPathChanged();
canvas.clipPath(mPath);
super.draw(canvas);
canvas.restoreToCount(saveCount);
}
这里checkPathChanged是检测Path对象是否有变化,在这里也就是Path中的圆形的半径有没有发生变化,通常,如果是做水波纹展开收起动画的话,半径是不断变化的。此外,此处还做了点小优化,如果半径没有发生变化,就不用重新改变Path了。checkPathChanged如下
private void checkPathChanged() {
if (mProgress == mBackProgress) {
return;
}
mBackProgress = mProgress;
mWidth = getWidth();
mHeight = getHeight();
setCenterXY(mWidth / 2, mHeight / 2);
int maxRadius = (int) Math.hypot(mWidth, mHeight) / 2;
mRadius = (int) (maxRadius * mProgress);
mPath.reset();
mPath.addCircle(mX, mY, mRadius, Path.Direction.CW);
}
这里的mProgress代表的是展开的进度,半径通过展开进度计算得出。同时记得每次变化Path时记得先调用reset重置Path状态。
什么,水波纹裁剪没有效果?
如果迫不及待的去尝试用以上原理代码去实现的话,你可能会发现,压根就没有裁剪效果!为什么没有效果,先不急,我们先给这个View设置一个背景试试,比较设置个颜色,再试试,是不是有效果了?好奇怪,为什么给这个View设置背景就有效果,不设置就没有?之前我也是被这个问题困扰了许久,后面通过查找找出了原因所在。下面说说查找的方法。
我在当前这个RippleLayout外层包装一个CustomRelativeLayout,它很简单,只是打印了一些信息负责调试。实现如下
public class CustomRelativeLayout extends RelativeLayout {
private final String TAG = getClass().getSimpleName();
public CustomRelativeLayout(Context context) {
super(context);
init();
}
public CustomRelativeLayout(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
}
@Override public void draw(Canvas canvas) {
Log.e(TAG, "----------------draw");
super.draw(canvas);
}
@Override protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Log.e(TAG, "----------------onDraw");
}
@Override protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
Log.e(TAG, "----------------drawChild");
return super.drawChild(canvas, child, drawingTime);
}
@Override protected void dispatchDraw(Canvas canvas) {
Log.e(TAG, "----------------dispatchDraw");
super.dispatchDraw(canvas);
}
}
除了打印信息,没有做任何的逻辑修改。当不给它设置背景时,查看打印信息。
com.test.ripple E/CustomRelativeLayout: ----------------dispatchDraw
com.test.ripple E/CustomRelativeLayout: ----------------drawChild
com.test.ripple E/CustomRelativeLayout: ----------------dispatchDraw
com.test.ripple E/CustomRelativeLayout: ----------------drawChild
会发现,并没有执行draw和onDraw方法!现在我给它设置一个背景颜色,再看看打印信息。
com.test.ripple E/CustomRelativeLayout: ----------------draw
com.test.ripple E/CustomRelativeLayout: ----------------onDraw
com.test.ripple E/CustomRelativeLayout: ----------------dispatchDraw
com.test.ripple E/CustomRelativeLayout: ----------------drawChild
com.test.ripple E/CustomRelativeLayout: ----------------draw
com.test.ripple E/CustomRelativeLayout: ----------------onDraw
com.test.ripple E/CustomRelativeLayout: ----------------dispatchDraw
com.test.ripple E/CustomRelativeLayout: ----------------drawChild
会发现这些方法都执行了,流程是draw->onDraw->dispatchDraw->drawChild,执行了两次,也就是刷新了两次。
我们知道,一般的绘制流程是从draw方法开始,然后是绘制自身背景,然后是onDraw绘制自身内容,然后dispatchDraw绘制包含的子View。但是这里如果没有设置背景的话,连draw方法都没有执行,而是执行了dispatchDraw和drawChild。因此还是要去看看源码是怎么实现的。通过查找dispatchDraw和drawChild方法,发现dispatchDraw除了被draw方法调用之外,还有在View.updateDisplayListIfDirty方法被调用
@NonNull
public RenderNode updateDisplayListIfDirty() {
...
// Fast path for layouts with no backgrounds
if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
dispatchDraw(canvas);
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().draw(canvas);
}
} else {
draw(canvas);
}
...
}
这里可以看出,如果没有设置背景,走的是dispatchDraw,有背景才走的draw。这样做应该是为了优化大部分View没有设置背景的情况,免去draw方法中处理绘制背景等逻辑。也就是说
默认View和View的子类,如果有设置背景,则正常会调用draw方法,如果没有设置背景,则不调用draw方法,而是调用dispatchDraw作子View的绘制。
OK,知道了原理之后,我们对RippleLayout初始化时针对没有设置背景的情况做特殊处理
public RippleLayout(Context context) {
super(context);
init();
}
public RippleLayout(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
if(getBackground() == null){
//需设置背景,否则无法显示圆角裁剪
setBackgroundColor(Color.TRANSPARENT);
}
mPath = new Path();
mPath.setFillType(Path.FillType.EVEN_ODD);
setCornerRadius(dp2px(4));
}
如果当前没有背景,则给其设置一个透明颜色的背景,这样就解决了没有设置背景无法实现裁剪的问题。
如何实现水波纹展开收起动画?
通过上面对RippleLayout中设置它的mProgress可改变它裁剪半径大小,也就可以实现圆形裁剪,因此只要不停改变大小,就可以实现水波纹展开收起动画了,这里使用ValueAnimator做动画的渐变操作。
private void expand(){
doRippleAnim(0.2f, 1);
}
private void unexpand(){
doRippleAnim(1, 0.2f);
}
private void doRippleAnim(final float fromPercent, final float toPercent){
ValueAnimator animator = ValueAnimator.ofFloat(fromPercent, toPercent).setDuration(ANIM_TIME);
animator.setInterpolator(new AccelerateDecelerateInterpolator());
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
final float progress = fromPercent + animation.getAnimatedFraction() * (toPercent - fromPercent);
rippleView.setProgress(progress);
}
});
animator.start();
}
通过ValueAnimator不断更新渐变值,在onAnimationUpdate回调中设置rippleView的setProgress来达到动画效果。
不过这里只是对视图的显示做了展开收起的操作,实际布局并没有变化,假如里面有按钮的话,点击该位置依然会产生点击事件,因此并不算真正的水波纹展开收起效果。因此我们这里再加上位移和缩放等属性动画,配合一起实现更真实的水波纹展开收起动画效果。
我们先看收起动画实现
/**
* 收起动画
*/
private void unexpandOther(){
final float fromPercent = 1f;
final float toPercent = 0.1f;
final float fromScale = 1f;
final float toScale = 0.1f;
final float fromX = 0;
final float fromY = 0;
final float toX = dp2px(100);
final float toY = dp2px(100);
final float fromProgress = 1;
final float toProgress =
(float) (rippleView.getWidth() / Math.hypot(rippleView.getWidth(), rippleView.getHeight()));
ValueAnimator animator = ValueAnimator.ofFloat(fromPercent, toPercent).setDuration(ANIM_TIME);
animator.setInterpolator(new AccelerateDecelerateInterpolator());
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
final float progress = animation.getAnimatedFraction();
rippleView.setProgress((toProgress - fromProgress) * progress + fromProgress);
rippleView.setX((toX - fromX) * progress + fromX);
rippleView.setY((toY - fromY) * progress + fromY);
rippleView.setScaleX((toScale - fromScale) * progress + fromScale);
rippleView.setScaleY((toScale - fromScale) * progress + fromScale);
}
});
animator.start();
}
根据动画进度0.1到1,不断对位置,缩放大小,水波纹裁剪度进行调整。同理,展开动画也是差不多的,参数不同而已。
/**
* 展开动画
*/
private void expandOther(){
final float fromPercent = 0.1f;
final float toPercent = 1f;
final float fromScale = 0.1f;
final float toScale = 1f;
final float fromX = dp2px(100);
final float fromY = dp2px(100);
final float toX = 0;
final float toY = 0;
final float fromProgress =
(float) (rippleView.getWidth() / Math.hypot(rippleView.getWidth(), rippleView.getHeight()));
final float toProgress = 1;
ValueAnimator animator = ValueAnimator.ofFloat(fromPercent, toPercent).setDuration(ANIM_TIME);
animator.setInterpolator(new AccelerateDecelerateInterpolator());
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
final float progress = animation.getAnimatedFraction();
rippleView.setProgress((toProgress - fromProgress) * progress + fromProgress);
rippleView.setX((toX - fromX) * progress + fromX);
rippleView.setY((toY - fromY) * progress + fromY);
rippleView.setScaleX((toScale - fromScale) * progress + fromScale);
rippleView.setScaleY((toScale - fromScale) * progress + fromScale);
}
});
animator.start();
}
现在再点击RippleLayout中的按钮所在的位置,就不会产生点击事件了,因为他们整理都进行缩放移动了,而不只是是显示的变化。
总结
本文讲解了,View绘制的大致流程,水波纹展开收起动画是根据对Canvas的在draw绘制之前做裁剪操作来实现的。然后分析了当没有设置背景时无法实现裁剪效果的问题,原因和解决办法。最后是如何实现水波纹展开收起动画,这里分为显示上的展开收起以及真正意义上的展开收起(涉及到属性动画)。水波纹展开动画效果在android原生的Google桌面上很好的展示效果。本文具体实现在这里。