一、前言
循环菜单有很多种自定义方式,我们可以利用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循环菜单的资料请关注代码网其它相关文章!
发表评论