【BYM】Android 实现相机快门动画

blow your mind

bym系列意在除开技术分享,还分享下思路,不止是做一个代码的搬运工。

1.背景简介

公司业务需要给相机做个快门动画,找了全网,案例不多。花了两天,终于用自定义View手写了一个快门动画。也算是自己的一个小进步,整体做完感觉其实难点不在于绘制,而在于数学。

2.复习几个知识点

Canvas

canvas是我们Android的画布,和其他语言一样,只要你有了下面的Paint你就可以随意绘制了。

Paint

Paint 常用方法就是setColor,setStyle,setStrokeWidth

Path

canvas基本的一些图形功能无法满足我们的需求的时候,我们可以使用path进行组合绘制,最后在调用canvas.drawPathpath.close()可以用来关闭图形,如果你设置了path.setStyle(Paint.Style.FILL),将填充满整个path的图形。

数学知识

sin正弦函数,cos余弦函数,好好复习下,可以辅导孩子写作业了。?

3.需求效果

图片.gif

4. 需求分析

image.png

image.png

image.png

我们抽取Gif的三张图片来分析。图形由2部分组成,1部分是外圈的圆,一部分是动态的不规则图形。

5.解题思路

a.找寻圆上的2个点

首先是6个等边三角形,逐步的往内圈圆心聚合,等边三角形的间距在缩小。那么这6个等边三角形只是所处的角度不一样,编写代码初期,我们可以先画一个,来实现一个等边三角形往圆心聚合的效果。
image.png

//可以得出我们的两个点(0,-R),(√3/2/R,R/2)
 float startX = 0;
 float startY = -mRadius;
 float stopX = (float) Math.sqrt(3) * mRadius / 2f;
 float stopY = -mRadius / 2f;
复制代码

b.找寻第三个点TagPoint。

如果简单的实现的话,我们可以当做第三个点,就在蓝色的线上。此时只要求解,当第三个点与(0,-R)组成的边a,(0,-R)与(√3/2/R,R/2)点组成的边b的夹角Θ的值动态变化下,我们第三个点的位置就行了。也就是说夹角Θ是变量,从0到60度。

WX20210430-094133@2x.png

c.推导公式求解x和y。

辅助线我就不画了,我直接给出推导公式

image.png

转换成java代码如下

  float tagX = (float) (Math.sin(Math.toRadians(60)) * mRadius 
  / Math.cos(Math.toRadians(30 - swipeAgenl)) 
  * Math.sin(Math.toRadians(60 - swipeAgenl)));
  
  float tagY = (float) (Math.sin(Math.toRadians(60)) * mRadius 
  / Math.cos(Math.toRadians(30 - swipeAgenl)) 
  * Math.cos(Math.toRadians(60 - swipeAgenl))) - mRadius;

复制代码

运行看下效果是不是符合预期

wkxyz-u17vn.gif

4.圆弧

点(0,-R)到(√3/2/R,R/2)点与圆相交处会有个圆弧。这里利用Path.addArc或者canvas.drawArc方法都可以绘制。所传参方式有两种,其实本质都是传入一个矩形然后在矩形框内绘制一个圆弧。

Path.java
 public void addArc(RectF oval, float startAngle, float sweepAngle) {
        addArc(oval.left, oval.top, oval.right, oval.bottom, startAngle, sweepAngle);
    }
   Canvas.java
  public void drawArc(@NonNull RectF oval, float startAngle, float sweepAngle, boolean useCenter,
            @NonNull Paint paint) {
        drawArc(oval.left, oval.top, oval.right, oval.bottom, startAngle, sweepAngle, useCenter,
                paint);
    }
    
复制代码

startAngle 为开始的角度,sweepAngle为圆弧滑过的角度。代码如下:

  RectF oval = new RectF(-mRadius, -mRadius, mRadius, mRadius);
  mPaint.setStyle(Paint.Style.FILL);
  path.addArc(oval, -90, 60);
复制代码

5.看看效果

画出了一个,那么6个就很好画,为了方面观察效果,我把每个都整成了不同颜色,方便大家看。

r729h-42qdy.gif

咦,不对劲呀,和我预想的不一样。我们可以再看看UI做的效果图。抛开图形旋转角度的因素,可以观察到,图形是随着时间的变化,红色边线一直拉长而形成的。最大长度是R,最小长度是0,或者你设置任意初始值,还有一点要注意,在红线延伸的过程中,夹角始终是60度。

WX20210430-101533@2x.png

6.重新计算

