【自定义控件】仿和平精英组队交流标点系统

本文作者受和平精英组队交流标点系统启发,尝试自行实现该功能。介绍了其原理,涉及三角函数等数学知识,以及红点移动、显示范围等逻辑。详细阐述了黑色按钮和中间圆盘的实现步骤,包括外圆、内圆、图标文字等绘制,还提及了不足与源码链接。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、前言

最近沉迷和平精英这款游戏,尽管从最开始的刺激战场转过来的时候,各种不适应,还挥手告别,但是玩了一段时候之后,嗯,真香!!!
不记得组队交流标点系统是什么时候上线的了,但是玩游戏的时候,发现在这个功能实在是太好用了,先来看下官方的宣传图:
和平精英组队交流标点系统
图片来源:https://gp.qq.com/main.shtml?ADTAG=media.innerenter.gamecom.navigation
身为一名程序猿,看见一个特别好用的控件时候,不免深思,这个功能是怎么实现的?能不能自己撸一个?实践是检验真理的唯一标准,光说不做假把式,打开AS,撸它一个天荒地老,撸到知其然并知其所以然。

二、原理

话不多说,先看下撸完之后的动态效果图和静待效果图:
仿组队交流标点系统动态图
仿组队交流标点系统静态图
看上去就是一个点击按钮(即图上右上角黑色按钮)和一个中间圆盘组成的自定义控件,但是这里涉及到三角函数、直线斜率、直线与圆交点的坐标、两点之间距离等等数学问题,最主要的是圆盘之中红色按钮要跟随手指点击黑色按钮移动而移动,这其中还涉及到红色按钮的显示范围及超过范围的显示方式,并且红色按钮所在扇形的变换形式等,原理讲的差不多了,接下来一步一步来打造一个组队交流标点系统。

三、实现

3.1 黑色按钮、中间圆盘

先确定中间圆盘的半径及圆心坐标和黑色按钮的圆心坐标。

@Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        centerX = w / 2;
        centerY = h / 2;
        radius = Math.min(w, h) / 2.0f;
        btnCenterX = w / 4 * 3;
        btnCenterY = h / 8;
        super.onSizeChanged(w, h, oldw, oldh);
    }

3.1.1 黑色按钮

黑色按钮是Bitmap,因此先要确定矩形,把Bitmap画在矩形内即可。

/**
     * 黑色按钮
     *
     * @param canvas
     */
    private void drawBtn(Canvas canvas) {
        float btnLeftX = btnCenterX - btnRadius;
        float btnRightX = btnCenterX + btnRadius;
        float btnTopY = btnCenterY - btnRadius;
        float btnBottomY = btnCenterY + btnRadius;
        RectF rectF = new RectF(btnLeftX, btnTopY, btnRightX, btnBottomY);
        Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.ic_btn);
        canvas.drawBitmap(bitmap, null, rectF, null);
    }

3.1.2 中间圆盘

中间黑色圆盘由以下几部分组成:外圆、内圆、分割线,扇形内的图标和文字、跟随手指移动的红色按钮。

外圆及分割线

外圆的圆心坐标和半径已经计算出来,画一个圆就简单多了:

canvas.drawCircle(centerX, centerY, radius, outpaint);

分割线通过Path实现,将外圆黑色圆盘平均分成8份,只需要计算出每个扇形的角度即可,注:Math的cos和sin函数,参数都是弧度,而角度转换成弧度的计算方法,Android已经提供了,就是Math.toRadians()函数。

 /**
     * 把外圆平均分成8份
     *
     * @param canvas
     */
    private void drawLines(Canvas canvas) {
        for (int i = 0; i < ARC_NUMBER; i++) {
            linePath.moveTo(centerX, centerY);
            linePath.lineTo((float) Math.cos(Math.toRadians(i * getArcAngle())) * radius + centerX,
                    (float) Math.sin(Math.toRadians(i * getArcAngle())) * radius + centerY);
        }
        canvas.drawPath(linePath, linepaint);
    }

先来看下效果:
圆平均分为8份

画内圆

这里需要注意一下,画内圆涉及到PorterDuffXfermode模式,有人可能会说,看上面的静态图效果,直接把画笔设置成白色不就可以了,先别急,对比看一下:

  • 改变画笔颜色
//为了方便查看效果,给Activity添加一个背景效果
android:background="@color/colorPrimary"
//外圆
canvas.drawCircle(centerX, centerY, radius, outpaint);
//内圆
outpaint.setColor(Color.WHITE); //改变画笔颜色
canvas.drawCircle(centerX, centerY, radius / 2, outpaint);

改变画笔颜色

  • 添加PorterDuffXfermode模式
