Home > Software design >  Android table as calendar view for whole year
Android table as calendar view for whole year

Time:08-06

I want to create a garden calendar in android for 12 months for each plant, with color bars according to sow or harvest action.

Example:

growing calendar

Each month should be divided to 3 quarters, so finally I need 36 columns for one row. (After consideration, it will be enough to have 2 halves per month, so 24 columns).

First row is for Planting inside, second for sow outside and third harvest (ignore the description on example picture).

The simpliest way is to create such an image and just show it via ImageView for each plant. But, the users will have a possibility to set their custom dates of sowing or harvesting, so the calendar should be generated accordingly, can't be fixed image.

For now I've found only solution to use TableLayout (of course programmatically, as the dates will later come from user's database):

This is my java file:

 int i;
    TableRow tbl =  findViewById(R.id.tblayout);

    for (i=1;i<=36;i  ) {
        TextView tv = new TextView(this);

        if (i>3 && i<6) {tv.setBackgroundColor(Color.RED);} else {tv.setBackgroundColor(Color.GRAY);}

        TableRow.LayoutParams paramsExample = new TableRow.LayoutParams(0, TableRow.LayoutParams.WRAP_CONTENT,1.0f);

        tv.setGravity(Gravity.CENTER);
        tv.setPadding(2, 2, 2, 2);
        tv.setLayoutParams(paramsExample);

        tbl.addView(tv);
    }

And this is my XML layout:

<TableLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#ffffff"
    android:shrinkColumns="*"
    android:stretchColumns="*">

    <TableRow
        android:id="@ id/tblayout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

    </TableRow>

So basically I am generating TextViews in TableRow and if the i variable will met the date requirements, the textView will have a background color.

Currently my result is now:

enter image description here

which is close to my requirements (for now just one row), but still I am not sure if this is a good solution to have so many textViews, then each month should have a vertical divider (not sure how to do it in textView - maybe generate another View after each textView) and possibly second color or a dark line that will indicate today's date.

Can you please help if there is any simplier solution or if I can go this way? Or to use some kind of progressbar, but then the grid in the background will be gone.

CodePudding user response:

I think that using a TableLayout and Textviews is the wrong way to go.

If you are not going to put text in the coloured rounded rectangles, then it would be more efficient to use a plain views to set the background of.

But also TableLayout have a lot of code to size columns to show the content and then you would have to set grow and shrink on columns to adjust the size to match the screen size.

You would also have to have a drawable for start of the rounded rectangles and an end drawable and then would would have to work out to put in each cell a start, middle or end background.

The vertical dividers would really have to be there own column very thin column (though best to be non growable/shrinkable) so they don't too big or same.

This all adds to a lot of complexity in constructing the table and then complexity in what it has to then calculate to draw it.

I think a much better solution in to create your own custom view that dynamically draws the right size of rounded rectangles and text. A few calculations based on the view's size will tell you how to divide it up in to 12 months and then the quarter (or half) of a month.

A custom view would perform much better than a TableView and be simpler to implement

e.g. enter image description here

public class GardenCalendarView extends View {

    private Plant plant;

    private Rect bounds;
    private RectF boundsF;
    private Paint boundPaint;
    private Paint subLinePaint;
    private Paint textPaint;
    private Paint barPaint;

    private RectF startInsideRect;
    private RectF transplantRect;
    private RectF sowOutsideRect;

    private static final int MARGIN = 40;
    private static final int PADDING = 5;

    public GardenCalendarView(Context context) {
        super(context);
        init(context);
    }

