当前位置: 代码网 > it编程>App开发>Android > Android使用Canvas 2D实现循环菜单效果

Android使用Canvas 2D实现循环菜单效果

2024年05月18日 Android 我要评论
一、前言循环菜单有很多种自定义方式,我们可以利用viewpager或者recyclerview + carousellayoutmanager 或者recyclerview + pagesnaphel

一、前言

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

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2025  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com