• Skip to primary navigation
  • Skip to main content
  • Skip to primary sidebar

陈文管的博客

分享有价值的内容

  • Android
  • Affiliate
  • SEO
  • 前后端
  • 网站建设
  • 自动化
  • 开发资源
  • 关于

美图手机音乐Widget动画实现

2018年11月15日发布 | 最近更新于 2023年8月28日

背景:

13年6月份毕业,刚好美图手机团队组建,从0开始几个高级工程师带着几个应届生小白开始做美图手机,从美图手机1.0到3.0版本做了近三年时间总算有点样子,跟着几位夏新出来的老前辈也学到了不少东西。个人负责手机的多媒体模块,音乐、视频、FM、录音机、下载、MediaProvider和相应Framework层的修改。

其中相对有意思的是音乐Widget,涉及到很多自定义动画的实现,包括专辑图片的转场切换、水波和自定义图形的进度条等。

一、Bitmap动画

以下给出主要的代码块逻辑,详细实现逻辑可从文末给出的GitHub链接工程中下载查看。

1、Widget 1.0 

Widget 1.0专辑图片切换动画
@RemoteView
 public class MusicWidgetRotateImageView extends View {
     
     // rotate angle array
     private int[] mSequence = {
             0, -5, -10, -9, -6, -1, 6, 15, 26, 39, 52, 63, 72, 79, 84, 87, 89,
             90
     };
     /**
      * @param context
      * @param attrs
      * @param defStyle
      */
     public MusicWidgetRotateImageView(Context context, AttributeSet attrs,
             int defStyle) {
         super(context, attrs, defStyle);
         initData();
     }
     /**
      * setAlbumArtBitmap: init rotate albumArt bitmap<br/>
      * 
      * @author wenguan.chen
      * @param pBmp Bitmap
      * @hide
      */
     @android.view.RemotableViewMethod
     public void setAlbumArtBitmap(Bitmap pBmp) {
         Log.v(TAG, "------->>setAlbumArtBitmap(),mShouldRotate:"+mShouldRotate);
         if (!mShouldRotate) {
             return;
         }
         initBmp();
         if (pBmp == null) {
             pBmp = mDefAlbumBmp;
         } else {
             Bitmap resizeBmp = Bitmap.createScaledBitmap(pBmp, mAlbumWidth,
                     mAlbumHeight, true);
             if (resizeBmp != pBmp) {
                 pBmp.recycle();
                 pBmp = resizeBmp;
             }
         }
         myHandler.removeMessages(WAIT_INIT_EVENT);
         mSequenceIndex = 0;
         mTempIndex = -1;
         if (mCurrentBmp == null || mCurrentBmp.isRecycled()) {
             mCurrentBmp = pBmp;
         }
         mMosaicBmp = mosaicBitmap(pBmp, mCurrentBmp);
         if (pBmp != mCurrentBmp) {
             if (mCurrentBmp != mDefAlbumBmp) {
                 Log.v(TAG, "------->>setAlbumArtBitmap(),recycle mCurrentBmp:");
                 mCurrentBmp.recycle();
             }
             mCurrentBmp = pBmp;
         }
         Message msg = myHandler.obtainMessage(WAIT_INIT_EVENT);
         myHandler.sendMessage(msg);
     }
     /**
      * image rotate invalidate handler
      */
     Handler myHandler = new Handler() {
         public void handleMessage(Message msg) {
             switch (msg.what) {
             case WAIT_INIT_EVENT:
                 if (mSequenceIndex < mSequence.length) {
                     if (mMosaicBmp == null || mMosaicBmp.isRecycled()) {
                         return;
                     }
                     if (mTempIndex != mSequenceIndex) {
                         mAnimBmp = rotateBmp(mMosaicBmp,
                                 mSequence[mSequenceIndex]);
                         if (mAnimBmp != null && !mAnimBmp.isRecycled()) {
                             mTempIndex = mSequenceIndex;
                             invalidate();
                         }
                     }
                     Message msg1 = myHandler.obtainMessage(WAIT_INIT_EVENT);
                     myHandler.removeMessages(WAIT_INIT_EVENT);
                     myHandler.sendMessageDelayed(msg1, 4);
                 }
             default:
                 break;
             }
         }
     };
     @Override
     protected void onDetachedFromWindow() {
         super.onDetachedFromWindow();
         Log.v(TAG, "------->>onDetachedFromWindow()");
         recycleBmp();
         myHandler.removeCallbacksAndMessages(null);
     }
     @Override
     protected void onDraw(Canvas canvas) {
         super.onDraw(canvas);
         canvas.setDrawFilter(mDrawFilter); // eliminate the aliasing effect
         if ((mSequenceIndex < mSequence.length) && mAnimBmp != null
                 && !mAnimBmp.isRecycled()) {
             canvas.drawBitmap(mAnimBmp, mStartPointX, mStartPointY, null);
             mSequenceIndex++;
         }
         if (mSequenceIndex == mSequence.length && mAnimBmp != null
                 && !mAnimBmp.isRecycled()) {
             canvas.drawBitmap(mAnimBmp, mStartPointX, mStartPointY, null);
         }
     }
     /**
      * rotateBmp: TODO<br/>
      * 
      * @author wenguan.chen
      * @param pBmp Bitmap
      * @param pRotateValue int
      * @return Bitmap
      */
     private Bitmap rotateBmp(Bitmap pBmp, int pRotateValue) {
         if (pBmp == null || pBmp.isRecycled()) {
             return null;
         }
         initData();
         Canvas canvas = new Canvas(mRotateBmp);
         canvas.setDrawFilter(mDrawFilter); // eliminate the aliasing effect
         canvas.save(Canvas.ALL_SAVE_FLAG);
         canvas.translate(0, -pBmp.getHeight() / 2);
         canvas.rotate(pRotateValue, 0, pBmp.getHeight() / 2);
         canvas.drawBitmap(pBmp, 0, 0, null);
         canvas.restore();
         mTailorBmp.eraseColor(Color.TRANSPARENT);
         canvas.setBitmap(mTailorBmp);
         canvas.save();
         canvas.drawPath(mRotatePath, mPaint);// draw triangle area
         mPaint.setXfermode(mXfermode);
         canvas.drawBitmap(mRotateBmp, 0, 0, mPaint);
         mPaint.setXfermode(null);
         canvas.restore();
         return mTailorBmp;
     }
     /**
      * mosaicBitmap: mosaic two bitmap to one for appwidget albumArt rotate<br/>
      * 
      * @author wenguan.chen
      * @param pFirstBmp Bitmap
      * @param pSecondBmp Bitmap
      * @return Bitmap
      */
     private Bitmap mosaicBitmap(Bitmap pFirstBmp, Bitmap pSecondBmp) {
         Log.v(TAG, "------->>mosaicBitmap()");
         if (pFirstBmp == null || pFirstBmp.isRecycled() || pSecondBmp == null
                 || pSecondBmp.isRecycled()) {
             return null;
         }
         initBmp();
         Canvas canvas = new Canvas(mTempBmp);
         canvas.save(Canvas.ALL_SAVE_FLAG);
         canvas.rotate(270, (float)mAlbumWidth / 2, (float)mAlbumHeight / 2);
         canvas.drawBitmap(pFirstBmp, 0, 0, null);
         canvas.restore();
         canvas.save();
         canvas.drawBitmap(pSecondBmp, 0, mAlbumHeight, null);
         canvas.restore();
         return mTempBmp;
     }
 }

