最近想写个小应用,有个类似抽奖转盘的控件需要实现,因此记录和分享这个实现过程。一开始打算使用自定义view来写的,毕竟之前写过,后来写了一半,发现SurfaceView是一个专门为频繁绘制图形而提供的高性能类,因此决定改为SurfaceView来实现。
使用硬件:Nexus7 2013版,十年前的平板了,性能表现还行。
按住中间的按钮开始转动灯盘,停止后加权两个灯盘的数字,获得加权和进行显示,效果如下:
由于图层的属性约束,因此绘制图形需要先从底层开始绘制,保障绘制顺序,那么按照所设计的转盘,分别从下到上的图层为:
背景圆盘
外层内容 与 内层 数字栅格
按钮图层
数字总和
其中,内层的数字栅格具有演示效果,考虑在灯盘转动过程中,将整个内层栅格全部置灰,以增强视觉对比效果,同时通过控制色块的位置,制造数字灯盘转动的效果,而灯盘转灯的间隔,则根据SurfaceView中的刷新线程间隔来进行控制。
考虑到转盘的整体效果,存在三种状态:初始态 - > 运行态 - > 等待态
初始态表示未发生过启动事件,无选中表现在界面,运行态表示灯盘转动过程,界面需要有对比和动画效果,等待态表示运行结束,选中结果保留在屏幕上。
因此,基于以上考虑,单独实现一个状态机来配合控制状态切换。
考虑到代码的灵活统一和可阅读性,我们除了状态机,还将外层内容,内层数字栅格,按钮及数字图层抽象为三个类,方便随时调整内部参数。
代码目录结构如下:
Component,各图层要素抽象类内容包;
fsm,状态机;
LuckyWheel, GUI绘制及逻辑控制。
外圈内容抽象,包含画笔,背景颜色,以及要定义显示的内容元素。
因为格子并非标准的矩形,因此需要使用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);}
}
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);}}
由于按钮有触摸事件,为了限制触发事件只在按钮圆圈内生效,需要使用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;}
动画控制原理只需要控制不同时刻的颜色变化即可,因此状态接口只需要定义颜色接口。
public interface State {int[] pickStridesColors(); //内圈栅格颜色int[] pickPRsColors(); //外圈栅格颜色
}
使用单例模式做控制器,实现基本的状态切换能力。
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();}
}
初始咋红台下,只要提供固定颜色即可,特殊颜色如内圈的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;}
}
运行状态中,需要灰化每一个数字灯格,表现为未选中状态,每一次运行,都会对选中数字栅格的颜色进行改变,由于我们设计的是双跑马灯,因此,需要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
等待装态需要做的事情为恢复灯盘的原本颜色,同时保留被选中栅格位置的颜色。
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;}
}
代码中使用了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
刷机包: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/
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博客
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