一、前语

循环菜单有很多种自界说办法,前面一篇《Android Canvas 3D 视图构建》咱们找到了界说3D视图的办法,还有咱们能够利用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.4 全部代码

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;
        }
    }
}