Android水波动画帮助类,一行代码实现View显示/隐藏/startActivity特效(0.3.1)

首先来看一个UI动效图。

动效来自Dribbble
动效来自Dribbble

效果图是是Dribbble上看到的,原作品在此。

我所实现的效果如下:
Watch on YouTube

CircularAnim
CircularAnim

使用方法

为了使用起来简单,我将动画封装成CircularAnim.

现在,让按钮收缩只需一行代码,如下:

CircularAnim.hide(mChangeBtn).go();

同理,让按钮伸展开:

CircularAnim.show(mChangeBtn).go();

以View为水波触发点收缩其它View:

CircularAnim.hide(mContentLayout).triggerView(mLogoBtnIv).go();

以View为水波触发点伸展其它View:

CircularAnim.show(mContentLayout).triggerView(mLogoBtnIv).go();

水波般铺满指定颜色并启动一个Activity:

CircularAnim.fullActivity(MainActivity.this, view)
.colorOrImageRes(R.color.colorPrimary)
.go(new CircularAnim.OnAnimationEndListener() {
@Override
public void onAnimationEnd() {
startActivity(new Intent(MainActivity.this, EmptyActivity.class));
}
});

这里,你还可以放图片:

.colorOrImageRes(R.mipmap.img_huoer_black)

同时,你还可以设置时长、半径、转场动画、动画结束监听器等参数。

用起来非常的方便,一切逻辑性的东西都由帮助类搞定。

Compile

So,你可以如下compile该library了,也可以把这个类CircularAnim拷贝到项目里去。

去GitHub Compile

源码

下面贡献源码。你可以直接新建一个CircularAnim的类,然后把下面的代码复制进去就OK了。

另外,GitHub Demo 地址在此,欢迎Star,欢迎喜欢,欢迎关注,哈哈哈 ^ ^ ~

package top.wefor.circularanim;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.support.annotation.DrawableRes;
import android.view.View;
import android.view.ViewAnimationUtils;
import android.view.ViewGroup;
import android.widget.ImageView;

/**
 * Created on 16/8/5.
 * <p/>
 * 对 ViewAnimationUtils.createCircularReveal() 方法的封装.
 * <p/>
 * GitHub: https://github.com/XunMengWinter
 * <p/>
 * latest edited date: 2016-08-05 20:00
 *
 * @author ice
 */
public class CircularAnim {

    public static final long PERFECT_MILLS = 618;
    public static final int MINI_RADIUS = 0;

    public interface OnAnimationEndListener {
        void onAnimationEnd();
    }

    @SuppressLint("NewApi")
    public static class VisibleBuilder {
        private View mAnimView, mTriggerView;

        private Float mStartRadius, mEndRadius;

        private long mDurationMills = PERFECT_MILLS;

        private boolean isShow;

        private OnAnimationEndListener mOnAnimationEndListener;

        public VisibleBuilder(View animView, boolean isShow) {
            mAnimView = animView;
            this.isShow = isShow;

            if (isShow) {
                mStartRadius = MINI_RADIUS + 0F;
            } else {
                mEndRadius = MINI_RADIUS + 0F;
            }
        }

        public VisibleBuilder triggerView(View triggerView) {
            mTriggerView = triggerView;
            return this;
        }

        public VisibleBuilder startRadius(float startRadius) {
            mStartRadius = startRadius;
            return this;
        }

        public VisibleBuilder endRadius(float endRadius) {
            mEndRadius = endRadius;
            return this;
        }

        public VisibleBuilder duration(long durationMills) {
            mDurationMills = durationMills;
            return this;
        }

        public VisibleBuilder onAnimationEndListener(OnAnimationEndListener onAnimationEndListener) {
            mOnAnimationEndListener = onAnimationEndListener;
            return this;
        }

        public void go() {
            // 版本判断
            if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.LOLLIPOP) {
                if (isShow)
                    mAnimView.setVisibility(View.VISIBLE);
                else
                    mAnimView.setVisibility(View.INVISIBLE);

                if (mOnAnimationEndListener != null)
                    mOnAnimationEndListener.onAnimationEnd();
                return;
            }

