自定义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

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,下面是一个实现圆形边和图片裁剪自定义View的完整代码: ``` public class CircleImageView extends ImageView { private Paint mBorderPaint; private int mBorderColor; private int mBorderWidth; private Bitmap mBitmap; private BitmapShader mBitmapShader; private int mBitmapWidth; private int mBitmapHeight; private float mRadius; private RectF mBorderRect; public CircleImageView(Context context) { super(context); init(); } public CircleImageView(Context context, AttributeSet attrs) { super(context, attrs); init(); } public CircleImageView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { mBorderPaint = new Paint(); mBorderPaint.setAntiAlias(true); mBorderRect = new RectF(); } @Override protected void onDraw(Canvas canvas) { if (mBitmapShader == null) { mBitmap = getBitmap(); if (mBitmap != null) { mBitmapShader = new BitmapShader(mBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); mBitmapWidth = mBitmap.getWidth(); mBitmapHeight = mBitmap.getHeight(); mRadius = Math.min(mBitmapWidth, mBitmapHeight) / 2; updateShaderMatrix(); } } if (mBitmapShader != null) { mBorderPaint.setShader(mBitmapShader); canvas.drawCircle(mRadius, mRadius, mRadius, mBorderPaint); mBorderPaint.setShader(null); mBorderPaint.setColor(mBorderColor); mBorderPaint.setStrokeWidth(mBorderWidth); mBorderRect.set(0, 0, getWidth(), getHeight()); canvas.drawArc(mBorderRect, 0, 360, false, mBorderPaint); } } private void updateShaderMatrix() { float scale; float dx = 0; float dy = 0; if (mBitmapWidth * getHeight() > getWidth() * mBitmapHeight) { scale = getHeight() / (float) mBitmapHeight; dx = (getWidth() - mBitmapWidth * scale) * 0.5f; } else { scale = getWidth() / (float) mBitmapWidth; dy = (getHeight() - mBitmapHeight * scale) * 0.5f; } Matrix matrix = new Matrix(); matrix.setScale(scale, scale); matrix.postTranslate((int) (dx + 0.5f), (int) (dy + 0.5f)); mBitmapShader.setLocalMatrix(matrix); } public void setBorderColor(int borderColor) { if (borderColor == mBorderColor) { return; } mBorderColor = borderColor; invalidate(); } public void setBorderWidth(int borderWidth) { if (borderWidth == mBorderWidth) { return; } mBorderWidth = borderWidth; invalidate(); } private Bitmap getBitmap() { Drawable drawable = getDrawable(); if (drawable == null) { return null; } if (drawable instanceof BitmapDrawable) { return ((BitmapDrawable) drawable).getBitmap(); } Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(bitmap); drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); drawable.draw(canvas); return bitmap; } } ``` 使用方法: 在布局文件中添加自定义View: ``` <com.example.CircleImageView android:id="@+id/circle_image_view" android:layout_width="100dp" android:layout_height="100dp" android:src="@drawable/avatar" app:border_color="#ffffff" app:border_width="4dp" /> ``` 其中 `app:border_color` 和 `app:border_width` 分别表示边的颜色和宽度,可以根据需要调整。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值