Widget 1.0版本需要在固定的三角形范围内,围绕一个固定的中心点做两张专辑图片的切换动画。 

(1)PorterDuffXfermode 

Android PorterDuffXfermode 模式示例

图片的裁剪需要用到PorterDuffXfermode类, 关于PorterDuffXfermode可以参考这篇博文:Android中Canvas绘图之PorterDuffXfermode使用及工作原理详解

(2)实现原理

以上动画的实现可以理解为一张拼接起来的Bitmap和一个等边三角形区域的交集区域,围绕一个固定的原点旋转画布进行裁剪,刷新之后形成音乐专辑的切换动画。

1) 在1.0版本的实现上不足是通过Handler去刷新,Handler的消息机制在主线程中并不能保证每次执行消息的间隔是固定的,特别是在主线程卡顿的情况下,容易导致动画绘制卡顿。

在拼接专辑图片或旋转裁剪图片的时候每次都去new一个Canvas对象,这个是多余的浪费,Canvas对象可以重复利用。

2) 这边涉及到一个要点是:如果是有频繁的Bitmap绘制需求,一定不要每次去new一个Bitmap对象来创建新的Bitmap,每次都去new一个Bitmap,用完回收虽然逻辑上没问题,但非常容易因为分配的内存回收不及时或内存碎片过多导致OutOfMemory异常。

可以始终都利用一张Bitmap,或者几张Bitmap缓存之后重复利用。方法就是通过Canvas对象去绘制新传入的Bitmap,在绘制之前使用eraseColor方法

mTailorBmp.eraseColor(Color.TRANSPARENT)

使用透明的颜色去抹除Bitmap,之后再在这张清空的Bitmap上绘制新的内容。

Bitmap相关可参考这篇博文:Android Bitmap变迁与原理解析(4.x-8.x) 

3) 如果在一个类中使用到类Handler消息机制,一定要在类销毁的时候使用removeCallbacksAndMessages(null)移除消息队列中所有的消息,避免OutOfMemory异常。