            int rippleCX, rippleCY, maxRadius;
            if (mTriggerView != null) {
                int[] tvLocation = new int[2];
                mTriggerView.getLocationInWindow(tvLocation);
                final int tvCX = tvLocation[0] + mTriggerView.getWidth() / 2;
                final int tvCY = tvLocation[1] + mTriggerView.getHeight() / 2;

                int[] avLocation = new int[2];
                mAnimView.getLocationInWindow(avLocation);
                final int avLX = avLocation[0];
                final int avTY = avLocation[1];

                int triggerX = Math.max(avLX, tvCX);
                triggerX = Math.min(triggerX, avLX + mAnimView.getWidth());

                int triggerY = Math.max(avTY, tvCY);
                triggerY = Math.min(triggerY, avTY + mAnimView.getHeight());

                // 以上全为绝对坐标

                int avW = mAnimView.getWidth();
                int avH = mAnimView.getHeight();

                rippleCX = triggerX - avLX;
                rippleCY = triggerY - avTY;

                // 计算水波中心点至 @mAnimView 边界的最大距离
                int maxW = Math.max(rippleCX, avW - rippleCX);
                int maxH = Math.max(rippleCY, avH - rippleCY);
                maxRadius = (int) Math.sqrt(maxW * maxW + maxH * maxH) + 1;
            } else {
                rippleCX = (mAnimView.getLeft() + mAnimView.getRight()) / 2;
                rippleCY = (mAnimView.getTop() + mAnimView.getBottom()) / 2;

                int w = mAnimView.getWidth();
                int h = mAnimView.getHeight();

                // 勾股定理 & 进一法
                maxRadius = (int) Math.sqrt(w * w + h * h) + 1;
            }

            if (isShow && mEndRadius == null)
                mEndRadius = maxRadius + 0F;
            else if (!isShow && mStartRadius == null)
                mStartRadius = maxRadius + 0F;

            Animator anim = ViewAnimationUtils.createCircularReveal(
                    mAnimView, rippleCX, rippleCY, mStartRadius, mEndRadius);
            mAnimView.setVisibility(View.VISIBLE);
            anim.setDuration(mDurationMills);

