最近视觉给了一个动效,类似于图片选择框的效果,其中包含选择框的一个从无到有的的放大过程。因为裁剪框位置不固定,视觉不能给出统一的动效,因此需要自己动手实现
我所掌握的资料:裁剪框的四点坐标。第一想法是通过属性动画的位移和缩放来实现,开始定义一个0dp的view,需要展示动画时,先移动到中点坐标,然后开始放大,但是裁剪框外部是阴影覆盖,内部是透明的,发现无法实现。然后就决定自定义view,一帧一帧的绘制来实现放大效果
自定义View的实现主要分为以下几个部分:
1. 如何绘制?
2. 什么时候绘制,绘制坐标的选取?
首先第一点:如何绘制?
我采用方向相反的的两个path来实现内部透明,外部遮罩的效果
private void initPaint(){
mPaint = new Paint();
//填充方式
mPaint.setStyle(Paint.Style.FILL);
//阴影
mPaint.setColor(Color.parseColor("#AA1C1C1C"));
mPath = new Path();
//内部框
innerRect = new RectF(0,0,0,0);
//外部框
outRect = new RectF(0,0,0,0);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//每次绘制前,都需要先调用 Path.reset()方法,清除原来的路径,这是一个坑,不加的话看不到效果
mPath.reset();
innerRect.set(drawCorr[0],drawCorr[1],drawCorr[2],drawCorr[3]);
outRect.set(0,0,getWidth(),getHeight());
//两个Path方向相反
mPath.addRect(outRect, Path.Direction.CW);
mPath.addRect(innerRect, Path.Direction.CCW);
canvas.drawPath(mPath, mPaint);
}
绘制的过程比较简单,就是在onDraw方法里面,通过两个方向相反的path,来实现遮罩的效果。这里要注意的就是,每次绘制前,需要先调用 Path.reset(),把原来的路径清除掉,不然看不到效果。
第二点:什么时候绘制,绘制坐标的选取?
要实现流畅的放大效果,这里我使用了属性动画,通过输入最终值,运行动画,拿到一系列的有序的中间值,进行转换后进行绘制
大概思路是,首先根据四点坐标计算出裁剪框的宽和高,然后根据宽高计算出对角线的边长,将对角线边长的二分之一作为属性动画的最终值,在animationUpdate中不断拿到中间值,已知裁剪框的宽和高,可以根据中间值计算出此时需要绘制的宽和高,然后调用invalidate方法进行绘制
获取对角线的边长二分之一
* 获取动画结束值
* @param animWidth 最终阴影宽
* @param animHeight 最终阴影高
* @return 阴影宽和阴影高的斜边长
*/
private float getAnimTarget(float animWidth, float animHeight){
return (float) Math.sqrt(animWidth * animWidth + animHeight * animHeight) / 2;
}
根据中间值获取此时要绘制的宽和高
/**
* 获取下一个要绘制的坐标
* @param animTarget 插值器返回的值
*/
private void getDrawCorr(float animTarget){
float x = animWidth/animHeight;
float drawHeight = (float) Math.sqrt((animTarget * animTarget) /
(1 + (animWidth * animWidth)/(animHeight * animHeight)));
float drawWidth = drawHeight * x;
drawCorr[0] = corrX - drawWidth;
drawCorr[1] = corrY - drawHeight;
drawCorr[2] = corrX + drawWidth;
drawCorr[3] = corrY + drawHeight;
}
对外暴露的开始动画接口
/**
* 开始放大动画
* @param left 左边距
* @param top 上边距
* @param right 右边距
* @param bottom 下边距
*/
public void startAnim(int left, int top, int right, int bottom){
if(right <= left || bottom <= top){
return;
}
animWidth = right - left;
animHeight = bottom - top;
corrX = (float) (right + left) / 2;
corrY = (float) (bottom + top) / 2;
ValueAnimator valueAnimator;
if(zoomOut){
valueAnimator = ValueAnimator.ofFloat(0f,getAnimTarget(animWidth,animHeight));
}else {
valueAnimator = ValueAnimator.ofFloat(getAnimTarget(animWidth,animHeight),0f);
}
valueAnimator.setDuration(duration);
valueAnimator.addUpdateListener(updateListener);
valueAnimator.start();
}
ValueAnimator.AnimatorUpdateListener updateListener = new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
getDrawCorr((float)animation.getAnimatedValue());
invalidate();
}
};
完整代码:
package view;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.view.View;
import androidx.annotation.Nullable;
/**
* 自定义裁剪框缩放View
* @author lh
*/
public class ClipAnimationView extends View {
/**
* 动画宽高
*/
private float animWidth;
private float animHeight;
/**
* 中点坐标
*/
private float corrX;
private float corrY;
private float[] drawCorr = new float[]{0,0,0,0};
private int duration = 500;
private Paint mPaint;
private Path mPath;
private RectF innerRect;
private RectF outRect;
/**
* 缩放方向
*/
private boolean zoomOut = true;
public ClipAnimationView(Context context) {
super(context);
}
public ClipAnimationView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initPaint();
}
public ClipAnimationView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public ClipAnimationView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
private void initPaint(){
mPaint = new Paint();
//填充方式
mPaint.setStyle(Paint.Style.FILL);
//阴影
mPaint.setColor(Color.parseColor("#AA1C1C1C"));
mPath = new Path();
//内部框
innerRect = new RectF(0,0,0,0);
//外部框
outRect = new RectF(0,0,0,0);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//每次绘制前,都需要先调用 Path.reset()方法,清除原来的路径,这是一个坑,不加的话看不到效果
mPath.reset();
innerRect.set(drawCorr[0],drawCorr[1],drawCorr[2],drawCorr[3]);
outRect.set(0,0,getWidth(),getHeight());
//两个Path方向相反
mPath.addRect(outRect, Path.Direction.CW);
mPath.addRect(innerRect, Path.Direction.CCW);
canvas.drawPath(mPath, mPaint);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
}
/**
* 开始放大动画
* @param left 左边距
* @param top 上边距
* @param right 右边距
* @param bottom 下边距
*/
public void startAnim(int left, int top, int right, int bottom){
if(right <= left || bottom <= top){
return;
}
animWidth = right - left;
animHeight = bottom - top;
corrX = (float) (right + left) / 2;
corrY = (float) (bottom + top) / 2;
ValueAnimator valueAnimator;
if(zoomOut){
valueAnimator = ValueAnimator.ofFloat(0f,getAnimTarget(animWidth,animHeight));
}else {
valueAnimator = ValueAnimator.ofFloat(getAnimTarget(animWidth,animHeight),0f);
}
valueAnimator.setDuration(duration);
valueAnimator.addUpdateListener(updateListener);
valueAnimator.start();
}
ValueAnimator.AnimatorUpdateListener updateListener = new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
getDrawCorr((float)animation.getAnimatedValue());
invalidate();
}
};
/**
* 获取动画结束值
* @param animWidth 最终阴影宽
* @param animHeight 最终阴影高
* @return 阴影宽和阴影高的斜边长
*/
private float getAnimTarget(float animWidth, float animHeight){
return (float) Math.sqrt(animWidth * animWidth + animHeight * animHeight) / 2;
}
/**
* 获取下一个要绘制的坐标
* @param animTarget 插值器返回的值
*/
private void getDrawCorr(float animTarget){
float x = animWidth/animHeight;
float drawHeight = (float) Math.sqrt((animTarget * animTarget) /
(1 + (animWidth * animWidth)/(animHeight * animHeight)));
float drawWidth = drawHeight * x;
drawCorr[0] = corrX - drawWidth;
drawCorr[1] = corrY - drawHeight;
drawCorr[2] = corrX + drawWidth;
drawCorr[3] = corrY + drawHeight;
}
public void setDuration(int duration){
this.duration = duration;
}
/**
* 设置缩放动画方向
* @param zoomOut 缩放方向
*/
public void setZoomOut(boolean zoomOut){
this.zoomOut = zoomOut;
}
}
然后通过调用 startAnim方法输入坐标,就可以实现缩放的动画效果了。
github地址:https://github.com/Lh1600852534/Demo/blob/master/app/src/main/java/view/ClipAnimationView.java