一、前言
旋转菜单是一种占用空间较大,实用性稍弱的ui,一方面由于展示空间的问题,其展示的数据有限,但另一方面真由于这个原因,对用户而言趣味性和操作性反而更有好。
二、绘制原理
绘制原理很简单,通过细微的观察,我们发现文字是不需要旋转的,也就是每个菜单是不需要自旋转,只需要旋转其位置坐标即可,实际上其难点并不是绘制,而是在于触摸事件的处理方式。
本篇菜单特性:
- 动态设置菜单
- 计算旋转方向和旋转角度
- 支持点击
难点1:
旋转方向判断,旋转时记录起始点,计算出旋转方向。
首先,我们要理解,touch事件也存在抽象的坐标体系,和view左上角重合,因此我们需要转换坐标
float cx = event.getx() - getwidth() / 2f; float cy = event.gety() - getheight() / 2f;
旋转角度的计算
这种计算是为了计算出与原始落点位置的夹角,这里的方法是计算使用math.asin反正切函数,然后结合坐标系进行判断
float linewidth = (float) math.sqrt(math.pow(cx, 2) math.pow(cy, 2)); float degreeradian = (float) math.asin(cy / linewidth); float dr = 0; if (cy > 0) { //一二象限 if (cx > 0) { dr = degreeradian; } else { dr = (float) ((math.pi - degreeradian)); } } else { //三四象限 if (cx > 0) { dr = (float) (math.pi * 2 - math.abs(degreeradian)); } else { dr = (float) ((math.pi math.abs(degreeradian))); } }
由于对math的了解我们知道,math.asin不能反映真实的夹角,因此需要做上面的补充。但是后来我们发现,math.atan2函数的存在,直接可以求出斜率夹角,而且不会丢失象限关系,一下子就省了好几行代码。
dr = (float) math.atan2(cy, cx);
难点2:实时更新
为了旋转,我们可能忘记记录最新位置,这个可能导致圆反向旋转,因此要实时记录位置
estartx = cx; estarty = cy;
难点3:由于拦截了up事件,因此需要对up事件进行专门处理
if (system.currenttimemillis() - startdowntime > 500) { break; } float upx = event.getx() - getwidth() / 2f; float upy = event.gety() - getheight() / 2f; handleclicktap(upx, upy);
全部代码:
public class oribitview extends view { private final string tag = "oribitview"; private displaymetrics displaymetrics; private float moutlineraduis; private float minlineradius; private textpaint mpaint; private float linewidth = 5f; private float textsize = 12f; private int itemcount = 5; private int mtouchslop = 0; private float rotatedegreeradian = 0; private onitemclicklistener onitemclicklistener; private float estartx = 0f; private float estarty = 0f; private boolean ismovetouch = false; private float startdegreeradian = 0l; //记录用于落点角度,用于参考 private long startdowntime = 0l; rect bounds = new rect(); private final listmoribititempoints = new arraylist<>(); public oribitview(context context) { this(context, null); } public oribitview(context context, @nullable attributeset attrs) { this(context, attrs, 0); } public oribitview(context context, @nullable attributeset attrs, int defstyleattr) { super(context, attrs, defstyleattr); displaymetrics = context.getresources().getdisplaymetrics(); initpaint(); mtouchslop = viewconfiguration.get(context).getscaledtouchslop(); setlayertype(layer_type_software,null); } private void initpaint() { // 实例化画笔并打开抗锯齿 mpaint = new textpaint(paint.anti_alias_flag); mpaint.setantialias(true); mpaint.settextsize(dptopx(textsize)); } private float dptopx(float dp) { return typedvalue.applydimension(typedvalue.complex_unit_dip, dp, getresources().getdisplaymetrics()); } @override protected void onmeasure(int widthmeasurespec, int heightmeasurespec) { super.onmeasure(widthmeasurespec, heightmeasurespec); int widthmode = measurespec.getmode(widthmeasurespec); int widthsize = measurespec.getsize(widthmeasurespec); if (widthmode != measurespec.exactly) { widthsize = displaymetrics.widthpixels / 2; } int heightmode = measurespec.getmode(heightmeasurespec); int heightsize = measurespec.getsize(heightmeasurespec); if (heightmode != measurespec.exactly) { heightsize = displaymetrics.widthpixels / 2; } widthsize = heightsize = math.min(widthsize, heightsize); setmeasureddimension(widthsize, heightsize); } @override protected void onsizechanged(int w, int h, int oldw, int oldh) { super.onsizechanged(w, h, oldw, oldh); moutlineraduis = w / 2.0f - dptopx(linewidth); minlineradius = moutlineraduis * 3 / 5.0f - dptopx(linewidth); } @override protected void ondraw(canvas canvas) { super.ondraw(canvas); int width = getwidth(); int height = getwidth(); mpaint.setstyle(paint.style.stroke); mpaint.setstrokewidth(dptopx(linewidth / 4)); mpaint.setcolor(color.gray); int id = canvas.save(); float centerradius = (moutlineraduis minlineradius) / 2; float itemradius = (moutlineraduis - minlineradius) / 2; canvas.translate(width / 2f, height / 2f); // canvas.drawcircle(0, 0, moutlineraduis, mpaint); //画外框 // canvas.drawcircle(0, 0, minlineradius, mpaint); //画内框 float strokewidth = mpaint.getstrokewidth(); mpaint.setstrokewidth(itemradius * 2 - dptopx(linewidth / 2)); mpaint.setcolor(color.dkgray); mpaint.setshadowlayer(10,0,10,color.dkgray); canvas.drawcircle(0, 0, centerradius, mpaint); mpaint.setstrokewidth(strokewidth); float degree = (float) (2 * math.asin(itemradius / centerradius)); //计算出从原点过item的切线夹角,求出每个圆所占夹角大小 float spacedegree = (float) ((math.pi * 2 - degree * itemcount) / itemcount); for (int i = 0; i < moribititempoints.size(); i ) { oribititempoint itempoint = moribititempoints.get(i); float x = (float) (centerradius * math.cos(rotatedegreeradian i * (spacedegree degree))); float y = (float) (centerradius * math.sin(rotatedegreeradian i * (spacedegree degree))); itempoint.x = x; itempoint.y = y; oribititem oribititem = itempoint.getoribititem(); mpaint.setstyle(paint.style.fill); mpaint.setcolor(oribititem.backgroundcolor); //减去线宽 float strokeoffset = dptopx(linewidth / 2); canvas.drawcircle(x, y, itemradius - strokeoffset, mpaint); mpaint.setcolor(oribititem.textcolor); string text = string.valueof(oribititem.text); mpaint.gettextbounds(text, 0, text.length(), bounds); float textbaseline = gettextpaintbaseline(mpaint) - y - bounds.height() strokeoffset; canvas.drawtext(text, x - bounds.width() / 2f, -textbaseline, mpaint); } canvas.restoretocount(id); } @override public boolean ontouchevent(motionevent event) { switch (event.getaction()) { case motionevent.action_down: estartx = event.getx() - getwidth() / 2f; //这里转为原点为画布中心的点,便于计算角度 estarty = event.gety() - getheight() / 2f; //求出落点与坐标系x轴方向的夹角( float locationradian = (float) math.asin(estarty / (float) math.sqrt(math.pow(estartx, 2) math.pow(estarty, 2))); // //根据正弦值计算起点在那个象限 // if (estarty > 0) { // //一二象限 // if (estartx < 0) { // startdegreeradian = (float) (math.pi - locationradian); // } else { // startdegreeradian = locationradian; // } // } else { // //三四象限 // if (estartx > 0) { // startdegreeradian = (float) (math.pi * 2 - math.abs(locationradian)); // } else { // startdegreeradian = (float) (math.pi math.abs(locationradian)); // } // } startdegreeradian = locationradian; startdowntime = system.currenttimemillis(); getparent().requestdisallowintercepttouchevent(true); super.ontouchevent(event); return true; case motionevent.action_move: //坐标转换 float cx = event.getx() - getwidth() / 2f; float cy = event.gety() - getheight() / 2f; float dx = cx - estartx; float dy = cy - estarty; float slideslop = (float) math.sqrt(math.pow(dx, 2) math.pow(dy, 2)); if (slideslop > mtouchslop) { ismovetouch = true; } else { ismovetouch = false; } if (ismovetouch) { float linewidth = (float) math.sqrt(math.pow(cx, 2) math.pow(cy, 2)); float degreeradian = (float) math.asin(cy / linewidth); float dr = 0; // // if (cy > 0) { // //一二象限 // if (cx > 0) { // dr = degreeradian; // } else { // dr = (float) ((math.pi - degreeradian)); // } // // } else { // //三四象限 // if (cx > 0) { // dr = (float) (math.pi * 2 - math.abs(degreeradian)); // } else { // dr = (float) ((math.pi math.abs(degreeradian))); // } // } dr = (float) math.atan2(cy, cx); rotatedegreeradian = (dr - startdegreeradian); startdegreeradian = dr; estartx = cx; estarty = cy; postinvalidate(); } break; case motionevent.action_up: case motionevent.action_cancel: case motionevent.action_outside: getparent().requestdisallowintercepttouchevent(false); if (ismovetouch) { ismovetouch = false; break; } if (system.currenttimemillis() - startdowntime > 500) { break; } float upx = event.getx() - getwidth() / 2f; float upy = event.gety() - getheight() / 2f; handleclicktap(upx, upy); break; } return super.ontouchevent(event); } private void handleclicktap(float upx, float upy) { if (itemcount == 0 || moribititempoints == null) return; oribititempoint clickitempoint = null; float itemradius = (moutlineraduis - minlineradius) / 2; for (oribititempoint itempoint : moribititempoints) { if (float.isnan(itempoint.x) || float.isnan(itempoint.y)) { continue; } float dx = (itempoint.x - upx); float dy = (itempoint.y - upy); float clickslop = (float) math.sqrt(math.pow(dx, 2) math.pow(dy, 2)); if (clickslop >= itemradius) { continue; } clickitempoint = itempoint; break; } if (clickitempoint == null) return; if (this.moribititempoints != null) { this.onitemclicklistener.onitemclick(this, clickitempoint.oribititem); } } public int getitemcount() { return itemcount; } public static float gettextpaintbaseline(paint p) { paint.fontmetrics fontmetrics = p.getfontmetrics(); return (fontmetrics.descent - fontmetrics.ascent) / 2 - fontmetrics.descent; } public void showitems(list oribititems) { moribititempoints.clear(); if (oribititems != null) { for (oribititem item : oribititems) { oribititempoint point = new oribititempoint(); point.x = float.nan; point.y = float.nan; point.oribititem = item; moribititempoints.add(point); } } this.itemcount = moribititempoints.size(); postinvalidate(); } public void setonitemclicklistener(onitemclicklistener onitemclicklistener) { this.onitemclicklistener = onitemclicklistener; } public static class oribititem { public string text; public int textcolor; public int backgroundcolor; } static class oribititempoint extends pointf { private t oribititem; public void setoribititem(t oribititem) { this.oribititem = oribititem; } public t getoribititem() { return oribititem; } } public interface onitemclicklistener { public void onitemclick(view contentview, oribititem item); } }
用法:
oribitview oribitview = findviewbyid(r.id.oribitview); oribitview.setonitemclicklistener(new oribitview.onitemclicklistener() { @override public void onitemclick(view contentview, oribitview.oribititem item) { toast.maketext(contentview.getcontext(),item.text,toast.length_short).show(); } }); listoribititems = new arraylist<>(); string[] chs = new string[]{"鲜花", "牛奶", "橘子", "生活", "新闻", "热点"}; int[] colors = new int[]{argb(random.nextfloat(), random.nextfloat(), random.nextfloat()), argb(random.nextfloat(), random.nextfloat(), random.nextfloat()), argb(random.nextfloat(), random.nextfloat(), random.nextfloat()), argb(random.nextfloat(), random.nextfloat(), random.nextfloat()), argb(random.nextfloat(), random.nextfloat(), random.nextfloat()), argb(random.nextfloat(), random.nextfloat(), random.nextfloat()) }; for (int i = 0; i < chs.length; i ) { oribitview.oribititem item = new oribitview.oribititem(); item.text = chs[i]; item.textcolor = color.white; item.backgroundcolor = colors[i]; oribititems.add(item); } oribitview.showitems(oribititems);
三、总结
本篇难点主要是事件处理,当然可能有人会问,使用layout添加岂不是更方便,答案是肯定的,但是本篇主要重点介绍canvas 绘制,后续有layout的布局,当然这里其实区别并不大,不同点是一个需要onlayout的调用,另一个是ondraw的调用,做好坐标轴转换即可,难度并不大。
以上就是android自定义实现转盘菜单的详细内容,更多关于android转盘菜单的资料请关注其它相关文章!