            anim.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {
                    super.onAnimationEnd(animation);
                    if (isShow)
                        mAnimView.setVisibility(View.VISIBLE);
                    else
                        mAnimView.setVisibility(View.INVISIBLE);

                    if (mOnAnimationEndListener != null)
                        mOnAnimationEndListener.onAnimationEnd();
                }
            });

            anim.start();
        }

    }

    @SuppressLint("NewApi")
    public static class FullActivityBuilder {
        private Activity mActivity;
        private View mTriggerView;
        private float mStartRadius = MINI_RADIUS;
        @DrawableRes
        private int mColorOrImageRes = android.R.color.white;
        private Long mDurationMills;
        private OnAnimationEndListener mOnAnimationEndListener;
        private int mEnterAnim = android.R.anim.fade_in, mExitAnim = android.R.anim.fade_out;

        public FullActivityBuilder(Activity activity, View triggerView) {
            mActivity = activity;
            mTriggerView = triggerView;
        }

        public FullActivityBuilder startRadius(float startRadius) {
            mStartRadius = startRadius;
            return this;
        }

        public FullActivityBuilder colorOrImageRes(@DrawableRes int colorOrImageRes) {
            mColorOrImageRes = colorOrImageRes;
            return this;
        }

        public FullActivityBuilder duration(long durationMills) {
            mDurationMills = durationMills;
            return this;
        }

        public FullActivityBuilder overridePendingTransition(int enterAnim, int exitAnim) {
            mEnterAnim = enterAnim;
            mExitAnim = exitAnim;
            return this;
        }

        public void go(OnAnimationEndListener onAnimationEndListener) {
            mOnAnimationEndListener = onAnimationEndListener;

            // 版本判断,小于5.0则无动画.
            if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.LOLLIPOP) {
                mOnAnimationEndListener.onAnimationEnd();
                return;
            }

            int[] location = new int[2];
            mTriggerView.getLocationInWindow(location);
            final int cx = location[0] + mTriggerView.getWidth() / 2;
            final int cy = location[1] + mTriggerView.getHeight() / 2;
            final ImageView view = new ImageView(mActivity);
            view.setScaleType(ImageView.ScaleType.CENTER_CROP);
            view.setImageResource(mColorOrImageRes);
            final ViewGroup decorView = (ViewGroup) mActivity.getWindow().getDecorView();
            int w = decorView.getWidth();
            int h = decorView.getHeight();
            decorView.addView(view, w, h);

            // 计算中心点至view边界的最大距离
            int maxW = Math.max(cx, w - cx);
            int maxH = Math.max(cy, h - cy);
            final int finalRadius = (int) Math.sqrt(maxW * maxW + maxH * maxH) + 1;

            Animator anim = ViewAnimationUtils.createCircularReveal(view, cx, cy, mStartRadius, finalRadius);
            int maxRadius = (int) Math.sqrt(w * w + h * h) + 1;
            // 若未设置时长,则以PERFECT_MILLS为基准根据水波扩散的距离来计算实际时间
            if (mDurationMills == null) {
                // 算出实际边距与最大边距的比率
                double rate = 1d * finalRadius / maxRadius;
                // 为了让用户便于感触到水波,速度应随最大边距的变小而越慢,扩散时间应随最大边距的变小而变小,因此比率应在 @rate 与 1 之间。
                mDurationMills = (long) (PERFECT_MILLS * Math.sqrt(rate));
            }
            final long finalDuration = mDurationMills;
            // 由于thisActivity.startActivity()会有所停顿,所以进入的水波动画应比退出的水波动画时间短才能保持视觉上的一致。
            anim.setDuration((long) (finalDuration * 0.9));
            anim.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {
                    super.onAnimationEnd(animation);

                    mOnAnimationEndListener.onAnimationEnd();

                    mActivity.overridePendingTransition(mEnterAnim, mExitAnim);

                    // 默认显示返回至当前Activity的动画.
                    mTriggerView.postDelayed(new Runnable() {
                        @Override
                        public void run() {
                            if (mActivity.isFinishing()) return;

                            Animator anim = ViewAnimationUtils.createCircularReveal(view, cx, cy,
                                    finalRadius, mStartRadius);
                            anim.setDuration(finalDuration);
                            anim.addListener(new AnimatorListenerAdapter() {
                                @Override
                                public void onAnimationEnd(Animator animation) {
                                    super.onAnimationEnd(animation);
                                    try {
                                        decorView.removeView(view);
                                    } catch (Exception e) {
                                        e.printStackTrace();
                                    }
                                }
                            });
                            anim.start();
                        }
                    }, 1000);

                }
            });
            anim.start();
        }
    }


    /* 上面为实现逻辑,下面为外部调用方法 */


    /* 伸展并显示@animView */
    public static VisibleBuilder show(View animView) {
        return new VisibleBuilder(animView, true);
    }

    /* 收缩并隐藏@animView */
    public static VisibleBuilder hide(View animView) {
        return new VisibleBuilder(animView, false);
    }

    /* 以@triggerView 为触发点铺满整个@activity */
    public static FullActivityBuilder fullActivity(Activity activity, View triggerView) {
        return new FullActivityBuilder(activity, triggerView);
    }

}

后记

需要注意的是,该帮助类适配了api 19以下的版本,因此你不需要判断版本号,但在这些低版本设备上是没有水波动画效果的,不过好的是并不会影响交互逻辑。
Demo: https://github.com/XunMengWinter/CircularAnim

另外,有木有手机版或者Mac版好用的Gif转换器推荐,表示好难找。
(感谢im_brucezzAkiossDev推荐的GIF录制器:licecap,非常好用,上面的gif已经用这个录制了~)

And有没有傻瓜式发布项目到JCenter的教程推荐?看过几篇都不管用。囧 ~

(感谢Issues区大家的推荐,我使用了YangHuitwiceYuan推荐的JitPack.io,用起来简单很多~)

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

推荐阅读更多精彩内容

  • 【Android 动画】 动画分类补间动画(Tween动画)帧动画(Frame 动画)属性动画(Property ...
    Rtia阅读 5,952评论 1 38
  • 最近在樊登读书会听了一部分书,书单拉出来,最有感觉的就是这本《即兴演讲》,特别有干货,而且可操作。 这个书名字是“...
    水晶Audrey阅读 556评论 0 4
  • 高考季,几家欢喜几家愁,而我属于愁的那家。高中三年的我怀着一颗炽热的电竞梦,他可能摧毁了我的学习。我该后悔吗?分数...
    IR1S阅读 154评论 0 1
  • 昨天,好友在微信群里分享了一篇文章《罗振宇的骗局》,文中开头就以刘刚的例子说明当下年轻人运用付费知识的状态...
    禾苗青青阅读 265评论 0 0