高级UI具体自定义控件Android 自定义view

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

2020-02-21  本文已影响0人  Android小Y

前言

如今随着直播行业的火爆,直播类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 技术小栈,更多精彩原创
上一篇下一篇

猜你喜欢

热点阅读