依旧是正弦函数,余弦函数,算出点在红线上的位置。

 float L1 = (float) Math.sqrt(Math.pow(mRadius, 2.0) - 
 Math.pow(Math.sin(Math.toRadians(60)) * a, 2.0));
 
 float consL1 = L1 / mRadius;
 float sinL1 = (float) Math.sin(Math.toRadians(60)) * a / mRadius;
 float L = (float) (L1 + Math.sin(Math.toRadians(30)) * a);

 float tagX = L * sinL;
 float tagY = L * consL - mRadius;
复制代码

7.大功告成

99u4k-wt6bw.gif

/**
 * @authoer create by markfrain
 * @github https://github.com/furuiCQ
 * 高怀见物理 和气得天真
 * 时间: 4/28/21
 * 描述: ShootView
 */
public class ShootView extends View {
    private static final int DEGREE_60 = 60;

    private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    private final RectF mBounds = new RectF();
    private int mRadius;
    private int mCenterX;
    private int mCenterY;



    private ValueAnimator mPlayAnimator;
    private float swipeAgenl = mRadius;//延伸的距离


    private float mShootLineTotalRotateAngle;//旋转角度
    private ValueAnimator mPreShootLineTotalRotateAnimator;


    private float padding = 20;
    private ValueAnimator paddingAnimator;

    private float fix = 4;
    private ValueAnimator fixAnimator;


    private static final float SHOOT_LINE_ROTATE_END_RADIANS = (float) (Math.PI / 6.0);//旋转结束弧度
    private static final float SHOOT_LINE_ROTATE_START_RADIANS = (float) (Math.PI / 2.0);//旋转开始弧度
    private static final float SHOOT_LINE_ROTATE_START_DEGREE =
            (float) Math.toDegrees(SHOOT_LINE_ROTATE_END_RADIANS);//旋转开始角度
    private static final int PRE_SHOOT_LINE_TOTAL_ROTATE_DURATION = 1000;

    public static final Property<ShootView, Float> SHOOT_LINE_TOTAL_ROTATE_DEGREE =
            new Property<ShootView, Float>(Float.class, null) {
                @Override
                public Float get(ShootView object) {
                    return object.mShootLineTotalRotateAngle;
                }

                @Override
                public void set(ShootView object, Float value) {
                    object.mShootLineTotalRotateAngle = value;
                    object.invalidate();
                }
            };

    public ShootView(Context context) {
        this(context, null);
    }

    public ShootView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public ShootView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(5);
        mPaint.setColor(Color.parseColor("#ffc6c6c6"));

        paddingAnimator = ValueAnimator.ofFloat(20, 0);
        paddingAnimator.setInterpolator(new LinearInterpolator());
        paddingAnimator.setDuration(PRE_SHOOT_LINE_TOTAL_ROTATE_DURATION);
        paddingAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                padding = (float) animation.getAnimatedValue();
                invalidate();
            }
        });
        fixAnimator = ValueAnimator.ofFloat(4, 0);
        fixAnimator.setInterpolator(new LinearInterpolator());
        fixAnimator.setDuration(1200);
        fixAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                fix = (float) animation.getAnimatedValue();
                invalidate();
            }
        });

        mPreShootLineTotalRotateAnimator =
                ValueAnimator.ofFloat(-(SHOOT_LINE_ROTATE_START_DEGREE / 2.0f) - 240.0f,
                        -(SHOOT_LINE_ROTATE_START_DEGREE / 2.0f) - 120.0f);
        mPreShootLineTotalRotateAnimator.setInterpolator(new LinearInterpolator());
        mPreShootLineTotalRotateAnimator.setDuration(PRE_SHOOT_LINE_TOTAL_ROTATE_DURATION);
        mPreShootLineTotalRotateAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mShootLineTotalRotateAngle = (float) animation.getAnimatedValue();
                invalidate();
            }
        });
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