//为了方便查看效果,给Activity添加一个背景效果
android:background="@color/colorPrimary"
canvas.drawCircle(centerX, centerY, radius, outpaint);
outpaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_OUT));
canvas.drawCircle(centerX, centerY, radius / 2, outpaint);
outpaint.setXfermode(null);

添加PorterDuffXfermode模式
对比很明显,这里就不再赘述,关于PorterDuffXfermode模式的理解,请大家自行了解。

画图标和文字

根据组队交流标点系统静态图可以看出,无论图标还是文字都是和扇形的角度相同,扇形的绘制以**-90°**开始,顺时针方向绘制。
先计算每个图标显示在扇形区域的中心点坐标,很明显每个中心点的角度不同给,因此通过如下方法计算出每个中心点的坐标:

for (int i = 1; i <= 15; i += 2) {
            float bitmapCenterX =
                    (float) (centerX + Math.cos(Math.toRadians((getArcAngle() / 2) * i - 90)) * (radius / 4) * 3);
            float bitmapCenterY =
                    (float) (centerY + Math.sin(Math.toRadians((getArcAngle() / 2) * i - 90)) * (radius / 4 * 3));
}

求出中心点之后,确定图标显示的位置:

RectF rectF = new RectF(
                    bitmapCenterX - imgWidth / 2,
                    bitmapCenterY - imgWidth / 2 + (imgWidth / 4),
                    bitmapCenterX + imgWidth / 2,
                    bitmapCenterY + imgWidth / 2 + (imgWidth / 4)

计算每个图标和文字的旋转角度:

 Matrix matrix = new Matrix();
 matrix.setRotate((getArcAngle() / 2) * i, bitmapCenterX, bitmapCenterY);
 canvas.setMatrix(matrix);

最后把图标和文字画出来:

canvas.drawBitmap(bitmap, null, rectF, null);
Paint.FontMetricsInt fontMetricsInt = textpaint.getFontMetricsInt();
//文字居中显示
float baseLine =(rectF.bottom + rectF.top - fontMetricsInt.bottom - fontMetricsInt.top) / 2;
 canvas.drawText(text,rectF.centerX(), baseLine - radius / 5,textpaint);

效果图:
中间圆盘效果图

红点

红色按钮即为跟随手指移动而移动的红点,当与外圈扇形重合时出发组队交流标点系统的语音提示,本文并未实现语音效果,实在找不着小姐姐录制语音,哎!

 enum BTN_TYPE {
        BTN_DOWN, BTN_MOVE, BTN_UP //手指按下,手指移动,手指抬起
    }
  1. 监听黑色按钮的点击和移动事件
 @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                x = event.getX();
                y = event.getY();
                btn_down_x = x;
                btn_down_y = y;
                if (onDownBtn(x, y)) {
                    btn_type = BTN_TYPE.BTN_DOWN;
                    isDrawOnInCircle = true;
                    postInvalidate();
                }
                return true;
            case MotionEvent.ACTION_MOVE:
                x = event.getX();
                y = event.getY();
                btnMoveRadius =
                        Math.sqrt((btn_down_x - x) * (btn_down_x - x) + (btn_down_y - y) * (btn_down_y - y));
                if (btnMoveRadius <= (radius / 2)) {
                    btn_type = BTN_TYPE.BTN_MOVE;
                    isDrawOnInCircle = true;
                    postInvalidate();
                }
                break;
        }
        return super.onTouchEvent(event);
    }

当手指点击在黑色按钮时候,记录手指点击的X,Y坐标,当手指移动的时候,记录手指移动的坐标同样是X,Y,并且不断重绘。
btn_down_x = x,btn_down_y = y 记录手指点击的坐标,Math.sqrt((btn_down_x - x) * (btn_down_x - x) + (btn_down_y - y) * (btn_down_y - y)); 获取手指移动的终点和手指点击位置的直线距离。

  1. 手指按下的瞬间重绘
canvas.drawCircle(centerX, centerY, btnRadius, btnpaint);

因为手指按下的瞬间一定是在中间圆盘的中心显示红点,很好理解,不需要过多讲解。

  1. 红点跟随手指移动而移动(圆内,此圆指内圆,即中间空白区域)
//点击按钮移动的点到按下的点X Y轴的坐标差
float moveX = x - btn_down_x;
float moveY = y - btn_down_y;

//内圆圆心和点击按钮的坐标比例
float bX = centerX / btnCenterY;
float bY = centerY / btnCenterY;

//中心按钮相对与点击按钮的圆心坐标
float mX = centerX + moveX * bX;
float mY = centerY + moveY * bY;

//中心按钮移动的圆心坐标到内圆圆心坐标的距离
centerBtnMoveRadius =Math.sqrt((mX - centerX) * (mX - centerX) + (mY - centerY) * (mY - centerY));

