[Android]使用自定义SurfaceView实现幸运转盘效果
迪丽瓦拉
2025-05-31 06:29:54
0
  1. 背景

最近想写个小应用,有个类似抽奖转盘的控件需要实现,因此记录和分享这个实现过程。一开始打算使用自定义view来写的,毕竟之前写过,后来写了一半,发现SurfaceView是一个专门为频繁绘制图形而提供的高性能类,因此决定改为SurfaceView来实现。

使用硬件:Nexus7 2013版,十年前的平板了,性能表现还行。

  1. 效果预览

按住中间的按钮开始转动灯盘,停止后加权两个灯盘的数字,获得加权和进行显示,效果如下:

  1. 实现想法和思路

由于图层的属性约束,因此绘制图形需要先从底层开始绘制,保障绘制顺序,那么按照所设计的转盘,分别从下到上的图层为:

  • 背景圆盘

  • 外层内容 与 内层 数字栅格

  • 按钮图层

  • 数字总和

其中,内层的数字栅格具有演示效果,考虑在灯盘转动过程中,将整个内层栅格全部置灰,以增强视觉对比效果,同时通过控制色块的位置,制造数字灯盘转动的效果,而灯盘转灯的间隔,则根据SurfaceView中的刷新线程间隔来进行控制。

考虑到转盘的整体效果,存在三种状态:初始态 - > 运行态 - > 等待态

初始态表示未发生过启动事件,无选中表现在界面,运行态表示灯盘转动过程,界面需要有对比和动画效果,等待态表示运行结束,选中结果保留在屏幕上。

因此,基于以上考虑,单独实现一个状态机来配合控制状态切换。

  1. 代码实现

考虑到代码的灵活统一和可阅读性,我们除了状态机,还将外层内容,内层数字栅格,按钮及数字图层抽象为三个类,方便随时调整内部参数。

代码目录结构如下:

Component,各图层要素抽象类内容包;

fsm,状态机;

LuckyWheel, GUI绘制及逻辑控制。

4.1图层内容抽象

4.1.1外层内容

外圈内容抽象,包含画笔,背景颜色,以及要定义显示的内容元素。

因为格子并非标准的矩形,因此需要使用path类进行绘制,利用弧度绘制显示,因此需要存储内外相切圆的半径。

将UI和文字的path存储至此类中,方便重复利用。

public class PunishmentAndReward {public int itemsNum;public float bg_out_radius;public float bg_in_radius;public float txt_radius;public final String[] txt_array= {"文字A","文字A","文字A","文字A","文字A","文字A","文字A","文字A"};public List layer_list = new ArrayList<>();public List text_list = new ArrayList<>();public Paint mPaintPRLayer,mPaintText;public final int PRsColorOne = 0xff60c5ba;public final int PRsColorTwo = 0xffffc952;public final int PRsColorThree = 0xffa5dff9;public final int PRsColorFour = 0xffef5285;public PunishmentAndReward(){itemsNum = txt_array.length;mPaintPRLayer = new Paint(Paint.ANTI_ALIAS_FLAG);mPaintPRLayer.setStyle(Paint.Style.FILL);mPaintText = new Paint(Paint.ANTI_ALIAS_FLAG);mPaintText.setStrokeWidth(2);mPaintText.setColor(Color.WHITE);mPaintText.setTextAlign(Paint.Align.CENTER);mPaintText.setTextSize(45);mPaintText.setStyle(Paint.Style.FILL);}
}

4.2.2 内层及数字内容

public class Stride {public int itemsNum;public float bg_out_radius;public float bg_in_radius;public float txt_radius;public final String[] txt_array= {"0", "8", "3", "7" ,"4", "9", "2", "1", "5", "10","6", "3", "1", "12","11","6", "8", "1", "10", "5","12","4","9","1","8","2","7","9","10","6","5","7","4","12","11","3","2","6","9","1"};public List layer_list = new ArrayList<>();public List text_list = new ArrayList<>();public final int defaultZeroColor = 0xff41D3BD;public final int defaultColorOne = 0xff090707;public final int defaultColorTwo = 0xffE53A40;public final int unselectedColor = 0xff9baec8;public final int selectedColor = 0xffffc952;public Paint mPaintStrideLayer, mPaintText;public Stride(){itemsNum = txt_array.length;mPaintStrideLayer = new Paint(Paint.ANTI_ALIAS_FLAG);mPaintStrideLayer.setStyle(Paint.Style.FILL);mPaintText = new Paint(Paint.ANTI_ALIAS_FLAG);mPaintText.setStrokeWidth(2);mPaintText.setColor(Color.WHITE);mPaintText.setTextAlign(Paint.Align.CENTER);mPaintText.setTextSize(55);mPaintText.setStyle(Paint.Style.STROKE);}}

