1. 程式人生 > >掃臉動畫

掃臉動畫

重復執行 n) scan wid 視覺 層級 cti 圖片 繼續

本文來自網易雲社區

作者:孫有軍


需求

現在視頻應用越來越多了,這裏我們希望在視頻開始之前,希望用戶臉部能夠正對著手機屏幕,以達到更好的效果。

基於上述的需求,這裏我們就需要在視頻流上層疊加一個讓用戶正對手機屏幕的效果,要求是懸浮層具有半透明,不完全遮擋視頻流,同時在界面上留出臉部的形狀,讓用戶有參考物,最後為了更好的視覺效果,我們需要在臉部有一個掃描效果。

實現

可以看出我們主要需要做一下幾個需求:

  • 疊加一個半透明層

  • 在半透明層級上挖出一個臉部形狀的空洞

  • 對空洞實現掃描效果,掃描效果向左向右交替進行,並且在交替過程中,有一個漸變消失的效果

疊加半透明層

疊加半透明層,我們可以在界面上添加一層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+款雲產品!

更多網易研發、產品、運營經驗分享請訪問網易雲社區


相關文章:
【推薦】 分布式存儲系統可靠性系列三:設計模式

掃臉動畫