Android UI 篇- 實(shí)現(xiàn)一個(gè)揭露動(dòng)畫(huà)

一、應(yīng)用場(chǎng)景

二、流程分析

三、代碼實(shí)現(xiàn)

一、應(yīng)用場(chǎng)景

1、 先上效果圖:
效果圖
2、 應(yīng)用場(chǎng)景分析:
  • 適用于 Activity 界面跳轉(zhuǎn)
  • 適用于 View 的切換
  • 支持所有 View 布局的動(dòng)畫(huà)效果
3、代碼使用(非常簡(jiǎn)潔好用):
  • 在你需要做動(dòng)畫(huà)的布局上,套上RevealAnimationLayout 就可以了,支持套任何布局!!!
    <com.revealanimation.RevealAnimationLayout
        android:id="@+id/animat_layout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        >
        <ImageView
            android:layout_width="match_parent"
            android:layout_height="400dp"
            android:layout_marginLeft="50dp"
            android:layout_marginRight="50dp"
            android:scaleType="centerCrop"
            android:src="@mipmap/test"/>
    </com.revealanimation.RevealAnimationLayout>
    
 // java 代碼中,調(diào)用  
mClipAnimationLayout.startAnimal(RevealAnimationLayout.AnimaType.Circle);  

二、流程分析

1、android 5.0 其實(shí)已經(jīng)擁有對(duì)應(yīng)的接口 ViewAnimationUtils.createCircularReveal ,然而還是有一定的局限性:
  • 只能再 android 5.0 上使用
  • 只提供圓形的揭露效果
  • 也沒(méi)有提供拓展其他圖形接口
2、對(duì)于這個(gè)效果,我們只能自定義 View,來(lái)實(shí)現(xiàn)。思路步驟如下:
  • 做一個(gè)空父布局,提供出去,可以套任何布局
  • 在空的布局對(duì)套進(jìn)來(lái)的子布局操作。
  • 要在 draw 這個(gè)函數(shù)下手,并且要在 super.draw() 之后去做剪裁,目的是確保子布局先 draw 完。
  • 使用畫(huà)筆 setXfermode 去實(shí)現(xiàn)這個(gè)動(dòng)畫(huà)效果。先用 Path 畫(huà)出圓形/矩形,重疊在畫(huà)布上面,取出重疊的 View
3、我們復(fù)寫(xiě)draw函數(shù),對(duì)draw函數(shù)進(jìn)行重寫(xiě)。回顧一下draw 函數(shù)的流程:
繪制流程
可以重寫(xiě)的只有:
  • draw
  • onDraw (只能繪制自己,繪制不了內(nèi)部子布局)
  • dispatchDraw
綜上:滿(mǎn)足條件的只有 drawdispatchDraw重寫(xiě)這兩個(gè)都可以實(shí)現(xiàn),譬如下面模擬代碼:
@Override
    public void draw(Canvas canvas) {
        canvas.saveLayer(mLayer, null, Canvas.ALL_SAVE_FLAG);
        super.draw(canvas); //自身繪制在canvas上,子布局繪制在canvas
        onClipDraw(canvas); //上面代碼繪制完畢,再對(duì)整個(gè)canvas進(jìn)行操作
        canvas.restore();
    }
@Override
    protected void dispatchDraw(Canvas canvas) {
     canvas.saveLayer(mLayer, null,Canvas.ALL_SAVE_FLAG);
     /**
     *子布局繪制在canvas,自身還沒(méi)有繪制完畢,還要跑繪制
     *drawAutofilledHighlight-onDrawForeground-drawDefau*ltFocusHighlight。不過(guò)沒(méi)關(guān)系我們只要對(duì)子布局操作
     */
        super.dispatchDraw(canvas);
        onClipDraw(canvas); //子布局繪制完畢,再對(duì)canvas進(jìn)行操作
        canvas.restore()
    }

三、代碼實(shí)現(xiàn)