    public GardenCalendarView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    public GardenCalendarView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }

    public void setPlant(Plant plant) {
        this.plant = plant;
        invalidate();
    }

    private void init(Context context) {
        bounds = new Rect();
        boundsF = new RectF();
        boundPaint = new Paint();
        boundPaint.setColor(Color.RED);
        boundPaint.setStyle(Paint.Style.STROKE);

        boundPaint.setAntiAlias(true);
        boundPaint.setStrokeWidth(1);
        boundPaint.setStrokeJoin(Paint.Join.ROUND);
        boundPaint.setStrokeCap(Paint.Cap.ROUND);

        subLinePaint = new Paint(boundPaint);
        subLinePaint.setColor(Color.GREEN);

        textPaint = new Paint(boundPaint);
        textPaint.setColor(Color.BLACK);
        textPaint.setTextSize(Utils.dpToPx(10f, context));

        startInsideRect = new RectF();
        transplantRect = new RectF();
        sowOutsideRect = new RectF();

        barPaint = new Paint();
        barPaint.setAntiAlias(true);
        barPaint.setStyle(Paint.Style.FILL_AND_STROKE);

    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (plant != null) {
            paint(canvas);
        }
    }

    private void paint(Canvas canvas) {
        getDrawingRect(bounds);

        Utils.reduceRectBy(bounds, MARGIN);
        canvas.drawRect(bounds, boundPaint);

        float partWidth = bounds.width() / 36f;

        // draw vertical lines
        for (int i = 0; i< 36;   i) {
            if (i % 3 == 0) {
                canvas.drawLine(
                        bounds.left   partWidth *i,
                        bounds.top,
                        bounds.left   partWidth *i,
                        bounds.bottom, boundPaint);

                //Paint month label
                String month = Plant.MONTHS[i/3].toString();
                canvas.drawText(month, bounds.left   partWidth *i, bounds.top - 4, textPaint);

            } else {
                canvas.drawLine(
                        bounds.left   partWidth *i,
                        bounds.top,
                        bounds.left   partWidth *i,
                        bounds.bottom, subLinePaint);
            }
        }

        float partHeight = bounds.height() / 3f;

        float monthWidth = partWidth*3;

        // draw start inside bar
        startInsideRect.left = bounds.left   (plant.startInside.startMonth - 1)* monthWidth;
        startInsideRect.right = bounds.left   (plant.startInside.endMonth - 1)* monthWidth;
        startInsideRect.top = bounds.top   0* partHeight   PADDING;
        startInsideRect.bottom = bounds.top   1* partHeight - PADDING;

        barPaint.setColor(plant.startInside.color);
        canvas.drawRect(startInsideRect, barPaint);

        // draw transplant bar
        transplantRect.left = bounds.left   (plant.transplant.startMonth - 1)* monthWidth;
        transplantRect.right = bounds.left   (plant.transplant.endMonth - 1)* monthWidth;
        transplantRect.top = bounds.top   1* partHeight   PADDING;
        transplantRect.bottom = bounds.top   2* partHeight - PADDING;

        barPaint.setColor(plant.transplant.color);
        canvas.drawRect(transplantRect, barPaint);

        // draw sow outside bar
        sowOutsideRect.left = bounds.left   (plant.sowOutside.startMonth - 1)* monthWidth;
        sowOutsideRect.right = bounds.left   (plant.sowOutside.endMonth - 1)* monthWidth;
        sowOutsideRect.top = bounds.top   2* partHeight   PADDING;
        sowOutsideRect.bottom = bounds.top   3* partHeight - PADDING;

        barPaint.setColor(plant.sowOutside.color);
        canvas.drawRect(sowOutsideRect, barPaint);
    }
}

public class Plant {

    public String name;

    public Bar startInside;
    public Bar transplant;
    public Bar sowOutside;

    public static String[] MONTHS = new String[]{
            "JAN", "FEB", "MAR", "APR", "MAY", "JUN",
            "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"
    };

    public Plant(String name, Bar startInside, Bar transplant, Bar sowOutside) {
        this.name = name;
        this.startInside = startInside;
        this.transplant = transplant;
        this.sowOutside = sowOutside;
    }
}

public class Bar {

    public int startMonth;
    public int endMonth;
    public int color;

    public Bar(int start, int end, int color) {
        this.startMonth = start;
        this.endMonth = end;
        this.color = color;
    }
}

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        GardenCalendarView gardenCalendar = findViewById(R.id.gardenCalendar);

        Bar startInsideBar = new Bar(2, 5, Color.MAGENTA);
        Bar transplantBar = new Bar(3, 6, Color.BLUE);
        Bar sowOutsideBar = new Bar(4, 7, Color.YELLOW);

        Plant bacopaPlant = new Plant(
                "Bacopa",
                startInsideBar,
                transplantBar,
                sowOutsideBar);
        
        gardenCalendar.setPlant(bacopaPlant);
    }
}

public class Utils {

    public static void reduceRectBy(Rect rect, int dx) {
        rect.left  = dx;
        rect.top  = dx;
        rect.right -= dx;
        rect.bottom -= dx;
    }

    public static float dpToPx(float dp, Context context) {
        return TypedValue.applyDimension(
                TypedValue.COMPLEX_UNIT_DIP,
                dp,
                context.getResources().getDisplayMetrics()
        );
    }
}

