100字范文,内容丰富有趣,生活中的好帮手!
100字范文 > [Android]使用自定义SurfaceView实现幸运转盘效果

[Android]使用自定义SurfaceView实现幸运转盘效果

时间:2019-08-16 08:03:07

相关推荐

[Android]使用自定义SurfaceView实现幸运转盘效果

背景

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

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

效果预览

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

GIF有时加载不出来,csdn的问题, 我贴张图片大家自行想象下,数字区域黄色的灯块会在用户按住中心按钮时进行滚动,松手后取和显示在中央。

不知道为什么,这个GIF在CSDN有时候就是显示不出来

实现想法和思路

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

背景圆盘

外层内容 与 内层 数字栅格

按钮图层

数字总和

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

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

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

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

代码实现

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

代码目录结构如下:

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<Path> layer_list = new ArrayList<>();public List<Path> 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<Path> layer_list = new ArrayList<>();public List<Path> 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<fsmm.strideObj.itemsNum;i++){if (i == selected_flags[0] || i ==selected_flags[1]){colors[i] = fsmm.strideObj.selectedColor;}else {colors[i] = fsmm.strideObj.unselectedColor;}}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.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 <FSMM.getInstance().strideObj.itemsNum ;i++){FSMM.getInstance().strideObj.mPaintStrideLayer.setColor(colors[i]);mCanvas.drawPath(FSMM.getInstance().strideObj.layer_list.get(i),FSMM.getInstance().strideObj.mPaintStrideLayer);}for (int i = 0 ; i< FSMM.getInstance().strideObj.itemsNum;i++){mCanvas.drawTextOnPath(FSMM.getInstance().strideObj.txt_array[i],FSMM.getInstance().strideObj.text_list.get(i),0,0, FSMM.getInstance().strideObj.mPaintText);}}/*数字层参数计算,包含文字*/private void calc_stride_layer_paras(float out_rads, float in_rads, float text_rads){int start_angle = 0;int offset_angle = 360 / FSMM.getInstance().strideObj.itemsNum; //计算每个item需要旋转的角度RectF text_recfF = new RectF(-text_rads,-text_rads,text_rads,text_rads);RectF out_rectF = new RectF(-out_rads, -out_rads, out_rads, out_rads);RectF in_recfF = new RectF(-in_rads, -in_rads, in_rads, in_rads);for (int i =0; i <FSMM.getInstance().strideObj.itemsNum; i++){int out_start_angle = start_angle + i * offset_angle; //每次偏移int in_start_angle = out_start_angle + offset_angle; //内偏多一个便宜角度反向开始扫弧度Path path = new Path();path.arcTo(out_rectF, out_start_angle, offset_angle,false);path.arcTo(in_recfF,in_start_angle,-offset_angle,false);path.close();FSMM.getInstance().strideObj.layer_list.add(path);Path mStrideTextPath = new Path();mStrideTextPath.arcTo(text_recfF,out_start_angle,offset_angle);FSMM.getInstance().strideObj.text_list.add(mStrideTextPath);}}private void draw_start_button_layer(){mCanvas.drawPath(FSMM.getInstance().startBtn.mPath,FSMM.getInstance().startBtn.mPaintTextBg);int num1 = Integer.parseInt(FSMM.getInstance().strideObj.txt_array[FSMM.getInstance().selected_flags[0]]);int num2 = Integer.parseInt(FSMM.getInstance().strideObj.txt_array[FSMM.getInstance().selected_flags[1]]);String numStr = String.valueOf(num1+num2);float offset = FSMM.getInstance().startBtn.offsetText;mCanvas.drawText(numStr,0,offset,FSMM.getInstance().startBtn.mPaintText);}private void draw_punishment_and_reward_layer(){if (initPRLayerCounter < 4){initPRLayerCounter ++ ;int[] colors = FSMM.getInstance().pickPRsColors();for (int i = 0; i <FSMM.getInstance().punRewObj.itemsNum ;i++){FSMM.getInstance().punRewObj.mPaintPRLayer.setColor(colors[i]);mCanvas.drawPath(FSMM.getInstance().punRewObj.layer_list.get(i),FSMM.getInstance().punRewObj.mPaintPRLayer);}for (int i = 0 ; i< FSMM.getInstance().punRewObj.itemsNum;i++){mCanvas.drawTextOnPath(FSMM.getInstance().punRewObj.txt_array[i],FSMM.getInstance().punRewObj.text_list.get(i),0,0, FSMM.getInstance().punRewObj.mPaintText);}}}private void calc_punishment_and_reward_paras(float out_rads, float in_rads, float text_rads){int start_angle = 0;int offset_angle = 360 / FSMM.getInstance().punRewObj.itemsNum;RectF text_recfF = new RectF(-text_rads,-text_rads,text_rads,text_rads);RectF out_rectF = new RectF(-out_rads, -out_rads, out_rads, out_rads);RectF in_recfF = new RectF(-in_rads, -in_rads, in_rads, in_rads);for (int i = 0; i < FSMM.getInstance().punRewObj.itemsNum;i++){int out_start_angle = start_angle + i * offset_angle; //每次偏移int in_start_angle = out_start_angle + offset_angle; //内偏多一个便宜角度反向开始扫弧度Path path = new Path();path.arcTo(out_rectF, out_start_angle, offset_angle,false);path.arcTo(in_recfF,in_start_angle,-offset_angle,false);path.close();FSMM.getInstance().punRewObj.layer_list.add(path);Path mTextPath = new Path();mTextPath.arcTo(text_recfF,out_start_angle,offset_angle);FSMM.getInstance().punRewObj.text_list.add(mTextPath);}}@SuppressLint("ClickableViewAccessibility")@Overridepublic boolean onTouchEvent(MotionEvent event) {float x = event.getX();float y = event.getY();switch (event.getAction()){case MotionEvent.ACTION_MOVE:break;case MotionEvent.ACTION_DOWN://判断是否落在按钮区域内,event.getXY获得的是屏幕坐标系,由于画布坐标系做过平移,因此需要做转换if (FSMM.getInstance().startBtn.mRegion.contains((int)event.getX()-(int)mWidth/2,(int)event.getY()-((int) mHeight/2))){FSMM.getInstance().selected_flags[0] = new Random().nextInt(FSMM.getInstance().strideObj.itemsNum);FSMM.getInstance().selected_flags[1] = new Random().nextInt(FSMM.getInstance().strideObj.itemsNum);FSMM.getInstance().setState(FSMM.getInstance().getStateByString("state_running"));}else {Log.i(TAG, "onTouchEvent: 未落在");}Log.i(TAG, "ACTION_DOWN: "+x+"---"+y);break;case MotionEvent.ACTION_UP:FSMM.getInstance().setState(FSMM.getInstance().getStateByString("state_waiting"));break;}return true;}}

引用参考

5.1Nexus7平板

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

刷机参考:https://blog.lzc.app/index.php//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 ()

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

5.3资源

/library/mdi/

/icons?selected=Material+Icons&icon.platform=android

/s?tab=file&type=2d&dim=interface_ui-is_vip_false

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。