先计算黑色按钮,手指按下和手指移动终点的X,Y坐标差,求出圆盘圆心坐标和黑色按钮坐标比例,根据黑色按钮移动的距离求出红点跟随手指移动后的终点坐标,最后求出红点的终点坐标与圆盘圆心的距离即centerBtnMoveRadius
centerBtnMoveRadius 与内圆半径进行比较,会有以下两种情况:

1、在内圆内,即 centerBtnMoveRadius < radius / 2

在圆内比较简单,直接用求出来的mX、mY 圆心坐标,不断画圆即可:

canvas.drawCircle(mX, mY, btnRadius, btnpaint);
2、在内圆外,即 centerBtnMoveRadius >= radius / 2

先计算红点移动的终点与圆心形成的直线与X轴形成的角度大小:

if ((mX > centerX && mY > centerY) || (mX < centerX && mY > centerY)) {
        radians = Math.atan2(mY - centerY, mX - centerX);
        degrees = radians * (180 / Math.PI);
}
if ((mX > centerX && mY < centerY) || (mX < centerX && mY < centerY)) {
       radians = Math.atan2(centerY - mY, mX - centerX);
       degrees = radians * (180 / Math.PI);
}

再计算红点移动的终点与圆心形成的直线和内圆交点的坐标,会有两个交点:
参考:https://blog.youkuaiyun.com/sinat_25911307/article/details/86598780

/**
     * 计算直线与圆相交的两点坐标
     *
     * @param x1      起点直线的X轴坐标
     * @param y1      起点直线的轴坐标
     * @param x2      终点直线的X轴坐标
     * @param y2      终点直线的Y轴坐标
     * @param r       圆半径
     * @param centerx 圆X轴坐标
     * @param centery 圆y轴坐标
     */
    private void measurePonit(float x1, float y1, float x2, float y2, float r, float centerx,
                              float centery) {
        //直线斜率不存在,垂直与X轴
        if ((x1 == x2) && (y1 != y2)) {
            if (Math.abs(centerx - x1) < r) {
                double y = Math.sqrt(r * r - ((x1 - centerx) * (x1 - centerx)));
                j_x_1 = x1;
                j_y_1 = (float) (centery + y);
                j_x_2 = x1;
                j_y_2 = (float) (centery - y);
            }
        }
        //两点重合
        else if ((x1 == x2) && (y1 == y2)) {
            j_x_1 = x1;
            j_y_1 = y1;
            j_x_2 = x2;
            j_y_2 = y2;
        }
        //直线斜率为0,平行于X轴
        else if ((y1 == y2) && (x1 != x2)) {
            double area = Math.abs(centery - y1);
            if (area <= r) {
                double x = Math.sqrt(r * r - ((y1 - centery) * (y1 - centery)));
                j_x_1 = (float) (centerx + x);
                j_y_1 = y1;
                j_x_2 = (float) (centerx - x);
                j_y_2 = y1;
            }
        } else {
            double k = (y2 - y1) / (x2 - x1);
            double b = y2 - k * (x2);
            double del =
                    4 * Math.pow((k * b - centerx - k * (centery)), 2) - 4 * (1 + k * k) *
                            (Math.pow((centerx), 2) + Math.pow((b - centery), 2) - r * r);
            if (del > 0) {
                double tmp = 2 * (k * b - centerx - k * centery);
                j_x_1 = (float) ((-tmp + Math.sqrt(del)) / (2 * (1 + k * k)));
                j_y_1 = (float) (k * (j_x_1) + b);
                j_x_2 = (float) ((-tmp - Math.sqrt(del)) / (2 * (1 + k * k)));
                j_y_2 = (float) (k * (j_x_2) + b);
            }
        }
    }

通过计算之后会得到两个点的坐标为A( j_x_1,j_y_1)B( j_x_2,j_y_2),其中用到的是直线斜率的知识点,不过关于到底是取A点还是B点的问题,暂时未完全明白,之后再来思考这个问题。
把圆平均分为四等份,如下所示:
四等份圆
上面已经求出直线与X轴形成的角度大小,切记只有直线在上图所示的1、3区域内,形成的角度才会大等于90°,小于等于180°,这个时候交点坐标取B点,反之交点坐标取A点。

 if (degrees > 90 && degrees < 180) {
	 //Y轴左
	canvas.drawCircle(j_x_2, j_y_2, btnRadius, btnpaint);
}else{
	 //Y轴右
     canvas.drawCircle(j_x_1, j_y_1, btnRadius, btnpaint);
}

到这里红点跟随手指移动而移动,并且手指怎么移动都不会超过内圆的范围已经实现了,不过还差最后一步,就是红点与扇形重合的时候,需要重新绘制扇形。

绘制扇形

