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

 更新时间:2024年01月24日 08:38:29   作者:时光少年  
循环菜单有很多种自定义方式,我们可以利用ViewPager或者RecyclerView + CarouselLayoutManager 或者RecyclerView + PageSnapHelper来实现这种效果,今天我们使用Canvas 2D来实现这种效果,感兴趣的朋友可以参考下

一、前言

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

相关文章

最新评论