一、前言
旋转菜单是一种占用空间较大,实用性稍弱的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 list<oribititempoint> moribititempoints = 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<oribititem> 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<t extends oribititem> 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();
}
});
list<oribitview.oribititem> oribititems = 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转盘菜单的资料请关注代码网其它相关文章!
发表评论