一、前言
循环菜单有很多种自定义方式,我们可以利用viewpager或者recyclerview + carousellayoutmanager 或者recyclerview + pagesnaphelper来实现这种效果,今天我们使用canvas 2d来实现这种效果。

二、实现
loopview 是常见的循环 view,一般应用于循环展示菜单项目,本次实现的是一组循环菜单,按照垂直方向,实际上,如果把某些变量互换,可以实现轮播图效果。
最终目标
- 在滑动过程中记录偏移的位置,将画出界面的从列表中移除,分别向两端添加。
- 离中心点越近,半径就会越大
- 模仿recyler机制,偏移到界面以外的item回收利用
2.1 定义菜单项
首先这里定义一下菜单item,主要标记颜色和文本内容
public static class loopitem {
private int color;
private string text;
public loopitem(string text, int color) {
this.color = color;
this.text = text;
}
public int getcolor() {
return color;
}
public void setcolor(int color) {
this.color = color;
}
public string gettext() {
return text;
}
public void settext(string text) {
this.text = text;
}
}
接下来需要定义绘制任务,将菜单数据和绘制任务解耦。
我们这里需要
- 半径
- x,y坐标
- 半径缩放增量
public static class drawtask<t extends loopitem> {
private t loopitem;
private float radius; //半径,定值
private float x;
private float y;
private float scaleoffset = 0; // 半径缩放偏移量,离中心越远,此值就会越小
public drawtask(float x, float y, float radius) {
this.radius = radius;
this.x = x;
this.y = y;
}
public void setloopitem(t loopitem) {
this.loopitem = loopitem;
}
public void draw(canvas canvas, textpaint textpaint) {
if (loopitem == null) return;
textpaint.setcolor(loopitem.getcolor());
textpaint.setstyle(paint.style.fill);
textpaint.setshadowlayer(10, 0, 5, 0x99444444);
//绘制圆
canvas.drawcircle(x, y, radius + scaleoffset, textpaint);
textpaint.setcolor(color.white);
textpaint.setstyle(paint.style.fill);
//绘制文本
string text = loopitem.gettext();
float textwidth = textpaint.measuretext(text);
float baseline = gettextpaintbaseline(textpaint);
canvas.drawtext(text, -textwidth / 2, y + baseline, textpaint);
}
public t getloopitem() {
return loopitem;
}
}
2.2 半径计算
半径计算其实只需要按默的最小边的一半除以要展示的数量,为什么要这样计算呢?因为这样可以保证圆心等距,我们这里实现的效果其实是放大圆而不是缩小圆的方式,因此,默认情况
int max_visible_count = 5 //这个值建议是奇数 circleradius = math.min(w / 2f, h / 2f) / max_visible_count;
2.3 通过位置偏移进行复用和回收
这里主要是模仿recycler机制,对drawtask回收和复用
//回收前处理,保证偏移连续
private void recyclerbefore(int height) {
if (istoucheventup) {
float centeroffset = getminyoffset();
resetitemyoffset(height, centeroffset);
} else {
resetitemyoffset(height, offsety);
}
istoucheventup = false;
}
//回收后处理,保证item连续
private void recyclerafter(int height) {
if (istoucheventup) {
float centeroffset = getminyoffset();
resetitemyoffset(height, centeroffset);
} else {
resetitemyoffset(height, 0);
}
}
//进行回收和复用,用head和tail指针对两侧外的item移除和复用
private void recycler() {
if (drawtasks.size() < (max_visible_count - 2)) return;
collections.sort(drawtasks, drawtaskcomparatory);
drawtask head = drawtasks.get(0); //head 指针
drawtask tail = drawtasks.get(drawtasks.size() - 1); //尾指针
int height = getheight();
if (head.y < -(height / 2f + circleradius)) {
drawtasks.remove(head);
addtocachepool(head);
head.setloopitem(null); //回收
} else {
drawtask drawtask = getcachepool(); //复用
loopitem loopitem = head.getloopitem();
loopitem preloopitem = getpreloopitem(loopitem);
drawtask.setloopitem(preloopitem);
drawtask.y = head.y - circleradius * 2;
drawtasks.add(0, drawtask);
}
if (tail.y > (height / 2f + circleradius)) {
drawtasks.remove(tail);
addtocachepool(tail);
tail.setloopitem(null);
} else {
drawtask drawtask = getcachepool();
loopitem loopitem = tail.getloopitem();
loopitem nextloopitem = getnextloopitem(loopitem);
drawtask.setloopitem(nextloopitem);
drawtask.y = tail.y + circleradius * 2;
drawtasks.add(drawtask);
}
}
2.4 防止靠近中心的view被绘制
远离中心的item要先绘制,意味着靠近边缘的要优先绘制,防止盖住中心的item,因此每次都需要排序 这里的outoffset半径偏移值,半径越小的就会排在前面
collections.sort(drawtasks, new comparator<drawtask>() {
@override
public int compare(drawtask left, drawtask right) {
float dx = math.abs(left.y) - math.abs(right.y);
if (dx > 0) {
return 1;
}
if (dx == 0) {
return 0;
}
return -1;
}
});
2.5 获取离中心点最近的item的y值
scaleoffset越大,离圆心越近,通过这种方式就能筛选出靠近圆心的item y坐标
private float getminyoffset() {
float miny = 0;
float offset = 0;
for (int i = 0; i < drawtasks.size(); i++) {
drawtask drawtask = drawtasks.get(i);
if (math.abs(drawtask.scaleoffset) > offset) {
miny = -drawtask.y;
offset = drawtask.scaleoffset;
}
}
return miny;
}

