Android自定义View高级动效之---安卓流星雨动效|Android流星雨专辑封面

篇章目标要点

最近看到酷我音乐App出了一则《穹顶流星》动效,看完之后决定自己尝试一下实现,本文将围绕通过自定义View实现流星雨效果,可以看到流星雨环绕专辑图的高级动效。通过完成这项开发,能够更深的理解自定义View。

实现效果

先上图看下效果,中间设置的是外部传入的图片,四周是通过自定义View实现的流星效果
静态图1
在这里插入图片描述

静态图2
在这里插入图片描述

视频效果
在这里插入图片描述

核心设计思路

1.裁剪ImageView原图获得圆形图片

这部分的基本思路是先将原图缩放至与目标大小合适的尺寸,然后进行裁剪。
在这里插入图片描述
其关键步骤是需要设置相交取背景,这部分有较多人员以及整理了相关资料,详情如下。其中SRC表示背景图,DST表示前景图。
在这里插入图片描述

2.旋转圆形图片

在onDraw中处理图片都是基于Bitmap类型对象进行操作的,而Java提供了Matrix用法处理Bitmap旋转,在这里虽然我们需要的是处理后的圆形效果的图片,但是图片的画布并非是圆形,而是矩形,只是其背景是透明色而已,因此旋转过程中,会旋转最合适的大小来容纳原图,故处理之后的图片的宽高是变化的,如下图所示,旋转过程中会产生补充区。
在这里插入图片描述

3.绘制流星

一个静态的流星的基本构成包含流星本体是一个圆形,流星的运动轨迹是弧线两个部分构成
在这里插入图片描述

这个只是静态的流星,如果要实现动态流星的效果,则需要通过以下3个参数保证

序号参数参数要求
1圆弧终点角圆弧的终点角 = 起点角 + 扫过角度,其值应当呈现顺时针方向变化,才能呈现旋转效果
2圆弧扫过角度要实现流星拖尾和消亡两个阶段,则扫过角度应当线性增加或减少,减少至0后回收
3圆弧颜色圆弧颜色应当有渐变效果,能够体现拖尾的光减弱效果

关键代码说明

1.首先创建一个内部类实现流星对象 ,其内部定义了起点角度/扫过角度/圆弧半径/流星头部的圆心横坐标/流星头部的纵坐标 等几个主要的要素信息

    /**
     * 流星对象,主要由起点角度/扫过角度/圆弧半径/流星头部的圆心横坐标/流星头部的纵坐标 5个要素构成
     */
    private class FallingStar{
        //流星规则的渐变色
        private final int[] SWEEP_COLORS = new int[]{Color.TRANSPARENT, 0xFFE0E0E0};
        private final float[] POSITIONS = new float[]{0.2f, 0.8f};
        private int startAngle;
        private int sweepAngle;
        private int radius;
        //流星头部圆形的横纵坐标
        private int starCenterX;
        private int starCenterY;
        private SweepGradient shader;
        private RectF rect;
        //true表示流星轨迹,处于增长期;反正为衰减期
        private boolean rise;
        //流星旋转速度
        private int velocity;

        public int getStartAngle() {
            return startAngle;
        }

        public FallingStar setStartAngle(int startAngle) {
            this.startAngle = startAngle;
            return this;
        }

        public int getSweepAngle() {
            return sweepAngle;
        }

        public FallingStar setSweepAngle(int sweepAngle) {
            this.sweepAngle = sweepAngle;
            return this;
        }

        public int getRadius() {
            return radius;
        }

        public FallingStar setRadius(int radius) {
            this.radius = radius;
            return this;
        }

        public int getStarCenterX() {
            return starCenterX;
        }

        public int getStarCenterY() {
            return starCenterY;
        }

        public SweepGradient getShader() {
            return shader;
        }

        public RectF getRect() {
            return rect;
        }

        public FallingStar build(){
            //计算流星头部的圆心坐标
            double endAngle = 2 *Math.PI*(getStartAngle()+getSweepAngle()) / 360 ;
            starCenterX = (int)(radius*Math.cos(endAngle)) + width/2;
            starCenterY = (int)(radius*Math.sin(endAngle)) + height/2;
            shader = new SweepGradient(width/2, height/2, SWEEP_COLORS, POSITIONS);
            rect = new RectF(width/2 - getRadius(), height/2 - getRadius(), width/2 + getRadius(), height/2 + getRadius());
            rise = true;
            velocity = mRandomInt.nextInt(2) + 2;
            return this;
        }

        /**
         * 调整流星起点角度和扫过角度,起点角度的算法是逆时针匀速调整,扫过角度的算法是阈值及以上匀速减少至0,阈值以下匀速增加至阈值
         */
        public void changeAngle(){
            startAngle +=velocity;
            startAngle %= 360;
            if(rise){
                sweepAngle ++;
                if(sweepAngle > MAX_SWEEP_ANGLE){
                    rise = false;
                    sweepAngle = MAX_SWEEP_ANGLE;
                }
            }else{
                sweepAngle --;
                if(sweepAngle <= 0){
                    rise = true;
                    sweepAngle = 0;
                    mFallingStarList.remove(this);
                }
            }
            //相应的调整圆头的位置
            double endAngle = 2 *Math.PI*(getStartAngle()+getSweepAngle()) / 360 ;
            starCenterX = (int)(getRadius()*Math.cos(endAngle)) + width/2;
            starCenterY = (int)(getRadius()*Math.sin(endAngle)) + height/2;
        }
    }