//        drawTest(canvas);
        drawArc(canvas);
        drawCircle(canvas);

    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mBounds.set(0 + getPaddingLeft(), 0 + getPaddingTop(), w - getPaddingRight(),
                h - getPaddingBottom());

        mRadius = (int) (Math.min(mBounds.width(), mBounds.height()) / 2);
        mCenterX = (int) mBounds.centerX();
        mCenterY = (int) mBounds.centerY();
        swipeAgenl = mRadius * 2 / 3f;
    }

    float mStartX;
    float mStartY;

    int colors[] = {Color.RED, Color.GREEN, Color.YELLOW, Color.MAGENTA, Color.CYAN, Color.GRAY};

    private void drawTest(Canvas canvas) {
        drawCircle(canvas);
        canvas.save();
        canvas.translate(mCenterX, mCenterY);

        int i = 0;
        canvas.save();
        canvas.rotate(-DEGREE_60 * i);
        Path path = new Path();

        float stopX = (float) Math.sqrt(3) * mRadius / 2f;
        float stopY = -mRadius / 2f;

        canvas.drawLine(0, -mRadius, stopX, stopY, mPaint);
        mPaint.setColor(Color.RED);
        canvas.drawPoint(stopX, stopY, mPaint);
        mPaint.setColor(Color.RED);

        //canvas.drawLine(0, 0, stopX, stopY, mPaint);

        RectF oval = new RectF(-mRadius, -mRadius, mRadius, mRadius);


        RectF rectF = new RectF(-mRadius, -2 * mRadius, mRadius, 0);

        mPaint.setStyle(Paint.Style.FILL);
        path.addArc(oval, -90, 60);


        //计算直接点的
        float tagX = (float) (Math.sin(Math.toRadians(60)) * mRadius / Math.cos(Math.toRadians(30 - swipeAgenl)) * Math.sin(Math.toRadians(60 - swipeAgenl)));
        float tagY = (float) (Math.sin(Math.toRadians(60)) * mRadius / Math.cos(Math.toRadians(30 - swipeAgenl)) * Math.cos(Math.toRadians(60 - swipeAgenl))) - mRadius;

//        float tagX = Math.sqrt(Math.pow(mRadius,2.0) - Math.pow(Math.sin(Math.toRadians(60) * swipeAgenl), 2.0));

//        float L = (float) Math.sqrt(Math.pow(mRadius, 2.0) - Math.pow(Math.sin(Math.toRadians(60)) * swipeAgenl, 2.0));
//        float consL = L / mRadius;
//        float sinL = (float) Math.sin(Math.toRadians(60)) * swipeAgenl / mRadius;
//        float LL = (float) (L + Math.sin(Math.toRadians(30)) * swipeAgenl);
//
//        float tagX = LL * sinL;
//        float tagY = LL * consL - mRadius;

         canvas.drawPoint(tagX, tagY, mPaint);

//        path.moveTo(stopX, stopY);
//        path.lineTo(tagX, tagY);
//        path.lineTo(0, -mRadius);
        canvas.drawPath(path, mPaint);
//        }


    }

    private void drawArc(Canvas canvas) {

        canvas.save();
        canvas.translate(mCenterX, mCenterY);
        canvas.rotate(mShootLineTotalRotateAngle);
        for (int i = 0; i < 6; i++) {
            canvas.save();
            canvas.rotate(-DEGREE_60 * i);
            Path path = new Path();
            mPaint.setColor(colors[i]);
            RectF oval = new RectF(-mRadius, -mRadius, mRadius, mRadius);
            mPaint.setStyle(Paint.Style.FILL);

//            mRadius- padding
            double asin = Math.toDegrees(Math.asin((mRadius - padding) / mRadius / 2)) * 2;
            path.addArc(oval, -90, (float) asin);

            float stopX = (float) Math.sqrt(3) * mRadius / 2f;
            float stopY = -mRadius / 2f;
            float L = (float) Math.sqrt(Math.pow(mRadius, 2.0) - Math.pow(Math.sin(Math.toRadians(60)) * swipeAgenl, 2.0));
            float consL = L / mRadius;
            float sinL = (float) Math.sin(Math.toRadians(60)) * swipeAgenl / mRadius;
            float LL = (float) (L + Math.sin(Math.toRadians(30)) * swipeAgenl);

            float tagX = LL * sinL;
            float tagY = LL * consL - mRadius;

            path.moveTo(stopX - padding, stopY - padding - fix);
            path.lineTo(tagX - padding, tagY - padding);
            path.lineTo(0, -mRadius);
            canvas.drawPath(path, mPaint);
            canvas.restore();
        }
        canvas.restore();
    }

    private void drawCircle(Canvas canvas) {
        mPaint.setColor(colors[1]);
        mPaint.setStyle(Paint.Style.STROKE);
        canvas.save();
        canvas.translate(mCenterX, mCenterY);
        canvas.drawCircle(0.0f, 0.0f, mRadius, mPaint);
        canvas.restore();
    }

    public void startPlay() {
        mPlayAnimator = ValueAnimator.ofFloat(mRadius, mRadius / 5f);
        mPlayAnimator.setDuration(PRE_SHOOT_LINE_TOTAL_ROTATE_DURATION);
        mPlayAnimator.setInterpolator(new LinearInterpolator());
        mPlayAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                swipeAgenl = (float) animation.getAnimatedValue();
                Log.i("swipeAgenl", "" + swipeAgenl);
                invalidate();
            }
        });
        mPlayAnimator.start();
        mPreShootLineTotalRotateAnimator.start();
        paddingAnimator.start();
        fixAnimator.start();
    }
}

复制代码

源代码Github

参考文献

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享