<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    tools:context=".MainActivity">

    <com.abdo.mycalendar.GardenCalendarView
        android:id="@ id/gardenCalendar"
        android:layout_width="match_parent"
        android:layout_height="80dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

Edit: Optimize drawing by performing size related calculations in onSizeChanged() instead of in onDrow()

public class GardenCalendarView extends View {

    private Plant plant;

    private Rect bounds;
    private Paint boundPaint;
    private Paint subLinePaint;
    private Paint textPaint;
    private Paint barPaint;

    private RectF startInsideRect;
    private RectF transplantRect;
    private RectF sowOutsideRect;

    private static final int MARGIN = 40;
    private static final int PADDING = 5;

    private float partWidth;

    public GardenCalendarView(Context context) {
        super(context);
        init(context);
    }

    public GardenCalendarView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    public GardenCalendarView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }

    public void setPlant(Plant plant) {
        this.plant = plant;
        invalidate();
    }

    private void init(Context context) {
        bounds = new Rect();
        boundPaint = new Paint();
        boundPaint.setColor(Color.RED);
        boundPaint.setStyle(Paint.Style.STROKE);

        boundPaint.setAntiAlias(true);
        boundPaint.setStrokeWidth(1);
        boundPaint.setStrokeJoin(Paint.Join.ROUND);
        boundPaint.setStrokeCap(Paint.Cap.ROUND);

        subLinePaint = new Paint(boundPaint);
        subLinePaint.setColor(Color.GREEN);

        textPaint = new Paint(boundPaint);
        textPaint.setColor(Color.BLACK);
        textPaint.setTextSize(Utils.dpToPx(10f, context));

        startInsideRect = new RectF();
        transplantRect = new RectF();
        sowOutsideRect = new RectF();

        barPaint = new Paint();
        barPaint.setAntiAlias(true);
        barPaint.setStyle(Paint.Style.FILL_AND_STROKE);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (plant != null) {
            paint(canvas);
        }
    }

    private void paint(Canvas canvas) {

        canvas.drawRect(bounds, boundPaint);

        // draw vertical lines
        for (int i = 0; i < 36;   i) {
            if (i % 3 == 0) {
                canvas.drawLine(
                        bounds.left   partWidth * i,
                        bounds.top,
                        bounds.left   partWidth * i,
                        bounds.bottom, boundPaint
                );

                //Paint month label
                canvas.drawText(
                        Plant.MONTHS[i / 3], 
                        bounds.left   partWidth * i, 
                        bounds.top - 4, textPaint);

            } else {
                canvas.drawLine(
                        bounds.left   partWidth * i,
                        bounds.top,
                        bounds.left   partWidth * i,
                        bounds.bottom, subLinePaint);
            }
        }

        // draw start inside bar
        barPaint.setColor(plant.startInside.color);
        canvas.drawRect(startInsideRect, barPaint);

        // draw transplant bar
        barPaint.setColor(plant.transplant.color);
        canvas.drawRect(transplantRect, barPaint);

        // draw sow outside bar
        barPaint.setColor(plant.sowOutside.color);
        canvas.drawRect(sowOutsideRect, barPaint);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {

        int xpad = getPaddingLeft()   getPaddingRight();
        int ypad = getPaddingTop()   getPaddingBottom();

        int ww = w - xpad;
        int hh = h - ypad;

        bounds.set(0, 0, ww, hh);

        Utils.reduceRectBy(bounds, MARGIN);

        partWidth = bounds.width() / 36f;

        float partHeight = bounds.height() / 3f;

        float monthWidth = partWidth * 3;

        startInsideRect.set(
                bounds.left   (plant.startInside.startMonth - 1) * monthWidth,
                bounds.top   0 * partHeight   PADDING,
                bounds.left   (plant.startInside.endMonth - 1) * monthWidth,
                bounds.top   1 * partHeight - PADDING
        );

        transplantRect.set(
                bounds.left   (plant.transplant.startMonth - 1) * monthWidth,
                bounds.top   1 * partHeight   PADDING,
                bounds.left   (plant.transplant.endMonth - 1) * monthWidth,
                bounds.top   2 * partHeight - PADDING
        );

        sowOutsideRect.set(
                bounds.left   (plant.sowOutside.startMonth - 1) * monthWidth,
                bounds.top   2 * partHeight   PADDING,
                bounds.left   (plant.sowOutside.endMonth - 1) * monthWidth,
                bounds.top   3 * partHeight - PADDING
        );
    }
}
  • Related