自定义View—裁剪框的过度动画实现

 最近视觉给了一个动效,类似于图片选择框的效果,其中包含选择框的一个从无到有的的放大过程。因为裁剪框位置不固定,视觉不能给出统一的动效,因此需要自己动手实现

 我所掌握的资料:裁剪框的四点坐标。第一想法是通过属性动画的位移和缩放来实现,开始定义一个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

 

你可能感兴趣的:(自定义View,android,java)