如上图所示,把圆平均分为1、2、3、4个部分,每个部分的计算方式都一样,只不过是角度不同而已,因此这里只讲2部分的实现方式,其他部分依次类推。
根据组队交流标点系统静态图可以看出,2部分有两个扇形,先求出2部分中心点在圆周上的坐标,然后和A点坐标进行比较,注:A活B点根据部分而定,计算角度,当红点与扇形重合时,重新绘制扇形区域。

cX = (float) (centerX + Math.cos(Math.toRadians(-getArcAngle())) * (radius / 2));
         if (j_x_1 < cX) {
                arcpaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_ATOP));
                canvas.drawArc(arcRectf, -90, getArcAngle(), true, arcpaint);
        } else {
                 arcpaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_ATOP));
                 canvas.drawArc(arcRectf, -getArcAngle(), getArcAngle(), true, arcpaint);
}

剩余1、3、4部分分别根据此方法一一实现即可,这里就不再赘述,到此仿和平精英组队交流标点系统便撸完了。

四、不足

手指移动时候,红点有时会卡住,并且关于直线斜率和直线和圆交点坐标还不是很了解,关于卡顿问题,有时间会好好研究研究,如果您发现任何问题或者建议,欢迎留言。

五、源码

部分代码:

/**
     * 利用混合模式,画两个圆,形成圆环
     *
     * @param canvas
     */
    private void drawCircleView(Canvas canvas) {
    	//重绘扇形时候需要
        arcRectf.left = centerX - radius;
        arcRectf.top = centerY - radius;
        arcRectf.right = centerX + radius;
        arcRectf.bottom = centerY + radius;
        int layerId = canvas.saveLayer(0, 0, getWidth(), getHeight(), null, Canvas.ALL_SAVE_FLAG);
        canvas.drawCircle(centerX, centerY, radius, outpaint);
        drawBitmapAndText(canvas);
        outpaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_OUT));
        drawLines(canvas);
        canvas.drawCircle(centerX, centerY, radius / 2, outpaint);
        outpaint.setXfermode(null);
        drawOnEventBtn(canvas);
        canvas.restoreToCount(layerId);
    }
	//Y轴右
canvas.drawCircle(j_x_1, j_y_1, btnRadius, btnpaint);
      if (j_y_1 > centerY) {
              cX = (float) (centerX + Math.cos(Math.toRadians(getArcAngle())) * (radius / 2));
             if (j_x_1 < cX) {
                  arcpaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_ATOP));
                   canvas.drawArc(arcRectf, getArcAngle(), getArcAngle(), true,arcpaint);
             } else {
                  arcpaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_ATOP));
                   canvas.drawArc(arcRectf, 0, getArcAngle(), true, arcpaint);
            }
   } else {
          cX = (float) (centerX + Math.cos(Math.toRadians(-getArcAngle())) * (radius / 2));
          if (j_x_1 < cX) {
               arcpaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_ATOP));
                canvas.drawArc(arcRectf, -90, getArcAngle(), true, arcpaint);
          } else {
                arcpaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_ATOP));
                canvas.drawArc(arcRectf, -getArcAngle(), getArcAngle(), true,arcpaint);
           }
}

源码:https://github.com/qylfzy/QCircleView

Visual Studio Code (VSCode)是一款强大的开源代码编辑器,为了更好地支持HTMLCSS和JavaScript等前端开发工作,你可以安装一些插件来增强其功能。以下是配置这些语言插件的一些步骤: 1. **安装Extension Manager**:如果你还没有,打开VSCode,在左下角点击"Extensions"或按 `Ctrl+Shift+X`,然后点击 "Install Extensions"。 2. **安装HTML插件**:推荐插件有: - "Live Server": 实时预览网页更改,方便调试。搜索并安装 "live-server"。 - "Prettier - HTML": 自动格式化HTML代码。搜索 "prettier-vscode" 或 "Prettier HTML"。 3. **安装CSS插件**: - "Color Highlight": 高亮显示CSS颜色。搜索并安装 "color-highlight" 或 "Color Highlight for CSS & SCSS"。 - "Autoprefixer": 根据浏览器前缀自动补全。搜索 "Autoprefixer"。 4. **安装JavaScript插件**: - "ESLint": JavaScript代码检查工具,搜索并安装 "eslint". - "JavaScript (TypeScript)": 如果你需要 TypeScript 支持,安装官方的 TypeScript 插件。 5. **配置设置**:安装完插件后,可以在文件夹或用户级别创建一个 `.vscode/settings.json` 文件,添加插件相关的配置,比如设置 ESLint 的路径或启用特定插件的功能。 6. **启动代码提示和自动完成**:确保在设置中启用了相应的代码提示功能,如 "editor.suggestOnTriggerCharacters" 和 "editor.codeActionsOnSave". 记得每次安装新插件后重启VSCode,以便加载新的设置和功能。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小二者也

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值