随机跳动的闪屏Logo标题-AnimLogoView

在日常开发中,经常会遇到各种视觉效果,有的效果可能一眼看去会让人觉得很复杂,但是我们必须明确一点:所有复杂动效都是可以分解成单一的基础动作,比如缩放,平移,旋转这些基础单元,然后将所有基础单元动作进行组合,就会产生让人眼前一亮的视觉动效。

首先看下下图效果:

AnimLogoView.gif

按照上面我们提到的思路进行分解:

  1. Logo的名称LitePlayer被拆分为单个文字
  2. 所有文字随机打散在屏幕各个位置
  3. 中间的Logo被隐藏
  4. Logo文字从随机位置平移到页面固定位置
  5. 中间的Logo图片逐渐显示,并且附带从下往上平移一小段位移
  6. Logo被打散的文字组合成名称
  7. Logo组合成名称后,有个渐变的光晕照射效果从左往右移动
  8. 动画结束

当我们把动画拆解后,就可以针对每个拆解单元去构造实现方案了。

  • 首先我们先对logo文字动画进行实现:
  1. 首先对于数据来源,我们期望传入一个logo的字符串,内部将字符串拆解为单个文字数组:
    // fill the text to array
    private void fillLogoTextArray(String logoName) {
        if (TextUtils.isEmpty(logoName)) {
            return;
        }
        if (mLogoTexts.size() > 0) {
            mLogoTexts.clear();
        }
        for (int i = 0; i < logoName.length(); i++) {
            char c = logoName.charAt(i);
            mLogoTexts.put(i, String.valueOf(c));
        }
    }
  1. 所有文字需要随机打散在屏幕各个位置,因为涉及到坐标,我们可以在onSizeChanged中进行logo文字随机位置的初始化,同时我们构建两个集合存储每个文字被打散和组合后的坐标状态:
    // 最终合成logo后的坐标
    private SparseArray<PointF> mQuietPoints = new SparseArray<>();
    // logo被随机打散的坐标
    private SparseArray<PointF> mRadonPoints = new SparseArray<>();

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mWidth = w;
        mHeight = h;
        initLogoCoordinate();
    }

    private void initLogoCoordinate() {
        float centerY = mHeight / 2f + mPaint.getTextSize() / 2 + mLogoOffset;
        // calculate the final xy of the text
        float totalLength = 0;
        for (int i = 0; i < mLogoTexts.size(); i++) {
            String str = mLogoTexts.get(i);
            float currentLength = mPaint.measureText(str);
            if (i != mLogoTexts.size() - 1) {
                totalLength += currentLength + mTextPadding;
            } else {
                totalLength += currentLength;
            }
        }
        // the draw width of the logo must small than the width of this AnimLogoView
        if (totalLength > mWidth) {
            throw new IllegalStateException("This view can not display all text of logoName, please change text size.");
        }
        float startX = (mWidth - totalLength) / 2;
        if (mQuietPoints.size() > 0) {
            mQuietPoints.clear();
        }
        for (int i = 0; i < mLogoTexts.size(); i++) {
            String str = mLogoTexts.get(i);
            float currentLength = mPaint.measureText(str);
            mQuietPoints.put(i, new PointF(startX, centerY));
            startX += currentLength + mTextPadding;
        }
        // generate random start xy of the text
        if (mRadonPoints.size() > 0) {
            mRadonPoints.clear();
        }
        // 构建随机初始坐标
        for (int i = 0; i < mLogoTexts.size(); i++) {
            mRadonPoints.put(i, new PointF((float) Math.random() * mWidth, (float) Math.random() * mHeight));
        }
    }
  1. 构建动画过程,定义一个属性动画从0-1计算进度,在动画过程通过重绘实现文字从凌乱打散的坐标到最终组合坐标进行移动:
    // init the translation animation
    private void initOffsetAnimation() {
        mOffsetAnimator = ValueAnimator.ofFloat(0, 1);
        mOffsetAnimator.setDuration(mOffsetDuration);
        mOffsetAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
        mOffsetAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                if (mQuietPoints.size() <= 0 || mRadonPoints.size() <= 0) {
                    return;
                }
                mOffsetAnimProgress = (float) animation.getAnimatedValue();
                invalidate();
            }
        });
    }

    @Override
    protected void onDraw(Canvas canvas) {
        if (!isOffsetAnimEnd) {// offset animation
            mPaint.setAlpha((int) Math.min(255, 255 * mOffsetAnimProgress + 100));
            for (int i = 0; i < mQuietPoints.size(); i++) {
                PointF quietP = mQuietPoints.get(i);
                PointF radonP = mRadonPoints.get(i);
                float x = radonP.x + (quietP.x - radonP.x) * mOffsetAnimProgress;
                float y = radonP.y + (quietP.y - radonP.y) * mOffsetAnimProgress;
                canvas.drawText(mLogoTexts.get(i), x, y, mPaint);
            }
        }
    }
  1. 此时我们已经把logo文字动画实现了,接下来看我们拆解的第7步,还有个光照效果。对于这种光照效果,首选方案是通过Gradient+Shader实现。因为绘制渐变也涉及到坐标,所以动画的初始化我们也放到了onSizeChanged中进行:
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mWidth = w;
        mHeight = h;
        initLogoCoordinate();// 初始化坐标动画
        initGradientAnimation(w);// 初始化渐变动画
    }

    // init the gradient animation
    private void initGradientAnimation(int width) {
        mGradientAnimator = ValueAnimator.ofInt(0, 2 * width);
        if (mGradientListener != null) {
            mGradientAnimator.addListener(mGradientListener);
        }
        mGradientAnimator.setDuration(mGradientDuration);
        mGradientAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mMatrixTranslate = (int) animation.getAnimatedValue();
                invalidate();
            }
        });
        mLinearGradient = new LinearGradient(-width, 0, 0, 0, new int[]{mTextColor, mGradientColor, mTextColor},
                new float[]{0, 0.5f, 1}, Shader.TileMode.CLAMP);
        mGradientMatrix = new Matrix();
    }
  1. 渐变动画是在文字移动动画结束后自动播放的,所以我们可以在初始化文字移动动画时对动画结束进行监听处理,同时在绘制onDraw中对文字进行绘制:
    // init the translation animation
    private void initOffsetAnimation() {
        ...
        // 初始化移动动画
        ...
        mOffsetAnimator.addListener(new AnimatorListenerAdapter() {

            @Override
            public void onAnimationEnd(Animator animation) {
                if (mGradientAnimator != null && isShowGradient) {
                    isOffsetAnimEnd = true;
                    mPaint.setShader(mLinearGradient);
                    mGradientAnimator.start();
                }
            }
        });
    }

    @Override
    protected void onDraw(Canvas canvas) {
        if (!isOffsetAnimEnd) {// offset animation
            ...
            // 文字移动动画
            ...
        } else {// gradient animation
            for (int i = 0; i < mQuietPoints.size(); i++) {
                PointF quietP = mQuietPoints.get(i);
                canvas.drawText(mLogoTexts.get(i), quietP.x, quietP.y, mPaint);
            }
            mGradientMatrix.setTranslate(mMatrixTranslate, 0);
            mLinearGradient.setLocalMatrix(mGradientMatrix);
        }
    }
  1. 到此,文字动画已经实现了。剩下来就是一些自定义属性的定义,对外提供一些属性的settergetter方法了,同时需要考虑在页面生命周期过程中动画的资源释放。好了,看下我们实现的效果:
    AnimLogoView2.gif
  1. 对于上面Logo图片的动画可以单独对一个ImageView进行平移+透明度动画实现,这里就不花篇幅去描述了。

自定义View我相信大部分同学都已经掌握熟练,但是对于复杂动画,是否能够将这些熟练的能力用在刀刃上呢,也许会有部份同学看到一个华丽的效果就不知所措了。本文没有对动画进行深入的分析,也没涉及到复杂的数据运算,只是通过一个简单的例子,阐述了一种通用的动效分析实现的方式,通过这种思维方式,你可以很清晰的了解自己每一步的实现以及目标。

最后总结一下,对于自定义动效而言,我们首先可以让UI提供最终视觉效果,通过工具进行单帧解析,观察其中的每一帧之间的动作关系,将其拆解为一个个基础单元。接着针对每个单元步骤进行实现,最后整合到一起,就能够实现一个连贯的效果了。这是一种思想,当你熟练掌握这种思想后,还需要对一些数学知识有一定的了解,比如三角函数,矩阵运算等等。只要培养好这两方面能力,日常开发中,任何复杂的动效都不足以为惧。

附项目源码地址: https://github.com/seagazer/animlogoview

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

推荐阅读更多精彩内容