2.然后熟悉一下单个流星的绘制,主要就是绘制一个圆形和一个弧线

    /**
    * 绘制单个流星包括绘制星星头部的圆形和绘制流星轨迹的渐变线
    * @param canvas
    * @param fallingStar
    */
   private void drawFallingStar(Canvas canvas, FallingStar fallingStar){
       //绘制流星头部圆形
       canvas.drawCircle(fallingStar.getStarCenterX(), fallingStar.getStarCenterY(), 2 , starPaint);
       //绘制轨迹线圆弧,设置弧线为渐变色
       fallingLinePaint.setShader(fallingStar.getShader());
       canvas.drawArc(fallingStar.getRect(), fallingStar.getStartAngle(), fallingStar.getSweepAngle(), false, fallingLinePaint);
   }

3.绘制流星群组和补充流星的过程

    /**
    * 绘制流星群组
    */
   private void drawFallingStarGroup(Canvas canvas){
       //剩余流星不及原来总数50%时补充流星
       if(mFallingStarList.size() <= 0){
           initFallingStarAngle();
       }else if(mFallingStarList.size() <= (FALLING_STAR_GROUP_SIZE/2)){
           addFallingStar();
       }
       //绘制流星群组
       for(int i = 0; i < mFallingStarList.size(); i++){
           drawFallingStar(canvas, mFallingStarList.get(i));
       }
   }

   /**
    * 调整流星群组的起点角度和扫过角度,起点角度的算法是逆时针匀速调整,扫过角度的算法是阈值及以上匀速减少至0,阈值以下匀速增加至阈值
    */
   private void changeFallingStarAngle(){
       for(int i = 0; i < mFallingStarList.size(); i++){
           mFallingStarList.get(i).changeAngle();
       }
       //调整中心图片的旋转角度,设置进行逆时针旋转
       mRotateAngle -= 2;
       mRotateAngle = (mRotateAngle + 360) % 360;
       invalidate();
   }

补充流星的详细代码

    /**
    * 在流星数量减少并处于衰竭状况下,补充流星个数,以确保整体的可观性
    */
   private void addFallingStar(){
       int additionSize = FALLING_STAR_GROUP_SIZE - mFallingStarList.size();
       int starPathNo = 1;
       int beginRandomAngle = mRandomInt.nextInt(360);
       //计算原有的轨道总层数,将待补充的流星分配至原各层级轨道
       int starPathCount = FALLING_STAR_GROUP_SIZE / (360 / MAX_SWEEP_ANGLE);
       if((FALLING_STAR_GROUP_SIZE % (360 / MAX_SWEEP_ANGLE)) > 0){
           starPathCount++;
       }
       //每一层待分配的流星个数
       int starCountPerPath = additionSize / starPathCount;
       for(int i = 0; i < additionSize; i++){
           starPathNo = i / starCountPerPath + 1;
           FallingStar star = new FallingStar().setRadius(mInsideImageRadius + starPathNo * 10)
                   .setStartAngle(i % (360 / MAX_SWEEP_ANGLE) * MAX_SWEEP_ANGLE + (starPathNo - 1) * 360 / PATH_COUNT_MAX + beginRandomAngle).setSweepAngle(mRandomInt.nextInt(MAX_SWEEP_ANGLE/6) + 5).build();
           mFallingStarList.add(star);
       }
   }

