前言
如今随着直播行业的火爆,直播类App数不胜数,提及直播就不得不涉及到各种交互的动效,其中挺常见的一种效果就是红包雨,当触发出该效果时,会从屏幕上方掉落很多的红包,用户通过点击掉落中的红包领取相对应的金额,本文将仿照这种交互定制成一个控件,最终效果如下:
实现
思路
要实现这个效果,有多种不同的思路可供实现,可以采用属性动画+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
,由于生成的位置有可能在上一次的左边,也有可能在右边,因此左右两边先各自生成一个随机值,最后再在这两个值中随机挑选一个。
左边的随机值:
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实战』自定义带入场动画的弧形百分比进度条
GitHub:GitHub-ZJYWidget
CSDN博客:IT_ZJYANG
简 书:Android小Y
在 GitHub 上建了一个集合炫酷自定义View的项目,里面有很多实用的自定义View源码及demo,会长期维护,欢迎Star~ 如有不足之处或建议还望指正,相互学习,相互进步,如果觉得不错动动小手点个喜欢, 谢谢~