1、首先開(kāi)啟一個(gè)動(dòng)畫(huà)器,拿到動(dòng)畫(huà)執(zhí)行的 百分比值 :mAnimatorValue
   /**
     * 初始化動(dòng)畫(huà)類(lèi)
     */

    private void initAnimator() {
        mUpdateListener = new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {

                //拿到動(dòng)畫(huà)的執(zhí)行的百分比mAnimatorValue
                mAnimatorValue = (float) animation.getAnimatedValue();
                invalidate();
            }
        };

        mStartingAnimator = new ValueAnimator().setDuration(defaultDuration);
        mStartingAnimator.setInterpolator(new AccelerateInterpolator());
        mStartingAnimator.addUpdateListener(mUpdateListener);
    }

    
   /**
     * 開(kāi)啟動(dòng)畫(huà)
     * @param animaType 動(dòng)畫(huà)類(lèi)型
     */
    
public void startAnimal(AnimaType animaType) {
        this.mAnimaType = animaType;
        setVisibility(View.VISIBLE);
        mStartingAnimator.cancel();
        if(mAnimaType == AnimaType.BackCircle ||
                mAnimaType == AnimaType.BackLeftRight ||
                mAnimaType == AnimaType.BackUpDown ) {
            mStartingAnimator.setFloatValues(1,0);
        } else {
            mStartingAnimator.setFloatValues(0,1);
        }
        mStartingAnimator.start();
    } 
    
    
2、我們?cè)賮?lái)生成個(gè)剪裁路徑 mClipPathPath類(lèi)),需要通過(guò)百分比 mAnimatorValue 計(jì)算出 mClipPath 需要添加半徑為多大的圓 :
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mLayer.set(0, 0, w, h);
        refreshRegion(this);
    }

    public void refreshRegion(View view) {
        int w = (int) mLayer.width();
        int h = (int) mLayer.height();
        RectF areas = new RectF();
        areas.left = view.getPaddingLeft();
        areas.top = view.getPaddingTop();
        areas.right = w - view.getPaddingRight();
        areas.bottom = h - view.getPaddingBottom();
        mClipPath.reset();

        PointF center = new PointF(w / 2, h / 2);
        if (mAnimaType == AnimaType.Circle || mAnimaType == AnimaType.BackCircle) {
            float d = (float) Math.hypot(areas.width(), areas.height());
            //通過(guò)動(dòng)畫(huà)的百分比mAnimatorValue,計(jì)算出圓的半徑
            float r = d / 2 * mAnimatorValue;
            if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.O_MR1) {
            //這里添加了一個(gè)圓
                mClipPath.addCircle(center.x, center.y, r, Path.Direction.CW);
                mClipPath.moveTo(0, 0);  // 通過(guò)空操作讓Path區(qū)域占滿(mǎn)畫(huà)布
                mClipPath.moveTo(w, h);
            } else {
                float y = h / 2 - r;
                mClipPath.moveTo(areas.left, y);
                mClipPath.addCircle(center.x, center.y, r, Path.Direction.CW);
            }
        }
    }
3、最后這里我們對(duì) Draw 下手,把生成好的 mClipPath,畫(huà)到 canvas (整個(gè)已經(jīng)繪制到的布局),使用 PorterDuff.Mode.DST_OUT 拿出 和圓重疊的部分。(其中使用了一個(gè)小技巧兼容 android 9.0 詳細(xì)看下面代碼)
@Override
    public void draw(Canvas canvas) {
        canvas.saveLayer(mLayer, null, Canvas.ALL_SAVE_FLAG);
        super.draw(canvas);
        onClipDraw(canvas);
        canvas.restore();
    }


    public void onClipDraw(Canvas canvas) {
        mPaint.setColor(Color.WHITE);
        mPaint.setStyle(Paint.Style.FILL);

        mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));

        mOpClipPath.reset();
        mOpClipPath.addRect(0, 0, mLayer.width(), mLayer.height(), Path.Direction.CW);
        //取反,因?yàn)?android 9.0 不對(duì) paint 以外(paint 自身有指定區(qū)域,這里在畫(huà)筆上添加的區(qū)域是一個(gè)圓)的布局繪制
        mOpClipPath.op(mClipPath, Path.Op.DIFFERENCE);
        canvas.drawPath(mOpClipPath, mPaint);

    }

代碼很簡(jiǎn)單,就一個(gè)布局。可以用來(lái)實(shí)現(xiàn) Activity 的跳轉(zhuǎn),界面部分 View 的切換。也極易拓展,建議大家下載下來(lái)看看。

github 完整源碼

最后鳴謝:gcssloop作者 給了我靈感。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。