4.裁剪获得圆形图片的过程,首先需要对原图进行适当的缩放,然后创建画图准备绘制输出后的图片,绘制圆形与缩放后的图片相交,取相交的背景即获得了圆形的图片

    private Bitmap createRoundBitmap(Bitmap inBitmap){
       Bitmap tempBitmap;
       //判断是否需要进行缩放
       if(inBitmap.getWidth() == (2 * mInsideImageRadius) && inBitmap.getHeight() == inBitmap.getWidth()){
           tempBitmap = inBitmap;
       }else {
           tempBitmap = Bitmap.createScaledBitmap(inBitmap, 2*mInsideImageRadius, 2*mInsideImageRadius, false);
       }
       //创建待输出图片的画布
       Bitmap result = Bitmap.createBitmap(tempBitmap.getWidth(), tempBitmap.getHeight(), Bitmap.Config.ARGB_8888);
       Canvas canvas = new Canvas(result);
       //设置画布透明
       canvas.drawColor(0x00FFFFFF);
       Paint paint = new Paint();
       paint.setAntiAlias(true);
       //绘制要裁剪的圆形
       canvas.drawCircle(tempBitmap.getWidth()/2, tempBitmap.getHeight()/2, mInsideImageRadius, paint);
       //设置相交模式
       paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
       Rect rect = new Rect(0, 0, tempBitmap.getWidth(), tempBitmap.getHeight());
       canvas.drawBitmap(tempBitmap, rect, rect, paint);
       canvas.setBitmap(null);
       tempBitmap.recycle();
       return result;
   }

5.对圆形图片设置旋转和初始化流星群组

    @Override
   protected void onDraw(Canvas canvas) {
       //设置画布透明
       canvas.drawARGB(0,0,0,0);
       //绘制中间的圆形图片
       Drawable drawable = getDrawable();
       if(null == drawable){
           return;
       }
       //将ImageView的原图裁剪成圆形图片
       Bitmap bitmap = ((BitmapDrawable)drawable).getBitmap();
       Bitmap roundBitmap = createRoundBitmap(bitmap);
       //通过Matrix设置圆形Bitmap旋转
       mMatrix.reset();
       mMatrix.setRotate(mRotateAngle);
       //获取旋转后的Bitmap
       Bitmap rotateBitmap = Bitmap.createBitmap(roundBitmap, 0, 0, 2*mInsideImageRadius, 2*mInsideImageRadius, mMatrix, false);
       //在画布上绘制旋转后的Bitmap,注意基于Matrix旋转后的Bitmap与原图的大小并不相等,故计算中心位置时应以转换后的Bitmap进行计算
       canvas.drawBitmap(rotateBitmap, width / 2 - rotateBitmap.getWidth()/2 , height / 2 - rotateBitmap.getHeight()/2, null);
       //绘制流星
       drawFallingStarGroup(canvas);
       //33ms后更新流星位置
       postDelayed(new Runnable() {
           @Override
           public void run() {
               changeFallingStarAngle();
           }
       }, 33);
       //回收过程中Bitmap
       roundBitmap.recycle();
   }

6.初始化流星群组
这步骤的主要目标是使流星群组相对均匀的分布在轨道上,间距始终,避免产生混乱感

    @Override
   protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
       super.onMeasure(widthMeasureSpec, heightMeasureSpec);
       width = getMeasuredWidth();
       height = getMeasuredHeight();
       //计算流星最外轨道半径
       mOutSideStarRadius = Math.min(width, height) / 2 * 9 / 10;
       //计算中心原图的半径
       mInsideImageRadius = mOutSideStarRadius * 2 / 3;
       //计算可设置的流星雨最大个数
       PATH_COUNT_MAX = (mOutSideStarRadius - mInsideImageRadius) / PATH_INTERVAL_FALLING_STAR + 1;
       FALLING_STAR_GROUP_SIZE = PATH_COUNT_MAX * 360 / MAX_SWEEP_ANGLE;
       //初始化流星群组
       initFallingStarAngle();
   }
    /**
    * 初始化流星群组的角度和半径参数
    */
   private void initFallingStarAngle(){
       mFallingStarList.clear();
       int starPathNo = 1;
       int beginRandomAngle = mRandomInt.nextInt(360);
       for(int i = 0; i < FALLING_STAR_GROUP_SIZE; i++){
           starPathNo = i / (360 / MAX_SWEEP_ANGLE) + 1;
           FallingStar star = new FallingStar().setRadius(mInsideImageRadius + starPathNo * PATH_INTERVAL_FALLING_STAR)
                   .setStartAngle(i % (360 / MAX_SWEEP_ANGLE) * MAX_SWEEP_ANGLE + (starPathNo - 1) * 360 / PATH_COUNT_MAX + beginRandomAngle).setSweepAngle(mRandomInt.nextInt(MAX_SWEEP_ANGLE/6) + 5).build();
           mFallingStarList.add(star);
       }
   }

学习心得

以上是初步实现的流星雨动效过程,并且实现了流星雨环绕圆形专辑图封面旋转。过程效果还存在部分改进空间,后续会进一步完善。如需要源码,请提供邮箱留言

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值