4.2.3按钮内容

由于按钮有触摸事件,为了限制触发事件只在按钮圆圈内生效,需要使用Region类进行判断。

public class StartBtn {public float mRadius=0;public Paint mPaintStartBtn;public Paint mPaintTextBg;public Paint mPaintText;public float offsetText;public Path mPath = new Path();public Region mRegion = new Region();public StartBtn(){mPaintStartBtn = new Paint(Paint.ANTI_ALIAS_FLAG);mPaintStartBtn.setStyle(Paint.Style.FILL);mPaintStartBtn.setColor(0xff1ec0ff);mPaintTextBg = new Paint(Paint.ANTI_ALIAS_FLAG);mPaintTextBg.setStyle(Paint.Style.FILL);mPaintTextBg.setColor(0xff1ec0ff);mPaintText = new Paint(Paint.ANTI_ALIAS_FLAG);mPaintText.setStyle(Paint.Style.FILL);mPaintText.setColor(Color.WHITE);mPaintText.setTextAlign(Paint.Align.CENTER);}public void calc_path(){mPath.addCircle(0,0,mRadius, Path.Direction.CW);
//        Log.i("TAG", "calc_path: "+mRadius);mRegion.setPath(mPath,new Region(-(int)mRadius,-(int)mRadius,(int)mRadius,(int)mRadius));mPaintText.setTextSize(mRadius);offsetText = mRadius/4;}

4.2状态机

4.2.1接口定义

动画控制原理只需要控制不同时刻的颜色变化即可,因此状态接口只需要定义颜色接口。

public interface State {int[] pickStridesColors();    //内圈栅格颜色int[] pickPRsColors();        //外圈栅格颜色
}

4.2.2状态机控制器

使用单例模式做控制器,实现基本的状态切换能力。

public class FSMM implements State{private final String TAG = "FSMM";private static FSMM instalce = null;private final Initial initialState;private final Running runningState;private final Waiting waitingState;private State state;public final String str_initial = "state_initial";public final String str_running = "state_running";public final String str_waiting = "state_waiting";public final int[] selected_flags = {0,1};public Stride strideObj = new Stride();public PunishmentAndReward punRewObj = new PunishmentAndReward();public StartBtn startBtn = new StartBtn();public static FSMM getInstance(){if (null == instalce){synchronized (FSMM.class){if (null == instalce){instalce = new FSMM();}}}return instalce;}private void init(){Log.i(TAG, "init: FSMM 初始化完成 ...");}private FSMM(){initialState = new Initial(this);runningState = new Running(this);waitingState = new Waiting(this);this.state = initialState;}public void setState(State state){Log.i(TAG, "setState: before --> " + getStateString());this.state = state;Log.i(TAG, "setState: after --> " + getStateString());}private State getState(){return this.state;}public State getStateByString(String str){switch (str){case str_initial:return initialState;case str_running:return runningState;case str_waiting:return waitingState;default:Log.i(TAG, "getStateByString: err here");return null;}}public String getStateString(){if (getState() instanceof Initial){return str_initial;}else if (getState() instanceof Running){return str_running;}else if (getState() instanceof Waiting){return str_waiting;}else {return "unknown_state";}}@Overridepublic int[] pickStridesColors() {return state.pickStridesColors();}@Overridepublic int[] pickPRsColors() {return state.pickPRsColors();}
}

4.2.3初始状态定义

初始咋红台下,只要提供固定颜色即可,特殊颜色如内圈的0栅格,额外设置即可。

public class Initial implements State{private final String TAG = "Initial";FSMM fsmm ;public Initial(FSMM fsmm) {this.fsmm = fsmm;}@Overridepublic int[] pickStridesColors() {int[] colors = new int[fsmm.strideObj.itemsNum];colors[0] = fsmm.strideObj.defaultZeroColor;for (int i = 1; i < fsmm.strideObj.itemsNum; i++){if (i % 2 == 0){colors[i] = fsmm.strideObj.defaultColorTwo;}else {colors[i] = fsmm.strideObj.defaultColorOne;}}return colors;}@Overridepublic int[] pickPRsColors() {int[] colors = new int[fsmm.punRewObj.itemsNum];colors[0] = fsmm.punRewObj.PRsColorOne;colors[1] = fsmm.punRewObj.PRsColorTwo;colors[2] = fsmm.punRewObj.PRsColorOne;colors[3] = fsmm.punRewObj.PRsColorThree;colors[4] = fsmm.punRewObj.PRsColorFour;colors[5] = fsmm.punRewObj.PRsColorTwo;colors[6] = fsmm.punRewObj.PRsColorThree;colors[7] = fsmm.punRewObj.PRsColorFour;return colors;}
}

4.2.4运行状态定义

运行状态中,需要灰化每一个数字灯格,表现为未选中状态,每一次运行,都会对选中数字栅格的颜色进行改变,由于我们设计的是双跑马灯,因此,需要FSMM控制器中定义int[] 来存储位置数字,每调运一次接口,选中位置标记分别++和--,并对栅格位置的颜色进行相应的改变。

public class Running implements State {private final String TAG = "Running";FSMM fsmm ;public Running(FSMM fsmm) {this.fsmm = fsmm;}@Overridepublic int[] pickStridesColors() {int[] selected_flags = fsmm.selected_flags;int[] colors = new int[fsmm.strideObj.itemsNum];selected_flags[0]++;selected_flags[1]--;if (selected_flags[1] == selected_flags[0]){selected_flags[0]++;}if (selected_flags[0] > 39){selected_flags[0] = 0;}if (selected_flags[0] < 0){selected_flags[0] = 39;}if (selected_flags[1] > 39){selected_flags[1] = 0;}if (selected_flags[1] < 0){selected_flags[1] = 39;}//        Log.i(TAG, "pickStridesColors: "+ selected_flags[0]+"--"+selected_flags[1]);for (int i=0;i

4.2.5等待状态定义

等待装态需要做的事情为恢复灯盘的原本颜色,同时保留被选中栅格位置的颜色。

public class Waiting implements State{private final String TAG = "Waiting";FSMM fsmm;public Waiting(FSMM fsmm) {this.fsmm = fsmm;}@Overridepublic int[] pickStridesColors() {int[] colors = new int[fsmm.strideObj.itemsNum];for (int i = 0; i < fsmm.strideObj.itemsNum; i++){if (i % 2 == 0){colors[i] = fsmm.strideObj.defaultColorTwo;}else {colors[i] = fsmm.strideObj.defaultColorOne;}if (i == fsmm.selected_flags[0] || i == fsmm.selected_flags[1]){colors[i] = fsmm.strideObj.selectedColor;}}if (0 != fsmm.selected_flags[0] && 0 != fsmm.selected_flags[1]){colors[0] = fsmm.strideObj.defaultZeroColor;}return colors;}@Overridepublic int[] pickPRsColors() {int[] colors = new int[fsmm.punRewObj.itemsNum];colors[0] = fsmm.punRewObj.PRsColorOne;colors[1] = fsmm.punRewObj.PRsColorTwo;colors[2] = fsmm.punRewObj.PRsColorOne;colors[3] = fsmm.punRewObj.PRsColorThree;colors[4] = fsmm.punRewObj.PRsColorFour;colors[5] = fsmm.punRewObj.PRsColorTwo;colors[6] = fsmm.punRewObj.PRsColorThree;colors[7] = fsmm.punRewObj.PRsColorFour;return colors;}
}

4.3基于SurfaceView的转盘代码实现

代码中使用了initBGCounter等需要注意下,本意是为了避免重复绘制造成性能损耗,因为SurfaceView具有多重缓冲的特性,因此如果想固定某个背景不再重新绘制,需要初始化三次画面,因此代码中使用了个计数器保证缓冲全部被填充。

public class LuckyWheel extends SurfaceView implements SurfaceHolder.Callback ,Runnable{private final String TAG = "LuckyWheel";private final Context mContext;private Paint mPaintBackground;private SurfaceHolder mSurfaceHolder;Canvas mCanvas;private float mWidth, mHeight;boolean isDrawing= false, isStarting= false;;float mBackLayerRadius;private int initBGCounter = 0;  //针对三缓冲而设置的计数器private int initPRLayerCounter =0;public LuckyWheel(Context context) {super(context);this.mContext = context;}public LuckyWheel(Context context, AttributeSet attrs) {super(context, attrs);this.mContext = context;}public LuckyWheel(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);this.mContext = context;}@Overrideprotected void onSizeChanged(int w, int h, int oldw, int oldh) {super.onSizeChanged(w, h, oldw, oldh);mWidth = getWidth();mHeight = getHeight();calc_paras();   //计算标准参数init();calc_punishment_and_reward_paras(FSMM.getInstance().punRewObj.bg_out_radius,FSMM.getInstance().punRewObj.bg_in_radius,FSMM.getInstance().punRewObj.txt_radius);calc_stride_layer_paras(FSMM.getInstance().strideObj.bg_out_radius,FSMM.getInstance().strideObj.bg_in_radius,FSMM.getInstance().strideObj.txt_radius);}@Overridepublic void surfaceCreated(@NonNull SurfaceHolder holder) {Log.i(TAG, "surfaceCreated: ");isDrawing = true;new Thread(this).start();}@Overridepublic void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) {}@Overridepublic void surfaceDestroyed(@NonNull SurfaceHolder holder) {Log.i(TAG, "surfaceDestroyed: ");isDrawing = false;}@Overridepublic void run() {Log.i(TAG, "run: 运行开始");long t = 0;while (isDrawing){t = System.currentTimeMillis();try {mCanvas = mSurfaceHolder.lockCanvas();axis_init();draw_background();draw_punishment_and_reward_layer();draw_strides_layer();draw_start_button_layer();}finally {if (mCanvas!=null){mSurfaceHolder.unlockCanvasAndPost(mCanvas);}}
//            Log.i(TAG, "run: 运行中");try {Thread.sleep(Math.max(0, 5-(System.currentTimeMillis()-t)));} catch (InterruptedException e) {e.printStackTrace();}}}private void init(){Log.d(TAG, "init: hehhehe");mSurfaceHolder = getHolder();mSurfaceHolder.addCallback(this);this.setZOrderOnTop(true);  //画布透明处理this.mSurfaceHolder.setFormat(PixelFormat.TRANSLUCENT);setFocusable(true);setFocusableInTouchMode(true);this.setKeepScreenOn(true); //屏幕常亮mPaintBackground = new Paint(Paint.ANTI_ALIAS_FLAG);mPaintBackground.setColor(0xff52616a);mPaintBackground.setStyle(Paint.Style.FILL);FSMM.getInstance().startBtn.mPaintStartBtn.setShadowLayer(FSMM.getInstance().startBtn.mRadius,0,0,0xff0080ff);FSMM.getInstance().startBtn.calc_path();}//坐标原点移动到中间,y轴翻转private void axis_init(){mCanvas.translate(mWidth/2, mHeight/2);
//        mCanvas.scale(1,-1);}private void draw_background(){if (initBGCounter < 4){mCanvas.drawCircle(0,0,mBackLayerRadius,mPaintBackground);mCanvas.drawPath(FSMM.getInstance().startBtn.mPath,FSMM.getInstance().startBtn.mPaintStartBtn);initBGCounter++;}}private void calc_paras(){mBackLayerRadius = mWidth/2;FSMM.getInstance().punRewObj.bg_out_radius = mWidth/30 * 14;FSMM.getInstance().punRewObj.bg_in_radius = mWidth/30 * 11;FSMM.getInstance().punRewObj.txt_radius = (float) (mWidth/30 * 12) ;FSMM.getInstance().strideObj.bg_out_radius = mWidth/30 * 11;FSMM.getInstance().strideObj.bg_in_radius = mWidth/30 * 9;FSMM.getInstance().strideObj.txt_radius = (float)(mWidth/30 * 9.5) ;FSMM.getInstance().startBtn.mRadius = mWidth/30 * 7;}private void draw_strides_layer(){int[] colors = FSMM.getInstance().pickStridesColors();for (int i = 0; i 
  1. 引用参考

5.1Nexus7平板

刷机包:https://forum.xda-developers.com/t/rom-flo-deb-unofficial-lineageos-19-1-2023-02-18.3569067/page-204#post-88244585

刷机参考:https://blog.lzc.app/index.php/2021/09/27/nexus-7-%E4%BA%8C%E4%BB%A3-%E5%88%B7%E6%9C%BA%E5%AE%89%E8%A3%85android-7-1-2%E7%B3%BB%E7%BB%9F/

5.2参考

Android 自定义View —— Path_android path 描边_胡小牧的博客-CSDN博客

安卓canvas path addArc()与arcTo()方法的区别_path.addarc_Java_noob1的博客-CSDN博客

Android_自定义遥控器按钮_CodeCopyer的博客-CSDN博客

android 自定义view 画板改变画笔颜色_android 不断变化paint颜色修改_小鲁班one的博客-CSDN博客

LOL Colors - Curated color palette inspiration (webdesignrankings.com)

Android 自定义View-文字绘制_android 自定义view绘制文字_xiangxiongfly915的博客-CSDN博客

5.3资源

https://pictogrammers.com/library/mdi/

https://fonts.google.com/icons?selected=Material+Icons&icon.platform=android

https://www.aigei.com/s?tab=file&type=2d&dim=interface_ui-is_vip_false

相关内容

热门资讯

linux入门---制作进度条 了解缓冲区 我们首先来看看下面的操作: 我们首先创建了一个文件并在这个文件里面添加了...
C++ 机房预约系统(六):学... 8、 学生模块 8.1 学生子菜单、登录和注销 实现步骤: 在Student.cpp的...
A.机器学习入门算法(三):基... 机器学习算法(三):K近邻(k-nearest neigh...
数字温湿度传感器DHT11模块... 模块实例https://blog.csdn.net/qq_38393591/article/deta...
有限元三角形单元的等效节点力 文章目录前言一、重新复习一下有限元三角形单元的理论1、三角形单元的形函数(Nÿ...
Redis 所有支持的数据结构... Redis 是一种开源的基于键值对存储的 NoSQL 数据库,支持多种数据结构。以下是...
win下pytorch安装—c... 安装目录一、cuda安装1.1、cuda版本选择1.2、下载安装二、cudnn安装三、pytorch...
MySQL基础-多表查询 文章目录MySQL基础-多表查询一、案例及引入1、基础概念2、笛卡尔积的理解二、多表查询的分类1、等...
keil调试专题篇 调试的前提是需要连接调试器比如STLINK。 然后点击菜单或者快捷图标均可进入调试模式。 如果前面...
MATLAB | 全网最详细网... 一篇超超超长,超超超全面网络图绘制教程,本篇基本能讲清楚所有绘制要点&#...
IHome主页 - 让你的浏览... 随着互联网的发展,人们越来越离不开浏览器了。每天上班、学习、娱乐,浏览器...
TCP 协议 一、TCP 协议概念 TCP即传输控制协议(Transmission Control ...
营业执照的经营范围有哪些 营业执照的经营范围有哪些 经营范围是指企业可以从事的生产经营与服务项目,是进行公司注册...
C++ 可变体(variant... 一、可变体(variant) 基础用法 Union的问题: 无法知道当前使用的类型是什...
血压计语音芯片,电子医疗设备声... 语音电子血压计是带有语音提示功能的电子血压计,测量前至测量结果全程语音播报࿰...
MySQL OCP888题解0... 文章目录1、原题1.1、英文原题1.2、答案2、题目解析2.1、题干解析2.2、选项解析3、知识点3...
【2023-Pytorch-检... (肆十二想说的一些话)Yolo这个系列我们已经更新了大概一年的时间,现在基本的流程也走走通了,包含数...
实战项目:保险行业用户分类 这里写目录标题1、项目介绍1.1 行业背景1.2 数据介绍2、代码实现导入数据探索数据处理列标签名异...
记录--我在前端干工地(thr... 这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助 前段时间接触了Th...
43 openEuler搭建A... 文章目录43 openEuler搭建Apache服务器-配置文件说明和管理模块43.1 配置文件说明...