随机跳动的闪屏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

推荐阅读更多精彩内容