Android UI 篇- 实现一个揭露动画

一、应用场景

二、流程分析

三、代码实现

一、应用场景

1、 先上效果图:
效果图
2、 应用场景分析:
  • 适用于 Activity 界面跳转
  • 适用于 View 的切换
  • 支持所有 View 布局的动画效果
3、代码使用(非常简洁好用):
  • 在你需要做动画的布局上,套上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 代码中,调用  
mClipAnimationLayout.startAnimal(RevealAnimationLayout.AnimaType.Circle);  

二、流程分析

1、android 5.0 其实已经拥有对应的接口 ViewAnimationUtils.createCircularReveal ,然而还是有一定的局限性:
  • 只能再 android 5.0 上使用
  • 只提供圆形的揭露效果
  • 也没有提供拓展其他图形接口
2、对于这个效果,我们只能自定义 View,来实现。思路步骤如下:
  • 做一个空父布局,提供出去,可以套任何布局
  • 在空的布局对套进来的子布局操作。
  • 要在 draw 这个函数下手,并且要在 super.draw() 之后去做剪裁,目的是确保子布局先 draw 完。
  • 使用画笔 setXfermode 去实现这个动画效果。先用 Path 画出圆形/矩形,重叠在画布上面,取出重叠的 View
3、我们复写draw函数,对draw函数进行重写。回顾一下draw 函数的流程:
绘制流程
可以重写的只有:
  • draw
  • onDraw (只能绘制自己,绘制不了内部子布局)
  • dispatchDraw
综上:满足条件的只有 drawdispatchDraw重写这两个都可以实现,譬如下面模拟代码:
@Override
    public void draw(Canvas canvas) {
        canvas.saveLayer(mLayer, null, Canvas.ALL_SAVE_FLAG);
        super.draw(canvas); //自身绘制在canvas上,子布局绘制在canvas
        onClipDraw(canvas); //上面代码绘制完毕,再对整个canvas进行操作
        canvas.restore();
    }
@Override
    protected void dispatchDraw(Canvas canvas) {
     canvas.saveLayer(mLayer, null,Canvas.ALL_SAVE_FLAG);
     /**
     *子布局绘制在canvas,自身还没有绘制完毕,还要跑绘制
     *drawAutofilledHighlight-onDrawForeground-drawDefau*ltFocusHighlight。不过没关系我们只要对子布局操作
     */
        super.dispatchDraw(canvas);
        onClipDraw(canvas); //子布局绘制完毕,再对canvas进行操作
        canvas.restore()
    }

三、代码实现

1、首先开启一个动画器,拿到动画执行的 百分比值 :mAnimatorValue
   /**
     * 初始化动画类
     */

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

                //拿到动画的执行的百分比mAnimatorValue
                mAnimatorValue = (float) animation.getAnimatedValue();
                invalidate();
            }
        };

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

    
   /**
     * 开启动画
     * @param animaType 动画类型
     */
    
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、我们再来生成个剪裁路径 mClipPathPath类),需要通过百分比 mAnimatorValue 计算出 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());
            //通过动画的百分比mAnimatorValue,计算出圆的半径
            float r = d / 2 * mAnimatorValue;
            if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.O_MR1) {
            //这里添加了一个圆
                mClipPath.addCircle(center.x, center.y, r, Path.Direction.CW);
                mClipPath.moveTo(0, 0);  // 通过空操作让Path区域占满画布
                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、最后这里我们对 Draw 下手,把生成好的 mClipPath,画到 canvas (整个已经绘制到的布局),使用 PorterDuff.Mode.DST_OUT 拿出 和圆重叠的部分。(其中使用了一个小技巧兼容 android 9.0 详细看下面代码)
@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);
        //取反,因为 android 9.0 不对 paint 以外(paint 自身有指定区域,这里在画笔上添加的区域是一个圆)的布局绘制
        mOpClipPath.op(mClipPath, Path.Op.DIFFERENCE);
        canvas.drawPath(mOpClipPath, mPaint);

    }

代码很简单,就一个布局。可以用来实现 Activity 的跳转,界面部分 View 的切换。也极易拓展,建议大家下载下来看看。

github 完整源码

最后鸣谢:gcssloop作者 给了我灵感。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,602评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,442评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,878评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,306评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,330评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,071评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,382评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,006评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,512评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,965评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,094评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,732评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,283评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,286评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,512评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,536评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,828评论 2 345