掃臉動畫
本文來自網易雲社區
作者:孫有軍
需求
現在視頻應用越來越多了,這裏我們希望在視頻開始之前,希望用戶臉部能夠正對著手機屏幕,以達到更好的效果。
基於上述的需求,這裏我們就需要在視頻流上層疊加一個讓用戶正對手機屏幕的效果,要求是懸浮層具有半透明,不完全遮擋視頻流,同時在界面上留出臉部的形狀,讓用戶有參考物,最後為了更好的視覺效果,我們需要在臉部有一個掃描效果。
實現
可以看出我們主要需要做一下幾個需求:
疊加一個半透明層
在半透明層級上挖出一個臉部形狀的空洞
對空洞實現掃描效果,掃描效果向左向右交替進行,並且在交替過程中,有一個漸變消失的效果
疊加半透明層
疊加半透明層,我們可以在界面上添加一層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+款雲產品!
更多網易研發、產品、運營經驗分享請訪問網易雲社區
相關文章:
【推薦】 分布式存儲系統可靠性系列三:設計模式
掃臉動畫