2.6 根据滑动方向重新计算每个item的偏移
item是需要移动的,因此在事件处理的时候一定要进行偏移处理,因此滑动过程需要对y值进行有效处理,当然要避免为1,防止view出现缩小而不是滑动的效果。
private void resetitemyoffset(int height, float centeroffset) {
for (int i = 0; i < drawtasks.size(); i++) {
drawtask task = drawtasks.get(i);
task.y = (task.y + centeroffset);
float ratio = math.abs(task.y) / (height / 2f);
if (ratio > 1f) {
ratio = 1f;
}
task.outoffset = ((10 + circleradius) * 3 / 4f) * (1 - ratio);
}
}
2.7 事件处理
我们要支持item移动,因此必然要处理touchevent,首先我们需要在action_down时拦截事件,其次需要处理action_move事件和action_up事件中产生的位置偏移。
另外,我们保留系统内默认view对事件处理的方式,具体原理就是在ontouchevent返回之前调用super.ontouchevent方法
super.ontouchevent(event); return true;
下面是事件处理完整的方法,基本是常规操作
@override
public boolean ontouchevent(motionevent event) {
int action = event.getactionmasked();
istoucheventup = false;
switch (action) {
case motionevent.action_down:
offsety = 0;
starteventx = event.getx() - getwidth() / 2f;
starteventy = event.gety() - getheight() / 2f;
super.ontouchevent(event);
return true;
case motionevent.action_move:
float eventx = event.getx();
float eventy = event.gety();
if (eventy < 0) {
eventy = 0;
}
if (eventx < 0) {
eventx = 0;
}
if (eventy > getwidth()) {
eventx = getwidth();
}
if (eventy > getheight()) {
eventy = getheight();
}
float currentx = eventx - getwidth() / 2f;
float currenty = eventy - getheight() / 2f;
float dx = currentx - starteventx;
float dy = currenty - starteventy;
if (math.abs(dx) < math.abs(dy) && math.abs(dy) >= sloptouch) {
istouchmove = true;
}
if (!istouchmove) {
break;
}
offsety = dy;
starteventx = currentx;
starteventy = currenty;
postinvalidate();
super.ontouchevent(event);
return true;
case motionevent.action_cancel:
case motionevent.action_outside:
case motionevent.action_up:
istouchmove = false;
istoucheventup = true;
offsety = 0;
log.d("eventup", "offsety=" + offsety);
postinvalidate();
break;
}
return super.ontouchevent(event);
}
三、使用方法
使用方法
loopview loopview = findviewbyid(r.id.loopviews);
final list<loopview.loopitem> loopitems = new arraylist<>();
int[] colors = {
color.red,
color.cyan,
color.gray,
color.green,
color.black,
color.magenta,
0xffff9922,
0xffff4081,
0xffffeac4
};
string[] items = {
"新闻",
"科技",
"历史",
"军事",
"小说",
"娱乐",
"电影",
"电视剧",
};
for (int i = 0; i < items.length; i++) {
loopview.loopitem loopitem = new loopview.loopitem(items[i], colors[i % colors.length]);
loopitems.add(loopitem);
}
loopview.setloopitems(loopitems);
}
loopview loopview = new loopview(this);
loopview.setloopitems(loopitems);
framelayout framelayout = new framelayout(this);
framelayout.marginlayoutparams layoutparams = new framelayout.marginlayoutparams(viewgroup.layoutparams.match_parent,720);
layoutparams.topmargin = 100;
layoutparams.leftmargin = 50;
layoutparams.rightmargin = 50;
framelayout.addview(loopview,layoutparams);
setcontentview(framelayout);
四、总结
4.1 整体效果
其实效果上还是可以的,本质上和listview和recyclerview思想类似,但是循环这一块儿其实和wheelview 思想类似。
4.2 点击事件处理
实际上本篇的view市支持点击事件的,当时点击区域没有判断,不过也是比较好处理,只要对drawtask排序,保证最中间的item可以点击即可,篇幅有限,这里就不处理了。
4.3 全部代码
public class loopview extends view {
private static final int max_visible_count = 5;
private textpaint mtextpaint = null;
private displaymetrics displaymetrics = null;
private int mlinewidth = 1;
private int mtextsize = 14;
private int sloptouch = 0;
private float circleradius;
private final list<drawtask> drawtasks = new arraylist<>();
private final list<drawtask> cachedrawtasks = new arraylist<>();
private final list<loopitem> loopitems = new arraylist<>();
boolean isinit = false;
private float starteventx = 0;
private float starteventy = 0;
private boolean istouchmove = false;
private float offsety = 0;
boolean istoucheventup = false;
public loopview(context context) {
this(context, null);
}
public loopview(context context, attributeset attrs) {
this(context, attrs, 0);
}
public loopview(context context, attributeset attrs, int defstyleattr) {
super(context, attrs, defstyleattr);
setclickable(true);
setfocusable(true);
setfocusableintouchmode(true);
displaymetrics = context.getresources().getdisplaymetrics();
mtextpaint = createpaint();
sloptouch = viewconfiguration.get(context).getscaledtouchslop();
setlayertype(layer_type_software, null);
initdesigneditmode();
}
private void initdesigneditmode() {
if (!isineditmode()) return;
int[] colors = {
color.red,
color.cyan,
color.yellow,
color.gray,
color.green,
color.black,
color.magenta,
0xffff9922,
};
string[] items = {
"新闻",
"科技",
"历史",
"军事",
"小说",
"娱乐",
"电影",
"电视剧",
};
for (int i = 0; i < items.length; i++) {
loopitem loopitem = new loopitem(items[i], colors[i % colors.length]);
loopitems.add(loopitem);
}
}
@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;
}
int heightmode = measurespec.getmode(heightmeasurespec);
int heightsize = measurespec.getsize(heightmeasurespec);
if (heightmode != measurespec.exactly) {
heightsize = (int) (displaymetrics.widthpixels * 0.9f);
}
setmeasureddimension(widthsize, heightsize);
}
private textpaint createpaint() {
// 实例化画笔并打开抗锯齿
textpaint paint = new textpaint(paint.anti_alias_flag);
paint.setantialias(true);
paint.setstrokewidth(dptopx(mlinewidth));
paint.settextsize(dptopx(mtextsize));
return paint;
}
private float dptopx(float dp) {
return typedvalue.applydimension(typedvalue.complex_unit_dip, dp, getresources().getdisplaymetrics());
}
/**
* 基线到中线的距离=(descent+ascent)/2-descent
* 注意,实际获取到的ascent是负数。公式推导过程如下:
* 中线到bottom的距离是(descent+ascent)/2,这个距离又等于descent+中线到基线的距离,即(descent+ascent)/2=基线到中线的距离+descent。
*/
public static float gettextpaintbaseline(paint p) {
paint.fontmetrics fontmetrics = p.getfontmetrics();
return (fontmetrics.descent - fontmetrics.ascent) / 2 - fontmetrics.descent;
}
@override
protected void onsizechanged(int w, int h, int oldw, int oldh) {
super.onsizechanged(w, h, oldw, oldh);
circleradius = math.min(w / 2f, h / 2f) / max_visible_count;
}
comparator<drawtask> drawtaskcomparator = new comparator<drawtask>() {
@override
public int compare(drawtask left, drawtask right) {
float dx = math.abs(right.y) - math.abs(left.y);
if (dx > 0) {
return 1;
}
if (dx == 0) {
return 0;
}
return -1;
}
};
comparator<drawtask> drawtaskcomparatory = new comparator<drawtask>() {
@override
public int compare(drawtask left, drawtask right) {
float dx = left.y - right.y;
if (dx > 0) {
return 1;
}
if (dx == 0) {
return 0;
}
return -1;
}
};
@override
protected void ondraw(canvas canvas) {
super.ondraw(canvas);
int width = getwidth();
int height = getheight();
int id = canvas.save();
canvas.translate(width / 2f, height / 2f);
initcircle();
//前期重置,以便recycler复用
recyclerbefore(height);
//复用和移除
recycler();
//再次处理,防止view复用之后产生其他位移
recyclerafter(height);
collections.sort(drawtasks, drawtaskcomparator);
for (int i = 0; i < drawtasks.size(); i++) {
drawtasks.get(i).draw(canvas, mtextpaint);
}
drawguideline(canvas, width);
canvas.restoretocount(id);
}
private float getminyoffset() {
float miny = 0;
float offset = 0;
for (int i = 0; i < drawtasks.size(); i++) {
drawtask drawtask = drawtasks.get(i);
if (math.abs(drawtask.scaleoffset) > offset) {
miny = -drawtask.y;
offset = drawtask.scaleoffset;
}
}
return miny;
}
private void recyclerafter(int height) {
if (istoucheventup) {
float centeroffset = getminyoffset();
resetitemyoffset(height, centeroffset);
} else {
resetitemyoffset(height, 0);
}
}
private void recyclerbefore(int height) {
if (istoucheventup) {
float centeroffset = getminyoffset();
resetitemyoffset(height, centeroffset);
} else {
resetitemyoffset(height, offsety);
}
istoucheventup = false;
}
private void recycler() {
if (drawtasks.size() < (max_visible_count - 2)) return;
collections.sort(drawtasks, drawtaskcomparatory);
drawtask head = drawtasks.get(0);
drawtask tail = drawtasks.get(drawtasks.size() - 1);
int height = getheight();
if (head.y < -(height / 2f + circleradius)) {
drawtasks.remove(head);
addtocachepool(head);
head.setloopitem(null);
} else {
drawtask drawtask = getcachepool();
loopitem loopitem = head.getloopitem();
loopitem preloopitem = getpreloopitem(loopitem);
drawtask.setloopitem(preloopitem);
drawtask.y = head.y - circleradius * 2;
drawtasks.add(0, drawtask);
}
if (tail.y > (height / 2f + circleradius)) {
drawtasks.remove(tail);
addtocachepool(tail);
tail.setloopitem(null);
} else {
drawtask drawtask = getcachepool();
loopitem loopitem = tail.getloopitem();
loopitem nextloopitem = getnextloopitem(loopitem);
drawtask.setloopitem(nextloopitem);
drawtask.y = tail.y + circleradius * 2;
drawtasks.add(drawtask);
}
}
private void resetitemyoffset(int height, float scaleoffset) {
for (int i = 0; i < drawtasks.size(); i++) {
drawtask task = drawtasks.get(i);
task.y = (task.y + scaleoffset);
float ratio = math.abs(task.y) / (height / 2f);
if (ratio > 1f) {
ratio = 1f;
}
task.scaleoffset = ((10 + circleradius) * 3 / 4f) * (1 - ratio);
}
}
rectf guiderect = new rectf();
private void drawguideline(canvas canvas, int width) {
if (!isineditmode()) return;
mtextpaint.setcolor(color.black);
mtextpaint.setstyle(paint.style.fill);
int i = 0;
int counter = 0;
while (counter < max_visible_count) {
float topy = i * 2 * circleradius;
guiderect.left = -width / 2f;
guiderect.right = width / 2f;
guiderect.top = topy - 0.5f;
guiderect.bottom = topy + 0.5f;
canvas.drawrect(guiderect, mtextpaint);
counter++;
float bottomy = -i * 2 * circleradius;
if (topy == bottomy) {
i++;
continue;
}
guiderect.top = bottomy - 0.5f;
guiderect.bottom = bottomy + 0.5f;
canvas.drawrect(guiderect, mtextpaint);
counter++;
i++;
}
}
private loopitem getnextloopitem(loopitem loopitem) {
int index = loopitems.indexof(loopitem);
if (index < loopitems.size() - 1) {
return loopitems.get(index + 1);
}
return loopitems.get(0);
}
private loopitem getpreloopitem(loopitem loopitem) {
int index = loopitems.indexof(loopitem);
if (index > 0) {
return loopitems.get(index - 1);
}
return loopitems.get(loopitems.size() - 1);
}
private drawtask getcachepool() {
if (cachedrawtasks.size() > 0) {
return cachedrawtasks.remove(0);
}
drawtask drawtask = createdrawtask();
return drawtask;
}
private void addtocachepool(drawtask top) {
cachedrawtasks.add(top);
}
private void initcircle() {
if (isinit) {
return;
}
isinit = true;
list<drawtask> drawtasklist = new arraylist<>();
int i = 0;
while (drawtasklist.size() < max_visible_count) {
float topy = i * 2 * circleradius;
drawtask drawtask = new drawtask(0, topy, circleradius);
drawtasklist.add(drawtask);
float bottomy = -i * 2 * circleradius;
if (topy == bottomy) {
i++;
continue;
}
drawtask = new drawtask(0, bottomy, circleradius);
drawtasklist.add(drawtask);
i++;
}
collections.sort(drawtasklist, new comparator<drawtask>() {
@override
public int compare(drawtask left, drawtask right) {
float dx = left.y - right.y;
if (dx > 0) {
return 1;
}
if (dx == 0) {
return 0;
}
return -1;
}
});
drawtasks.clear();
if (loopitems.size() == 0) return;
for (int j = 0; j < drawtasklist.size(); j++) {
drawtasklist.get(j).setloopitem(loopitems.get(j % loopitems.size()));
}
drawtasks.addall(drawtasklist);
}
private drawtask createdrawtask() {
drawtask drawtask = new drawtask(0, 0, circleradius);
return drawtask;
}
@override
public boolean ontouchevent(motionevent event) {
int action = event.getactionmasked();
istoucheventup = false;
switch (action) {
case motionevent.action_down:
offsety = 0;
starteventx = event.getx() - getwidth() / 2f;
starteventy = event.gety() - getheight() / 2f;
return true;
case motionevent.action_move:
float eventx = event.getx();
float eventy = event.gety();
if (eventy < 0) {
eventy = 0;
}
if (eventx < 0) {
eventx = 0;
}
if (eventy > getwidth()) {
eventx = getwidth();
}
if (eventy > getheight()) {
eventy = getheight();
}
float currentx = eventx - getwidth() / 2f;
float currenty = eventy - getheight() / 2f;
float dx = currentx - starteventx;
float dy = currenty - starteventy;
if (math.abs(dx) < math.abs(dy) && math.abs(dy) >= sloptouch) {
istouchmove = true;
}
if (!istouchmove) {
break;
}
offsety = dy;
starteventx = currentx;
starteventy = currenty;
postinvalidate();
return true;
case motionevent.action_cancel:
case motionevent.action_outside:
case motionevent.action_up:
istouchmove = false;
istoucheventup = true;
offsety = 0;
log.d("eventup", "offsety=" + offsety);
invalidate();
break;
}
return super.ontouchevent(event);
}
public void setloopitems(list<loopitem> loopitems) {
this.loopitems.clear();
this.drawtasks.clear();
this.cachedrawtasks.clear();
this.isinit = false;
if (loopitems != null) {
this.loopitems.addall(loopitems);
}
postinvalidate();
}
public static class drawtask<t extends loopitem> {
private t loopitem;
private float radius;
private float x;
private float y;
private float scaleoffset = 0;
public drawtask(float x, float y, float radius) {
this.radius = radius;
this.x = x;
this.y = y;
}
public void setloopitem(t loopitem) {
this.loopitem = loopitem;
}
public void draw(canvas canvas, textpaint textpaint) {
if (loopitem == null) return;
textpaint.setcolor(loopitem.getcolor());
textpaint.setstyle(paint.style.fill);
textpaint.setshadowlayer(10, 0, 5, 0x99444444);
canvas.drawcircle(x, y, radius + scaleoffset, textpaint);
textpaint.setcolor(color.white);
textpaint.setstyle(paint.style.fill);
string text = loopitem.gettext();
float textwidth = textpaint.measuretext(text);
float baseline = gettextpaintbaseline(textpaint);
textpaint.setshadowlayer(0, 0, 0, color.transparent);
canvas.drawtext(text, -textwidth / 2, y + baseline, textpaint);
}
public t getloopitem() {
return loopitem;
}
}
public static class loopitem {
private int color;
private string text;
public loopitem(string text, int color) {
this.color = color;
this.text = text;
}
public int getcolor() {
return color;
}
public void setcolor(int color) {
this.color = color;
}
public string gettext() {
return text;
}
public void settext(string text) {
this.text = text;
}
}
}
以上就是android使用canvas 2d实现循环菜单效果的详细内容,更多关于android canvas 2d循环菜单的资料请关注代码网其它相关文章!
发表评论