美文网首页高级UI具体自定义控件Android 自定义view
『Android自定义View实战』自定义直播红包雨效果

『Android自定义View实战』自定义直播红包雨效果

作者: Android小Y | 来源:发表于2020-02-21 01:01 被阅读0次

    前言

    如今随着直播行业的火爆,直播类App数不胜数,提及直播就不得不涉及到各种交互的动效,其中挺常见的一种效果就是红包雨,当触发出该效果时,会从屏幕上方掉落很多的红包,用户通过点击掉落中的红包领取相对应的金额,本文将仿照这种交互定制成一个控件,最终效果如下:


    YFallingSurfaceView.gif

     

    实现

    思路

    要实现这个效果,有多种不同的思路可供实现,可以采用属性动画+View的形式去做,但要考虑View的复用问题,毕竟如果是1000个红包...这谁顶得住。另外也可以通过属性动画+Bitmap的方式去绘制,但由于这种场景的刷新频率太高,采用普通的View可能还是会容易遇到卡顿问题,所以最终考虑采用SurfaceView去实现这个效果。主要步骤和实现方式如下:

    1.包装红包属性对象,后续所有的动画的值都是由这些属性决定。
    2.开启SurfaceView线程,不断生成新的红包对象,直到达到最大红包数,就停止。
    3.不断刷新获取各个红包最新的属性值,包括旋转角度、位移等,并将其绘制在画布上。
    4.在手指触摸事件中判断是否点击了某个红包。

     

    1.包装红包属性对象

    由于后续的关于Bitmap的一系列变幻,都是通过角度、坐标和位移去决定的,所以先将它们包装成一个红包对象,方便后续更改和刷新:

    class FallingItem {
    
            /**
             * 起始X坐标
             */
            private int startX;
            /**
             * 线的起始Y坐标
             */
            private int startY;
            /**
             * 坠落速度
             */
            private int speed;
            /**
             * 旋转的度数
             */
            private int rotate;
    
            public int getRotate() {
                return rotate;
            }
    
            public void setRotate(int rotate) {
                this.rotate = rotate;
            }
    
            public int getSpeed() {
                return speed;
            }
    
            public FallingItem setSpeed(int speed) {
                this.speed = speed;
                return this;
            }
    
            public int getStartX() {
                return startX;
            }
    
            public void setStartX(int startX) {
                this.startX = startX;
            }
    
            public int getStartY() {
                return startY;
            }
    
            public void setStartY(int startY) {
                this.startY = startY;
            }
    }
    

    可以看到有4个属性值,x和y坐标就不用讲了,决定了红包在屏幕中的位置,rotate决定了红包旋转的角度,speed则代表红包下落的速度,也就是每次刷新,都会将其原来的Y坐标加上这个speed,作为新的Y坐标,从而实现下落的效果。

     

    2.红包的产生和停止

    上一步我们已经封装好了红包对象,因此红包的生成其实就是生成一个FallingItem类对象,在生成之前首先要判断一下当前的数量是否已经达到红包总数:

    /**
     * 掉落对象的集合
     */
    private List<FallingItem> fallingItems;
    
    private void addItem() {
            //超过红包总数,拦截
            if(curGenerateCount >= maxCount) {
                return;
            }
            FallingItem item = new FallingItem();
            fallingItems.add(item);
            curGenerateCount++;
    }
    

    生成红包对象后,需要为每一个红包对象的每一个属性进行初始化,由于要形成随机掉落的效果,所以红包的初始横坐标需要通过随机数来生成:

    private void addItem() {
            //超过红包总数,拦截
            if(curGenerateCount >= maxCount) {
                return;
            }
            FallingItem item = new FallingItem();
            int startInLeft = 0;
            if(lastStartX > bitmapWidth) {
                startInLeft = random.nextInt(lastStartX - bitmapWidth);
            }
            int startInRight = 0;
            if(lastStartX < mCanvasWidth - bitmapWidth + 1){
                startInRight = random.nextInt(mCanvasWidth - lastStartX - bitmapWidth + 1) + lastStartX;
            }
            if(startInLeft > 0 && startInRight > 0){
                item.startX = random.nextBoolean() ? startInLeft : startInRight;
            }else{
                if(startInLeft == 0){
                    item.startX = startInRight;
                }
                if(startInRight == 0){
                    item.startX = startInLeft;
                }
            }
            //int startInRight = random.nextInt(mCanvasWidth - bitmapWidth - lastStartX) + lastStartX + bitmapWidth;
            if(item.startX > mCanvasWidth - bitmapWidth){
                item.startX = mCanvasWidth - bitmapWidth;
            }
            fallingItems.add(item);
            curGenerateCount++;
    }
    
    

    首先为了尽量避免连续好多次都是同一位置掉落,因此记录了上一次的横坐标 lastStartX ,由于生成的位置有可能在上一次的左边,也有可能在右边,因此左右两边先各自生成一个随机值,最后再在这两个值中随机挑选一个。

    生成范围示意图.png

    左边的随机值:

    int startInLeft = 0;
    if(lastStartX > bitmapWidth) {
        startInLeft = random.nextInt(lastStartX - bitmapWidth);
    }
    

    也就是以0为起点,以上一个红包的左边缘偏移一个位图的位置为终点,这个范围内随机一个值。

    右边的随机值:

    int startInRight = 0;
    if(lastStartX < mCanvasWidth - bitmapWidth + 1){
        startInRight = random.nextInt(mCanvasWidth - lastStartX - bitmapWidth + 1) + lastStartX;
    }
    

    右边区域是以上一个红包的左边缘偏移一个像素为起点,画布右边缘减去一个红包宽度为终点,这个范围内随机一个值,那么就是(lastStartX, mCanvasWidth - bitmapWidth),从而可以根据random.nextInt(mCanvasWidth - lastStartX - bitmapWidth + 1) + lastStartX来获取这个范围的随机值。在计算之前判断lastStartX < mCanvasWidth - bitmapWidth + 1是因为random参数不能小于等于0

    两边的值都计算完之后,如果只有一边满足条件,则取满足的那个值,如果两边都有满足条件的值,则随机取两者中的一个:

    if(startInLeft > 0 && startInRight > 0){
         item.startX = random.nextBoolean() ? startInLeft : startInRight;
    }else{
         if(startInLeft == 0){
              item.startX = startInRight;
         }
         if(startInRight == 0){
              item.startX = startInLeft;
         }
    }
    

    得到起始横坐标之后,还有起始纵坐标、速度、角度等属性需要初始化:

    item.startY = -60;
    item.speed = (random.nextInt(3)+2)*5;
    item.rotate = random.nextInt(360);
    lastStartX = item.startX;
    

    -60是让红包从屏幕外开始,速度和角度也给了个随机值,让整个效果更为丰富。

     

    3.不断刷新获取各个红包最新的属性值,包括旋转角度、位移等,并将其绘制在画布上。

    在SurfaceView的方法里,不断循环得去生成新红包并修改其属性值,最后绘制在画布上,实现动画效果:

    @Override
    public void run() {
            Canvas canvas = null;
            FallingItem item = null;
            while (mFlag) {
                try {
                    canvas = surfaceHolder.lockCanvas();
                    if(mCanvasHeight == 0) {
                        mCanvasHeight = canvas.getHeight();
                        mCanvasWidth = canvas.getWidth();
                    }
                    //清空画布
                    canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
                } catch (Exception e) {
                    break;
                }
    
                for (int i = 0; i < fallingItems.size(); i++) {
                    item = fallingItems.get(i);
                    mMatrix.setRotate(item.rotate, (float) bitmapWidth / 2, (float) bitmapHeight / 2);
                    mMatrix.postTranslate(item.startX, item.startY);
                    canvas.drawBitmap(mBitmap, mMatrix, paint);
                    item.setStartY(item.getStartY() + item.speed);
                }
    
                 //解锁画布
                 surfaceHolder.unlockCanvasAndPost(canvas);
    
                //添加坠落对象
                addItem();
    
                if (fallingItems.size() > 50) {
                    fallingItems.remove(0);
                }
            }
    }
    

    获取集合里面存储的红包对象,通过Matrix遍历更改它们的属性值,然后调用canvas.drawBitmap将其绘制在画布上,并在原来纵坐标的基础上加上每次降落的距离(speed),从而不断降落。
     

    4.红包点击事件

    点击事件,自然是重写其onTouchEvent方法,在ACTION_DOWN事件里面去检测触摸区域是否属于红包范围:

    @Override
    public boolean onTouchEvent(MotionEvent event) {
            int action = event.getActionMasked();
            switch (action) {
                case MotionEvent.ACTION_DOWN:
                    checkInRect((int) event.getX(), (int) event.getY());
                    break;
            }
            return true;
    }
    

    红包的x、y坐标均能获取到,红包的宽高也能获取到,那么就可以得到其范围,然后将手指触摸的点的横纵坐标与每个红包的范围做对比,检测是否包含其中:

    /**
     * 是否点击在红包区域
     * @param x
     * @param y
     */
    private void checkInRect(int x, int y) {
            Log.d("Falling", "checkInRect");
            int length = fallingItems.size();
            for (int i = 0; i < length; i++) {
                FallingItem moveModel = fallingItems.get(i);
                Rect rect = new Rect((int) moveModel.startX, (int) moveModel.startY, (int) moveModel.startX + bitmapWidth, (int) moveModel.startY + bitmapHeight);
                if (rect.contains(x, y)) {
                    count++;
                    resetMoveModel(moveModel);
                    Log.d("Falling", "count: " + count);
                    break;
                }
            }
    }
    

    如果点击到了某个红包,则将其属性值重置并从红包集合中移除掉:

    private void resetMoveModel(FallingItem moveModel) {
            moveModel.startX = 0;
            moveModel.startY = -100;
            if(fallingItems.contains(moveModel)){
                fallingItems.remove(moveModel);
            }
    }
    

     

    结语

    虽然基本效果实现了,但还有一些可以优化的地方,例如红包对象缓存的管理、避免大数量时内存消耗,完整代码已上传到 一个集合酷炫效果的自定义组件库,欢迎Issue。
     

    欢迎关注 Android小Y 的简书,更多Android精选自定义View

    『Android自定义View实战』实现一个小清新的弹出式圆环菜单
    『Android自定义View实战』玩转PathMeasure之自定义支付结果动画
    『Android自定义View实战』自定义弧形旋转菜单栏——卫星菜单
    『Android自定义View实战』自定义带入场动画的弧形百分比进度条

    GitHubGitHub-ZJYWidget
    CSDN博客IT_ZJYANG
    简 书Android小Y
    GitHub 上建了一个集合炫酷自定义View的项目,里面有很多实用的自定义View源码及demo,会长期维护,欢迎Star~ 如有不足之处或建议还望指正,相互学习,相互进步,如果觉得不错动动小手点个喜欢, 谢谢~

    关注Android 技术小栈,更多精彩原创

    相关文章

      网友评论

        本文标题:『Android自定义View实战』自定义直播红包雨效果

        本文链接:https://www.haomeiwen.com/subject/wlirzctx.html