zoukankan      html  css  js  c++  java
  • 扫脸动画

    本文来自网易云社区

    作者:孙有军


    需求

    现在视频应用越来越多了,这里我们希望在视频开始之前,希望用户脸部能够正对着手机屏幕,以达到更好的效果。

    基于上述的需求,这里我们就需要在视频流上层叠加一个让用户正对手机屏幕的效果,要求是悬浮层具有半透明,不完全遮挡视频流,同时在界面上留出脸部的形状,让用户有参考物,最后为了更好的视觉效果,我们需要在脸部有一个扫描效果。

    实现

    可以看出我们主要需要做一下几个需求:

    • 叠加一个半透明层

    • 在半透明层级上挖出一个脸部形状的空洞

    • 对空洞实现扫描效果,扫描效果向左向右交替进行,并且在交替过程中,有一个渐变消失的效果

    叠加半透明层

    叠加半透明层,我们可以在界面上添加一层View,对View设置半透明背景,保持View占满整个屏幕。也可以自定义一个View,绘制View为半透明,这里我们需要后续效果都作用在同一个View上,因此我们采用第二种,自定义View。

    canvas.drawRect(0, 0, width, height, paint);

    这里主要是绘制一个半透明矩形框

    绘制的颜色,设置在paint中

    挖洞

    有了半透明层后,我们需要在界面上挖洞,挖出的形状需要视觉定义,之后将两层叠加进行处理,这里我们可以设置后一个图片的PorterDuffXfermode属性,我们设置属性为PorterDuff.Mode.DST_OUT,将后面部分露出,因此我们需要在前一次的基础上进行处理:

    canvas.saveLayer(0, 0, width, height, null, Canvas.ALL_SAVE_FLAG);
    canvas.drawRect(0, 0, width, height, paint);
    
    canvas.drawBitmap(mask, upLeft, upTop, paint1);
    canvas.restore();


    这里主要是新开了一个Layer

    其次将Mask绘制到View,paint1设置了Xfermode属性为new PorterDuffXfermode(PorterDuff.Mode.DST_OUT)

    扫描

    扫描效果是独立与上面的View,只是位置上是在挖洞的位置,因此我们需要单独处理,这里我们扫描动画分为两个,一个向左,一向右,这里原理是与上面一致的,也是每次调整扫描View的宽度,因此我们需要裁剪一个与空洞相同大小的扫描层,每次改变扫描图形的宽度,因此这里我们需要对paint设置Xfermode的属性为SRC_IN,保证相交部分上部分露出。

    裁剪后,我们只需要每次更改扫描图形的宽度,这里我们可以动态改变每次的宽度,我们可以采用一个ValueAnimator来动态改变宽度,也可以采用Handler加postDelayed来实现,这里采用ValueAnimator来实现,ValueAnimator重复执行:

    canvas.saveLayer(0, 0, width, height, null, Canvas.ALL_SAVE_FLAG);
    canvas.translate(upLeft, upTop);
    canvas.drawRect(startX, 0, endX, faceH, paint3);
    canvas.drawBitmap(left2Right ? leftScan : rightScan, 0, 0, paint2);
    canvas.translate(-upLeft, -upTop);
    canvas.restore();

    这里我们继续在刚才的View上叠加,我们新创建一个Layer,在新的Layer上进行绘制,canvas.translate其实可以不执行,不需要将原点移动到空洞的左上角,如果不移动这里需要注意叠加位置。

    首先我们绘制一个动态改变的矩形框

    将扫描图片与矩形框融合,这里需要注意的是两个扫描图片是不同的,因此在转向的时候需要绘制对应的图形

    在交替的时候,我们动态改变paint2的alpha值,实现渐变消失的效果

    代码

    完整代码如下:

    public class ScanView extends View {
    
        int width;
    
        int height;
    
        private Paint paint, paint1, paint2, paint3;
    
        private Bitmap mask, rect, leftScan, rightScan;
    
        private float ratio = 0.55f;
    
        private int upLeft, upTop;
    
        private int downLeft, downTop;
    
        private int faceW, faceH;
    
        private ValueAnimator animator;
    
        private int offset, startX, endX;
    
        public static final int OFFSET_ONE_TIME = 5;
    
        public static final int STAY_TIME = 25 * OFFSET_ONE_TIME;
    
        public ScanView(Context context) {
            super(context);
            init(context, null);
        }
    
        public ScanView(Context context, @Nullable AttributeSet attrs) {
            super(context, attrs);
            init(context, attrs);
        }
    
        public ScanView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            init(context, attrs);
        }
    
        private void init(Context context, AttributeSet attrs) {
            mask = BitmapFactory.decodeResource(context.getResources(), R.drawable.face_mask);
            rect = BitmapFactory.decodeResource(context.getResources(), R.drawable.face_rect);
            leftScan = BitmapFactory.decodeResource(context.getResources(), R.drawable.up_left_scan);
            rightScan = BitmapFactory.decodeResource(context.getResources(), R.drawable.up_right_scan);
            faceW = mask.getWidth();
            faceH = mask.getHeight();
            paint = new Paint();
            paint.setAntiAlias(true);
            paint.setColor(Color.parseColor("#4C0C0F3C"));
            paint1 = new Paint();
            paint1.setAntiAlias(true);
            paint1.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
            paint2 = new Paint();
            paint2.setAntiAlias(true);
            paint2.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
            paint3 = new Paint();
            paint3.setAntiAlias(true);
            paint3.setColor(Color.parseColor("#000000"));
        }
    
        @Override
        protected void onSizeChanged(int w, int h, int oldw, int oldh) {
            super.onSizeChanged(w, h, oldw, oldh);
            width = w;
            height = h;
            upLeft = width / 2 - mask.getWidth() / 2;
            upTop = (int) (height * ratio / 2 - mask.getHeight() / 2);
    
            downLeft = width / 2 - mask.getWidth() / 2;
            downTop = (int) (((height * (1 - ratio)) / 2 - mask.getHeight() / 2) + height * ratio);
            scan();
        }
    
        private void scan() {
            ValueAnimator animator = getAnimator();
            animator.start();
        }
    
        public void stop() {
            if (animator != null) {
                animator.cancel();
            }
        }
    
        boolean left2Right = true;
    
        @NonNull
        private ValueAnimator getAnimator() {
            if (animator == null) {
                animator = ValueAnimator.ofFloat(0.0f, 1.0f);
                animator.setDuration(3000);
                animator.setRepeatCount(ValueAnimator.INFINITE);
                animator.setRepeatMode(ValueAnimator.REVERSE);
                animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                    @Override
                    public void onAnimationUpdate(ValueAnimator animation) {
                        if (left2Right) {
                            offset += OFFSET_ONE_TIME;
                            startX = 0;
                            if (offset > faceW + STAY_TIME) {
                                left2Right = false;
                                endX = faceW;
                            } else if (offset > faceW) {
                                paint2.setAlpha(paint2.getAlpha() - 10);
                                endX = faceW;
                            } else {
                                endX = offset < 0 ? 0 : offset;
                                paint2.setAlpha(255);
                            }
                        } else {
                            offset -= OFFSET_ONE_TIME;
                            if (offset < -STAY_TIME) {
                                left2Right = true;
                                startX = 0;
                            } else if (offset < 0) {
                                startX = 0;
                                paint2.setAlpha(paint2.getAlpha() - 10);
                            } else {
                                startX = offset > faceW ? faceW : offset;
                                paint2.setAlpha(255);
                            }
                            endX = faceW;
                        }
                        invalidate();
                    }
                });
            }
            return animator;
        }
    
        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
            canvas.saveLayer(0, 0, width, height, null, Canvas.ALL_SAVE_FLAG);
            canvas.drawRect(0, 0, width, height, paint);
    
            canvas.drawBitmap(mask, upLeft, upTop, paint1);
            canvas.drawBitmap(mask, downLeft, downTop, paint1);
            canvas.restore();
            canvas.saveLayer(0, 0, width, height, null, Canvas.ALL_SAVE_FLAG);
            canvas.translate(upLeft, upTop);
            canvas.drawRect(startX, 0, endX, faceH, paint3);
            canvas.drawBitmap(left2Right ? leftScan : rightScan, 0, 0, paint2);
            canvas.translate(-upLeft, -upTop);
            canvas.restore();
            canvas.drawBitmap(rect, upLeft, upTop, null);
            canvas.drawBitmap(rect, downLeft, downTop, null);
        }
    
        @Override
        protected void onDetachedFromWindow() {
            super.onDetachedFromWindow();
            stop();
        }
    }
    • 这里需要注意的几点是设置ValueAnimator的重复次数为INFINITE

    • 我们采用了每次改变位置的方式来动态改变裁剪的位置,STAY_TIME表示渐变消失的位置移动距离

    • 对所使用的bitmap提前加载

    • 这里我们还可以采用自定义属性来定义是否自动开启,半透明颜色,这里都硬编码了

    运行效果

    完成后,我们可以动态运行一下该效果,我们将自定义View使用在界面中,这里界面上加入了一个背景图片,界面布局代码如下:


    <?xml version="1.0" encoding="utf-8"?><FrameLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@drawable/bg"
        tools:context="com.demo.example.activity.MoveActivity">
    
        <com.demo.example.widget.ScanView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:visibility="visible"/></FrameLayout>

    主要是为了显示效果,效果如下,这里由于上传限制,压缩了一个比较小的gif:



    网易云免费体验馆,0成本体验20+款云产品! 

    更多网易研发、产品、运营经验分享请访问网易云社区

         


    相关文章:
    【推荐】 分布式存储系统可靠性系列三:设计模式

  • 相关阅读:
    Python 模拟SQL对文件进行增删改查
    Python用户登陆
    计算程序的内存和占比
    列出top中的pid
    编写类du命令Python脚本
    生成器版本的文件MD5校验
    利用os、hash模块生成目录下所有文件的md5
    文件Copy和文件夹Copy
    Access数据库连接方式
    js常用方法收集
  • 原文地址:https://www.cnblogs.com/163yun/p/9711860.html
Copyright © 2011-2022 走看看