Android 自定义 View 新年烟花、横幅动画

3,630 阅读8分钟

新年了,项目中要作个动画,如下效果图:


整体要求实现:彩带乱飞,烟花冲天而起,烟花缩放,小鸡换图,小鸡飘移,横幅裁剪、展开等动画效果,

全局大量使用了属性动画来实现。

我在实现过程中,横幅的裁剪计算,捣腾了比较久的时间,初版采用属性动画计算float的一个比率值,来配合每一帧的裁剪绘制,如下代码:

  1. private static class RollView extends View {  
  2.        private Bitmap mBitmap;  
  3.        private Rect mSrc;  
  4.        private Rect mDst;  
  5.        private int mRollWidth = 60;  
  6.        private float mRate;  
  7.        private boolean mIsStopAnim;  
  8.   
  9.        public RollView(Context context) {  
  10.            super(context);  
  11.            mSrc = new Rect();  
  12.            mDst = new Rect();  
  13.        }  
  14.        @Override  
  15.        protected void onMeasure(int widthMeasureSpec,  int heightMeasureSpec) {  
  16.            super.onMeasure(widthMeasureSpec, heightMeasureSpec);  
  17.        }  
  18.   
  19.        @Override  
  20.        protected void onDraw(Canvas canvas) {  
  21.            if (mBitmap == nullreturn;  
  22.   
  23.           drawFromMiddleByFloatCompute(canvas);  
  24.   
  25.        }  
  26.   
  27.        private void drawFromMiddleByFloatCompute(Canvas canvas) {  
  28.            /* 
  29.            以下src 都需要加上mBitmap. 的前缀,, 因从drawable拿到的是原始图片宽高 
  30.            而适配时,可能view的宽高比 drawable的宽高还小或大 
  31.             */  
  32.            final float rate = mRate;  
  33.   
  34.            mSrc.left = 0;  
  35.            mSrc.top = 0;  
  36.            mSrc.right = mRollWidth;  
  37.            mSrc.bottom = mBitmap.getHeight();  
  38.   
  39.            mDst.left = (int) ((getWidth() / 2 - mRollWidth) - (getWidth() /  2 - mRollWidth) * rate);  
  40.            mDst.top = 0;  
  41.            mDst.right = mDst.left + mRollWidth + 1;//因精度问题,这里强制+1  
  42.            mDst.bottom = getHeight();  
  43.            canvas.drawBitmap(mBitmap, mSrc, mDst, null);  
  44.   
  45.            //中间  
  46.            int sw = (int) ((mBitmap.getWidth() - mRollWidth *  2) * rate);  
  47.            mSrc.left = mBitmap.getWidth() / 2 - sw / 2;  
  48.            mSrc.top = 0;  
  49.            mSrc.right = mSrc.left + sw;  
  50.            mSrc.bottom = mBitmap.getHeight();  
  51.   
  52.            int dw = (int) ((getWidth() - mRollWidth *  2) * rate);  
  53.            mDst.left = getWidth() / 2 - dw / 2;  
  54.            mDst.top = 0;  
  55.            mDst.right = mDst.left + dw;  
  56.            mDst.bottom = getHeight();  
  57.            canvas.drawBitmap(mBitmap, mSrc, mDst, null);  
  58.   
  59.            //右边  
  60.            mSrc.left = mBitmap.getWidth() - mRollWidth;  
  61.            mSrc.top = 0;  
  62.            mSrc.right = mBitmap.getWidth();  
  63.            mSrc.bottom = mBitmap.getHeight();  
  64.   
  65.            mDst.left = (int) (getWidth() / 2 + (getWidth() /  2 - mRollWidth) * rate);  
  66.            mDst.top = 0;  
  67.            mDst.right = mDst.left + mRollWidth;  
  68.            mDst.bottom = getHeight();  
  69.   
  70.            canvas.drawBitmap(mBitmap, mSrc, mDst, null);  
  71.        }  
  72.   
  73.        public void setRes(int resId) {  
  74.            mBitmap = getBitmapFromLocal(resId);  
  75.        }  
  76.   
  77.        @RequiresApi(api = Build.VERSION_CODES.HONEYCOMB)  
  78.        public void startFloatComputeAnim() {  
  79.            /* 
  80.            如果有float获取比率值,从而计算出相应的坐标值,那么可能由于最终在转成Rect的坐标时, 
  81.            float to int ,有精度的损失:1个px    而引起效果的不理想 
  82.             */  
  83.            ValueAnimator animator = ValueAnimator.ofFloat(01);  
  84.            animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {  
  85.                @Override  
  86.                public void onAnimationUpdate(ValueAnimator animation) {  
  87.                    if (mIsStopAnim) {  
  88.                        animation.cancel();  
  89.                        return;  
  90.                    }  
  91.                    mRate = (float) animation.getAnimatedValue();  
  92.                    invalidate();  
  93.   
  94.                }  
  95.            });  
  96.            animator.setDuration(2000);  
  97.            animator.start();  
  98.        }  
  99.   
  100.        public void stopAnim() {  
  101.            mIsStopAnim = true;  
  102.        }  
  103.    }  
 private static class RollView extends View {
        private Bitmap mBitmap;
        private Rect mSrc;
        private Rect mDst;
        private int mRollWidth = 60;
        private float mRate;
        private boolean mIsStopAnim;

        public RollView(Context context) {
            super(context);
            mSrc = new Rect();
            mDst = new Rect();
        }
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        }

        @Override
        protected void onDraw(Canvas canvas) {
            if (mBitmap == null) return;

           drawFromMiddleByFloatCompute(canvas);

        }

        private void drawFromMiddleByFloatCompute(Canvas canvas) {
            /*
            以下src 都需要加上mBitmap. 的前缀,, 因从drawable拿到的是原始图片宽高
            而适配时,可能view的宽高比 drawable的宽高还小或大
             */
            final float rate = mRate;

            mSrc.left = 0;
            mSrc.top = 0;
            mSrc.right = mRollWidth;
            mSrc.bottom = mBitmap.getHeight();

            mDst.left = (int) ((getWidth() / 2 - mRollWidth) - (getWidth() / 2 - mRollWidth) * rate);
            mDst.top = 0;
            mDst.right = mDst.left + mRollWidth + 1;//因精度问题,这里强制+1
            mDst.bottom = getHeight();
            canvas.drawBitmap(mBitmap, mSrc, mDst, null);

            //中间
            int sw = (int) ((mBitmap.getWidth() - mRollWidth * 2) * rate);
            mSrc.left = mBitmap.getWidth() / 2 - sw / 2;
            mSrc.top = 0;
            mSrc.right = mSrc.left + sw;
            mSrc.bottom = mBitmap.getHeight();

            int dw = (int) ((getWidth() - mRollWidth * 2) * rate);
            mDst.left = getWidth() / 2 - dw / 2;
            mDst.top = 0;
            mDst.right = mDst.left + dw;
            mDst.bottom = getHeight();
            canvas.drawBitmap(mBitmap, mSrc, mDst, null);

            //右边
            mSrc.left = mBitmap.getWidth() - mRollWidth;
            mSrc.top = 0;
            mSrc.right = mBitmap.getWidth();
            mSrc.bottom = mBitmap.getHeight();

            mDst.left = (int) (getWidth() / 2 + (getWidth() / 2 - mRollWidth) * rate);
            mDst.top = 0;
            mDst.right = mDst.left + mRollWidth;
            mDst.bottom = getHeight();

            canvas.drawBitmap(mBitmap, mSrc, mDst, null);
        }

        public void setRes(int resId) {
            mBitmap = getBitmapFromLocal(resId);
        }

        @RequiresApi(api = Build.VERSION_CODES.HONEYCOMB)
        public void startFloatComputeAnim() {
            /*
            如果有float获取比率值,从而计算出相应的坐标值,那么可能由于最终在转成Rect的坐标时,
            float to int ,有精度的损失:1个px    而引起效果的不理想
             */
            ValueAnimator animator = ValueAnimator.ofFloat(0, 1);
            animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    if (mIsStopAnim) {
                        animation.cancel();
                        return;
                    }
                    mRate = (float) animation.getAnimatedValue();
                    invalidate();

                }
            });
            animator.setDuration(2000);
            animator.start();
        }

        public void stopAnim() {
            mIsStopAnim = true;
        }
    }


因float转int有一个精度损失的问题,所以在计算中强制加上了1px(代码中有);

这样虽然解决了有1px没有绘制的问题,但是会发生绘制时不够平滑,而出现抖动的情形(在某些devices上)

所以最好还是不要使用float来计算

> 后来,同事猜想使用一个固定int值 来参与计算,可能可以解决上述问题:

比如每秒30帧,这里动画时长2秒,即共30*2=60帧;

图片宽度、左画轴、右画轴  对  60帧数 做相应的除法及其他计算,可得出一个单帧中 它们应该运动的x距离

> 之后,我又想了一种,使用一个属性动画,来计算出从0到getWidth()之间的 动画值,

从而通过计算,使得横幅从左向右拉开, 如下:


代码就不整体开源了···