2、Widget 2.0

Widget 2.0专辑图片切换动画
@RemoteView
 public class CustomScaleAnimView extends View {
    public CustomScaleAnimView(Context context, AttributeSet attrs, int defStyle) {
         super(context, attrs, defStyle);
         initData();
     }
    @Override
     protected void onDraw(Canvas canvas) {
         super.onDraw(canvas);
         canvas.setDrawFilter(mDrawFilter); // eliminate the aliasing effect

        if ((mSequenceIndex < mSequence.length) && mAnimBmp != null
                 && !mAnimBmp.isRecycled()) {
             canvas.drawBitmap(mAnimBmp, mStartPointX, mStartPointY, null);
             mSequenceIndex++;
             myHandler.removeMessages(WAIT_INIT_EVENT);
             myHandler.sendEmptyMessage(WAIT_INIT_EVENT);
         }

        if ((mSequenceIndex == mSequence.length) && mAnimBmp != null
                 && !mAnimBmp.isRecycled()) {
             canvas.drawBitmap(mAnimBmp, mStartPointX, mStartPointY, null);
         }
     }
    /**
      * initPath: TODO<br/>
      * 
      * @author wenguan.chen
      */
     private void initPath() {
         if (mAlbumPath == null) {
             mAlbumPath = new Path();
             mAlbumPath.moveTo(360f - mXOffset, 0);
             mAlbumPath.lineTo(322.5f - mXOffset, 64.95f);
             mAlbumPath.lineTo(247.5f - mXOffset, 64.95f);
             mAlbumPath.lineTo(172f - mXOffset, 194.85f);
             mAlbumPath.lineTo(209.5f - mXOffset, 259.8f);
             mAlbumPath.lineTo(134.5f - mXOffset, 389.7f);
             mAlbumPath.lineTo(285f - mXOffset, 389.7f);
             mAlbumPath.lineTo(360f - mXOffset, 519.6f);
             mAlbumPath.lineTo(435f - mXOffset, 389.7f);
             mAlbumPath.lineTo(585f - mXOffset, 389.7f);
             mAlbumPath.lineTo(510f - mXOffset, 259.8f);
             mAlbumPath.lineTo(547f - mXOffset, 194.85f);
             mAlbumPath.lineTo(472.5f - mXOffset, 64.95f);
             mAlbumPath.lineTo(397.5f - mXOffset, 64.95f);
             mAlbumPath.close();
         }
        if (mDefaultAlbumPath == null) {
             mDefaultAlbumPath = new Path();
             mDefaultAlbumPath.moveTo(247.5f - mXOffset, 64.95f);
             mDefaultAlbumPath.lineTo(172f - mXOffset, 194.85f);
             mDefaultAlbumPath.lineTo(360f - mXOffset, 519.6f);
             mDefaultAlbumPath.lineTo(547f - mXOffset, 194.85f);
             mDefaultAlbumPath.lineTo(472.5f - mXOffset, 64.95f);
             mDefaultAlbumPath.close();
         }
     }
    /**
      * initAnimPath: TODO<br/>
      * 
      * @author wenguan.chen
      */
     private void initAnimPath() {
         if (mAnimPath01 == null) {
             mAnimPath01 = new Path();
             mAnimPath01.moveTo(360f - mXOffset, 0);
             mAnimPath01.lineTo(322.5f - mXOffset, 64.95f);
             mAnimPath01.lineTo(247.5f - mXOffset, 64.95f);
             mAnimPath01.lineTo(172f - mXOffset, 194.85f);
             mAnimPath01.lineTo(209.5f - mXOffset, 259.8f);
             mAnimPath01.lineTo(172f - mXOffset, 324.75f);
             mAnimPath01.lineTo(209.5f - mXOffset, 389.7f);
             mAnimPath01.lineTo(285f - mXOffset, 389.7f);
             mAnimPath01.lineTo(360f - mXOffset, 519.6f);
             mAnimPath01.lineTo(435f - mXOffset, 389.7f);
             mAnimPath01.lineTo(585f - mXOffset, 389.7f);
             mAnimPath01.lineTo(510f - mXOffset, 259.8f);
             mAnimPath01.lineTo(547f - mXOffset, 194.85f);
             mAnimPath01.lineTo(472.5f - mXOffset, 64.95f);
             mAnimPath01.lineTo(397.5f - mXOffset, 64.95f);
             mAnimPath01.close();
             mPathMap.put(String.valueOf(0), mAnimPath01);
         }

        if (mAnimPath02 == null) {
             mAnimPath02 = new Path();
             mAnimPath02.moveTo(360f - mXOffset, 0);
             mAnimPath02.lineTo(322.5f - mXOffset, 64.95f);
             mAnimPath02.lineTo(247.5f - mXOffset, 64.95f);
             mAnimPath02.lineTo(172f - mXOffset, 194.85f);
             mAnimPath02.lineTo(209.5f - mXOffset, 259.8f);
             mAnimPath02.lineTo(172f - mXOffset, 324.75f);
             mAnimPath02.lineTo(247.5f - mXOffset, 324.75f);
             mAnimPath02.lineTo(209.5f - mXOffset, 389.7f);
             mAnimPath02.lineTo(285f - mXOffset, 389.7f);
             mAnimPath02.lineTo(360f - mXOffset, 519.6f);
             mAnimPath02.lineTo(435f - mXOffset, 389.7f);
             mAnimPath02.lineTo(585f - mXOffset, 389.7f);
             mAnimPath02.lineTo(510f - mXOffset, 259.8f);
             mAnimPath02.lineTo(547f - mXOffset, 194.85f);
             mAnimPath02.lineTo(472.5f - mXOffset, 64.95f);
             mAnimPath02.lineTo(397.5f - mXOffset, 64.95f);
             mAnimPath02.close();
             mPathMap.put(String.valueOf(1), mAnimPath02);
         }
        if (mAnimPath03 == null) {
             mAnimPath03 = new Path();
             mAnimPath03.moveTo(360f - mXOffset, 0);
             mAnimPath03.lineTo(322.5f - mXOffset, 64.95f);
             mAnimPath03.lineTo(247.5f - mXOffset, 64.95f);
             mAnimPath03.lineTo(172f - mXOffset, 194.85f);
             mAnimPath03.lineTo(360f - mXOffset, 519.6f);
             mAnimPath03.lineTo(435f - mXOffset, 389.7f);
             mAnimPath03.lineTo(585f - mXOffset, 389.7f);
             mAnimPath03.lineTo(510f - mXOffset, 259.8f);
             mAnimPath03.lineTo(547f - mXOffset, 194.85f);
             mAnimPath03.lineTo(472.5f - mXOffset, 64.95f);
             mAnimPath03.lineTo(397.5f - mXOffset, 64.95f);
             mAnimPath03.close();
             mPathMap.put(String.valueOf(2), mAnimPath03);
         }
    }
    /**
      * getTailorBmp: TODO<br/>
      * 
      * @author wenguan.chen
      * @param pBmp Bitmap
      * @return Bitmap
      */
     private Bitmap getAlbumTailorBmp(Bitmap pBmp, Path pPath) {
         Canvas canvas = new Canvas();
         mAlbumTailorBmp.eraseColor(Color.TRANSPARENT);
         canvas.setBitmap(mAlbumTailorBmp);
         canvas.save();
         canvas.drawPath(pPath, mPaint);// draw path area
         mPaint.setXfermode(mXfermode);
         canvas.drawBitmap(pBmp, 0, 0, mPaint);
         mPaint.setXfermode(null);
         canvas.restore();
         return mAlbumTailorBmp;
     }
    /**
      * getDefaultAlbumTailorBmp: TODO<br/>
      * 
      * @author wenguan.chen
      * @param pBmp Bitmap
      * @return Bitmap
      */
     private Bitmap getDefaultAlbumTailorBmp(Bitmap pBmp) {
         Bitmap resizeBmp = Bitmap.createScaledBitmap(pBmp, (int)mAlbumWidth,
                 (int)mAlbumHeight, true);
         Bitmap canvasBmp = Bitmap.createBitmap((int)mAlbumWidth,
                 (int)mAlbumHeight, Config.ARGB_8888);
         Canvas canvas = new Canvas();
         canvas.setBitmap(canvasBmp);
         canvas.save();
         canvas.drawPath(mDefaultAlbumPath, mPaint);// draw path area
         mPaint.setXfermode(mXfermode);
         canvas.drawBitmap(resizeBmp, 0, 0, mPaint);
         mPaint.setXfermode(null);
         canvas.restore();
         resizeBmp.recycle();
         return canvasBmp;
     }
    /**
      * getAnimTailorBmp: TODO<br/>
      * 
      * @author wenguan.chen
      * @param pBmp Bitmap
      * @param pPath Path
      * @return Bitmap
      */
     private Bitmap getAnimTailorBmp(Bitmap pBmp, Path pPath) {
         Canvas canvas = new Canvas();
         mAnimTailorBmp.eraseColor(Color.TRANSPARENT);
         if (pPath != null && pBmp != null && !pBmp.isRecycled()) {
             canvas.setBitmap(mAnimTailorBmp);
             canvas.save();
             canvas.drawPath(pPath, mPaint);// draw path area
             mPaint.setXfermode(mXfermode);
             canvas.drawBitmap(pBmp, 0, 0, mPaint);
             mPaint.setXfermode(null);
             canvas.restore();
         }
         mAnimMosaicBmp.eraseColor(Color.TRANSPARENT);
         canvas.setBitmap(mAnimMosaicBmp);
         canvas.save();
         canvas.drawBitmap(mAlbumBmp, 0, 0, null);
         canvas.drawBitmap(mAnimTailorBmp, 0, 0, null);
         canvas.restore();
        return mAnimMosaicBmp;
     } 

在Widget 2.0实现的效果是在一个多边形区域内,前一张专辑图片以鳞片消逝的方式来逐步显示下一张专辑图片,所以上面有很多多边形区域的Path对象初始化,目的就是为了裁剪每一步消逝的多边形区域来叠加形成动画。

3、Widget 3.0

Widget 3.0专辑图片切换动画
@RemoteView
 public class CircleAlbumSwitchView extends View {

    public CircleAlbumSwitchView(Context context, AttributeSet attrs,
             int defStyleAttr, int defStyleRes) {
         super(context, attrs, defStyleAttr, defStyleRes);
         this.mContext = context;
         init();
     }
    /**
      * setHorizonTransRatio: Shift the wave horizontally according to
      * <code>waveShiftRatio</code>.<br/>
      * 
      * @author wenguan.chen
      * @param waveShiftRatio Should be 0 ~ 1. Default to be 0. <br/>
      *            Result of waveShiftRatio multiples width of WaveView is the
      *            length to shift.
      * @hide
      */
     public void setHorizonTransRatio(float transRatio) {
         if (mHorizonTransRatio != transRatio) {
             mHorizonTransRatio = transRatio;
             invalidate();
         }
     }

    private AnimatorListener mTransAnimListener = new AnimatorListener() {

        @Override
         public void onAnimationStart(Animator arg0) {
             mIsPlayingTransAnim = true;
         }

        @Override
         public void onAnimationRepeat(Animator arg0) {}

        @Override
         public void onAnimationEnd(Animator arg0) {
             onAnimEnd();
         }

        @Override
         public void onAnimationCancel(Animator arg0) {
             onAnimEnd();
         }
     };

    private AnimatorListener mRotateAnimListener = new AnimatorListener() {

         @Override
         public void onAnimationStart(Animator arg0) {
             mIsPlayingRotateAnim = true;
         }

         @Override
         public void onAnimationRepeat(Animator arg0) {}

         @Override
         public void onAnimationEnd(Animator arg0) {
             mIsPlayingRotateAnim = false;
         }

         @Override
         public void onAnimationCancel(Animator arg0) {
             mIsPlayingRotateAnim = false;
         }
     };

    /**
      * onAnimEnd: TODO<br/>
      * 
      * @author wenguan.chen
      */
     private void onAnimEnd() {
         mIsPlayingTransAnim = false;
         mHorizonTransRatio = -1;
         clearAnimation();
         if (mCurrentBmp != mDefAlbumBmp && mCurrentBmp != mAlbumBmp) {
             mCurrentBmp.recycle();
         }
         mCurrentBmp = mAlbumBmp;
         invalidate();
         if (mIsPlaying && !mIsPlayingRotateAnim) {
             mRotateObjAnim.start();
         }
     }

     @Override
     protected void onDraw(Canvas canvas) {
         super.onDraw(canvas);
         canvas.setDrawFilter(mDrawFilter); // eliminate the aliasing effect
         if (mHorizonTransRatio > 0 && mHorizonTransRatio <= 1.0f) {// draw switch anim
             Bitmap tailorBmp = petAnimTailorBmg(mHorizonTransRatio);
             if (tailorBmp != null && !tailorBmp.isRecycled()) {
                 canvas.drawBitmap(tailorBmp, mViewWidth / 2f - mRadius,
                         mViewHeight / 2f - mRadius, null);
             }
         } else {// draw mCurrentBmp
             if (mCurrentBmp != null && !mCurrentBmp.isRecycled()) {
                 canvas.drawBitmap(mCurrentBmp, mViewWidth / 2f - mRadius,
                         mViewHeight / 2f - mRadius, null);
             }
         }
     }

    /**
      * getAnimTailorBmp: unnecessary create any new object during album bitmap
      * tailor<br/>
      * 
      * @author wenguan.chen
      * @param pRatio float
      * @return Bitmap
      */
     private Bitmap getAnimTailorBmp(float pRatio) {
         if (mAnimTempAlbum != null && !mAnimTempAlbum.isRecycled()
                 && mCurrentBmp != null && !mCurrentBmp.isRecycled()
                 && mAlbumBmp != null && !mAlbumBmp.isRecycled()
                 && mAnimTailorBmp != null && !mAnimTailorBmp.isRecycled()) {
             // draw current rotate album
             mAnimTempAlbum.eraseColor(Color.TRANSPARENT);
             mCanvas.setBitmap(mAnimTempAlbum);
             mCanvas.save();
             mCanvas.translate(mAlbumSideLength * pRatio / 13, -mAlbumSideLength
                 * pRatio / 13);
             mCanvas.rotate(360f * pRatio, mRadius, mRadius);
             mCanvas.drawBitmap(mCurrentBmp, 0, 0, null);
             mCanvas.restore();
             // draw new rotate album
             mCanvas.save();
             mCanvas.translate(-mAlbumSideLength * (1f - pRatio), 0);
             mCanvas.rotate(-360f * (1f - pRatio), mRadius, mRadius);
             mCanvas.drawBitmap(mAlbumBmp, 0, 0, null);
             mCanvas.restore();

            // tailor bitmap in center circle area
             mAnimTailorBmp.eraseColor(Color.TRANSPARENT);
             mCanvas.setBitmap(mAnimTailorBmp);
             mCanvas.save();
             mCanvas.drawCircle(mRadius, mRadius, mRadius, mPaint);
             mPaint.setXfermode(mXfermode);
             mCanvas.drawBitmap(mAnimTempAlbum, 0, 0, mPaint);
             mPaint.setXfermode(null);
             mCanvas.restore();
             return mAnimTailorBmp;
         }
         return null;
     }
 }

Widget 3.0版本实现的效果是新的专辑图片在固定圆形区域内以滚动的方式覆盖前一张专辑图片,有了之前两个版本实现的基础做起来就比较顺手,这边去掉了用Handler来触发每次刷新的方式,改为用ObjectAnimator属性动画,用水平变换和整个布局的旋转叠加形成专辑转场动画,用属性动画的好处是可以使整个绘制的流程过度更平滑。

二、水波动画

在做Widget1.0的时候并没找到好的水波动画开源代码实现,当时是用一张水波纹效果的长图去平移截取的方式形成动画,后面在Widget3.0的时候在GitHub上找到了美团开源的用属性动画实现的水波纹动画:

https://github.com/gelitenight/WaveView

相比用Bitmap截图的方式实现效果好太多。也应该是目前在Android平台上实现水波纹动画的最佳方式。

这边介绍下在Widget1.0使用长图片平移裁剪实现的水波动画,

水波效果图片1
水波效果图片2
水波裁剪范围蒙层图片

因为要实现水波下降上升的效果,需要做上下的移动,紫色和黄色图片上方都留有较大的空白。如果水波的波幅是固定的,图片可以弄成动画显示宽度两倍长度大小即可,使用这么长图片的原因是要让水波效果看起来更真实点,不是固定的波纹效果,最终合成的效果图如下:

水波效果示例

因为图片比较大,拼接和裁剪比较耗时,如果把图片的拼接和裁剪放在Java层实现,出来的效果很糟糕。而且图片的裁剪和绘制刷新都必须在主线程,不能异步处理,异步处理出来的效果更差,所以把图片的裁剪放在C层实现,效率比较高,效果勉强可接受,Java层的主要代码实现如下:

    /**
      * water anim control invalidate handler
      */
     Handler myHandler = new Handler() {
         public void handleMessage(Message msg) {
             switch (msg.what) {
             case WATER_RISE_UPDATE:
                 mIsWaterRiseUpdate = true;
                 mIsWaterDownUpdate = false;
                 mCountNum += 2;
                 mPurpleX += 4;
                 mYellowX += 3;
                 if (mCountNum % 2 == 0 && mPurpleY < 245) {
                     mPurpleY += 1;
                 }
                 if (mCountNum % 2 == 0 && mYellowY < 261) {
                     mYellowY += 1;
                 }
                 if (mCountNum >= 4096) {
                     mCountNum = 0;
                 }
                 if (mPurpleX >= 792) {
                     mPurpleX = 0;
                 }
                 if (mYellowX >= 792) {
                     mYellowX = 0;
                 }
                 invalidate();
                 myHandler.removeMessages(WATER_RISE_UPDATE);
                 myHandler.sendMessageDelayed(
                         myHandler.obtainMessage(WATER_RISE_UPDATE), 30);
                 break;
             case WATER_DOWN_UPDATE:
                 mIsWaterRiseUpdate = false;
                 mIsWaterDownUpdate = true;
                 mPurpleX += 4;
                 mPurpleY -= 8;
                 mYellowX += 3;
                 mYellowY -= 8;
                 if (mPurpleX >= 792) {
                     mPurpleX = 0;
                 }
                 if (mPurpleY <= 0 || mYellowY <= 0) {
                     mPurpleY = 0;
                     mYellowY = 0;
                     myHandler.removeMessages(WATER_RISE_UPDATE);
                     myHandler.sendMessageDelayed(
                             myHandler.obtainMessage(WATER_RISE_UPDATE), 30);
                 } else {
                     invalidate();
                     myHandler.removeMessages(WATER_DOWN_UPDATE);
                     myHandler.sendMessageDelayed(
                             myHandler.obtainMessage(WATER_DOWN_UPDATE), 30);
                 }
                 break;
             case WATER_END_ANIM:
                 mIsWaterRiseUpdate = false;
                 mIsWaterDownUpdate = false;
                 mPurpleX += 4;
                 mPurpleY -= 8;
                 mYellowX += 3;
                 mYellowY -= 8;
                 if (mPurpleX >= 792) {
                     mPurpleX = 0;
                 }
                 if (mYellowX >= 792) {
                     mYellowX = 0;
                 }
                 if (mPurpleY <= 0 || mYellowY <= 0) {
                     mPurpleY = 0;
                     mYellowY = 0;
                     mIsEndAnim = true;
                     invalidate();
                 } else {
                     invalidate();
                     myHandler.removeMessages(WATER_END_ANIM);
                     myHandler.sendMessageDelayed(
                             myHandler.obtainMessage(WATER_END_ANIM), 30);
                 }
                 break;
             default:
                 break;
             }
             super.handleMessage(msg);
         }
     };
     /**
      * tailorBmp: TODO<br/>
      * 
      * @author wenguan.chen
      * @return Bitmap
      */
     private Bitmap tailorBmp() {
         initData();
         mTailorBmp.eraseColor(Color.TRANSPARENT);
         Canvas canvas = new Canvas(mTailorBmp);
         canvas.setDrawFilter(mDrawFilter); // eliminate the aliasing effect
         canvas.save();
         canvas.drawPath(mClipPath, mPaint);// draw triangle area
         mPaint.setXfermode(mXfermode);
         canvas.drawBitmap(applyEffect(mPurpleX, mPurpleY, mYellowX, mYellowY),
                 0, 0, mPaint);
         mPaint.setXfermode(null);
         canvas.restore();
         return mTailorBmp;
     }
     /**
      * applyEffect: TODO<br/>
      * 
      * @author wenguan.chen
      * @param pX1 int
      * @param pY1 int
      * @param pX2 int
      * @param pY2 int
      * @return Bitmap
      */
     private Bitmap applyEffect(int pX1, int pY1, int pX2, int pY2) {
         nativeWaterWave(mPurpleBmp, mPurpleBmpWidth, mPurpleBmpHeight,
                 mYellowBmp, mPurpleBmpWidth, mPurpleBmpHeight, mMaskBmp,
                 mAnimBmp, pX1, pY1, pX2, pY2, mMaskWidth, mMaskHeight);
         return mAnimBmp;
     }

C++层完整实现代码如下:

 #include "meitu_waterWave.h"
 #include <jni.h>
 #include <GraphicsJNI.h>
 #include <JNIHelp.h>
 #include <android_runtime/AndroidRuntime.h>

namespace android {

int Bitmap_lockPixels(JNIEnv* env, jobject jbitmap, void** addrPtr) {
     if (NULL == env || NULL == jbitmap) {
         return ANDROID_BITMAP_RESULT_BAD_PARAMETER;
     }

    SkBitmap* bm = GraphicsJNI::getNativeBitmap(env, jbitmap);
     if (NULL == bm) {
         return ANDROID_BITMAP_RESULT_JNI_EXCEPTION;
     }

    bm->lockPixels();
     void* addr = bm->getPixels();
     if (NULL == addr) {
         bm->unlockPixels();
         return ANDROID_BITMAP_RESULT_ALLOCATION_FAILED;
     }

    if (addrPtr) {
         *addrPtr = addr;
     }
     return ANDROID_BITMAP_RESUT_SUCCESS;
 }

int Bitmap_unlockPixels(JNIEnv* env, jobject jbitmap) {
     if (NULL == env || NULL == jbitmap) {
         return ANDROID_BITMAP_RESULT_BAD_PARAMETER;
     }

    SkBitmap* bm = GraphicsJNI::getNativeBitmap(env, jbitmap);
     if (NULL == bm) {
         return ANDROID_BITMAP_RESULT_JNI_EXCEPTION;
     }

    // notifyPixelsChanged() needs be called to apply writes to GL-backed
     // bitmaps.  Note that this will slow down read-only accesses to the
     // bitmaps, but the NDK methods are primarily intended to be used for
     // writes.
     bm->notifyPixelsChanged();

    bm->unlockPixels();
     return ANDROID_BITMAP_RESUT_SUCCESS;
 }

static void Java_com_meitu_mobile_view_musicwidget01_WaterAnimView_nativeWaterWave(JNIEnv * env, jobject obj,
 jobject srcBitmap, jint srcW, jint srcH,
 jobject srcBitmap2, jint srcW2, jint srcH2,
 jobject maskBitmap,
 jobject desBitmap, jint x, jint y, jint x2, jint y2,
 jint width, jint height)
 {

 char* source = 0;
 char* source2 = 0;
 char* destination = 0;
 char* mask = 0;
 Bitmap_lockPixels(env, srcBitmap, (void**) &source);
 Bitmap_lockPixels(env, srcBitmap2, (void**) &source2);
 Bitmap_lockPixels(env, desBitmap, (void**) &destination);
 Bitmap_lockPixels(env, maskBitmap, (void**) &mask);
 unsigned char * srcRGB = (unsigned char * )source;
 unsigned char * srcRGB2 = (unsigned char * )source2;
 unsigned char * desRGB = (unsigned char * )destination;
 unsigned char * maskRGB = (unsigned char * )mask;
 int i, n, index1, index2;
 int collenth = width * 4;
 int offset = ( y * srcW + x ) * 4;
 int offset2 = ( y2 * srcW2 + x2 ) * 4;
 for (n = 0; n < height; n++) {
     for( i = 0; i < collenth; i++ ) {
         index1 = i + offset + n * srcW * 4;
         index2 = i + offset2 + n * srcW2 * 4;
         if( i % 4 != 3 ) {
             destination[i + collenth * n] = (source[index1] * source[index1 + (3 - index1 % 4) ] + source2[index2] * (255 - source[index1 + (3 - index1 % 4) ]))/255;
         } else {
             destination[i + collenth * n] = MAX(source[i + offset + n * srcW * 4], source2[i + offset2 + n * srcW2 * 4] ) * maskRGB[i + collenth * n] /255;
             if(destination[i + collenth * n] == 0) {
                 destination[i + collenth * n - 1] = 0;
                 destination[i + collenth * n - 2] = 0;
                 destination[i + collenth * n - 3] = 0;
             }
         }
    }
 }
 Bitmap_unlockPixels(env, maskBitmap);
 Bitmap_unlockPixels(env, desBitmap);
 Bitmap_unlockPixels(env, srcBitmap);
 Bitmap_unlockPixels(env, srcBitmap2);
 }

static JNINativeMethod gMethods[] = {
     {"nativeWaterWave", "(Landroid/graphics/Bitmap;IILandroid/graphics/Bitmap;IILandroid/graphics/Bitmap;Landroid/graphics/Bitmap;IIIIII)V", (void*)Java_com_meitu_mobile_view_musicwidget01_WaterAnimView_nativeWaterWave },
 };

int register_com_meitu_mobile_view_musicwidget01_WaterAnimView(JNIEnv *env)
 {
 return AndroidRuntime::registerNativeMethods(env, "com/meitu/mobile/view/musicwidget01/WaterAnimView",
             gMethods, NELEM(gMethods));
 }
 };

三、Widget其他动画

整个音乐Widget动画实现不止这些,每一个版本完整Widget的实现包括了多种动画布局的叠加,形成最终的效果。其他的动画实现类详细如下:

音乐Widget各个版本实现类

以上类所在文件夹对应每个Widget的版本

自定义进度条类:MusicWidgetProgressbar、MusicWidgetProgressbar、CustomWidgetProgressbar 

自定义进度条过度动画:CatearPrgAnimView 

文本转场切换动画:CustomWidgetViewFlipper、MusicWidgetViewFlipper 

水波动画:WaterAnimView、WaveView

专辑图片切换动画:MusicWidgetRotateImageView、CustomScaleAnimView、CircleAlbumSwitchView 

萤火虫动画:CustomFireflyView 

流星动画:CustomShootingStarView 

菱镜闪烁动画:CustomPrismFlickerView 

GitHub源码地址:

https://github.com/wenguan0927/MusicWidgetAnim

四、动画相关参考资料

自定义view实现水波纹效果

一个绚丽的loading动效分析与实现!

NineOldAndroids

美团开源水波动画WaveView

转载请注明出处:陈文管的博客 – 美图手机音乐Widget动画实现

扫码或搜索:文呓

博客公众号

微信公众号 扫一扫关注

文章目录

  • 一、Bitmap动画
    • 1、Widget 1.0 
    • (1)PorterDuffXfermode 
    • (2)实现原理
    • 2、Widget 2.0
    • 3、Widget 3.0
  • 二、水波动画
  • 三、Widget其他动画
  • 四、动画相关参考资料
博客公众号

闽ICP备18001825号-1 · Copyright © 2025 · Powered by chenwenguan.com