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

前言

如今随着直播行业的火爆,直播类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~ 如有不足之处或建议还望指正,相互学习,相互进步,如果觉得不错动动小手点个喜欢, 谢谢~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容

  • 【Android 动画】 动画分类补间动画(Tween动画)帧动画(Frame 动画)属性动画(Property ...
    Rtia阅读 6,102评论 1 38
  • 本文整理自: Google 官方文档之自定义 View,笔者省略了对自己帮助不大的章节,拜读原文请点链接。 一、继...
    程序员K哥阅读 412评论 0 3
  • 1 背景 不能只分析源码呀,分析的同时也要整理归纳基础知识,刚好有人微博私信让全面说说Android的动画,所以今...
    未聞椛洺阅读 2,691评论 0 10
  • 转自:https://www.jianshu.com/p/e9d8420b1b9c 自定义View是Android...
    CQ_TYL阅读 1,829评论 0 26
  • 目录 1. 自定义View基础 1.1 分类 自定义View的实现方式有以下几种 类型定义自定义组合控件多个控件组...
    銀灬楓阅读 188,199评论 21 535