androidcalendarandroid-tablelayout

Android table as calendar view for whole year


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.


Solution

  • I think the best way is to create your custom view. Here is an example close to what you want, you can customize it according to your needs:

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

    Edit2: To be able to update the chart at runtime and also draw any number of bars :

    enter image description here

    GardenCalendarView.java

    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 barRect;
    
        private static final int MARGIN = 40;
        private static final int PADDING = 5;
    
        private float partWidth;
    
        private Rect textBound;
    
        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));
            textPaint.setTypeface(Typeface.MONOSPACE);
    
            barRect = new RectF();
    
            barPaint = new Paint();
            barPaint.setAntiAlias(true);
            barPaint.setStyle(Paint.Style.FILL_AND_STROKE);
    
            textBound = new Rect();
            textPaint.getTextBounds("JAN", 0, 3, textBound);
        }
    
        @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);
            partWidth = bounds.width() / 36f;
            float partHeight = bounds.height() / 3f;
            float monthWidth = partWidth * 3;
    
            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);
                }
            }
    
            for (Bar bar: plant.bars) {
                switch (bar.type) {
                    case Bar.STARTINDIDE: { // first row
                        barRect.set(
                                bounds.left + (bar.startMonth - 1) * monthWidth,
                                bounds.top + 0 * partHeight + PADDING,
                                bounds.left + (bar.endMonth - 1) * monthWidth,
                                bounds.top + 1 * partHeight - PADDING
                        );
                        break;
                    }
                    case Bar.TRANSPLANT: { //second row
                        barRect.set(
                                bounds.left + (bar.startMonth - 1) * monthWidth,
                                bounds.top + 1 * partHeight + PADDING,
                                bounds.left + (bar.endMonth - 1) * monthWidth,
                                bounds.top + 2 * partHeight - PADDING
                        );
                        break;
                    }
                    case Bar.SOWOUTSIDE: { //third row
                        barRect.set(
                                bounds.left + (bar.startMonth - 1) * monthWidth,
                                bounds.top + 2 * partHeight + PADDING,
                                bounds.left + (bar.endMonth - 1) * monthWidth,
                                bounds.top + 3 * partHeight - PADDING
                        );
                        break;
                    }
                }
    
                barPaint.setColor(bar.color);
                canvas.drawRect(barRect, barPaint);
            }
        }
    }
    

    Bar.java

    public class Bar {
        public static final int STARTINDIDE = 0;
        public static final int TRANSPLANT = 1;
        public static final int SOWOUTSIDE = 2;
    
        public int startMonth;
        public int endMonth;
        public int color;
        public int type;
    
        public Bar(int start, int end, int color, int type) {
            this.startMonth = start;
            this.endMonth = end;
            this.color = color;
            this.type = type;
        }
    }
    

    Plant.java

    public class Plant {
        public String name;
        public List<Bar> bars;
    
        public static String[] MONTHS = new String[]{
                "JAN", "FEB", "MAR", "APR", "MAY", "JUN",
                "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"
        };
    
        public Plant(String name) {
            this.name = name;
            this.bars = new ArrayList<>();
        }
    
        public Plant(String name, List<Bar> bars) {
            this.name = name;
            this.bars = bars;
        }
    
        public void addBar(Bar bar){
            bars.add(bar);
        }
    
        public void deleteBar(Bar bar){
            bars.remove(bar);
        }
    }
    

    MainActivity.java

    public class MainActivity extends AppCompatActivity {
    
        private int[] COLORS = {Color.RED, Color.GREEN, Color.BLUE};
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            GardenCalendarView gardenCalendar = findViewById(R.id.gardenCalendar);
    
            Spinner spinner = findViewById(R.id.spinner);
            EditText startEditText = findViewById(R.id.startEditText);
            EditText endEditText = findViewById(R.id.endEditText);
            Button addButton = findViewById(R.id.addButton);
    
            // Create an ArrayAdapter using the string array and a default spinner layout
            ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this,
                    R.array.parts, android.R.layout.simple_spinner_item);
            // Specify the layout to use when the list of choices appears
            adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
            // Apply the adapter to the spinner
            spinner.setAdapter(adapter);
    
            Plant bacopaPlant = new Plant("Bacopa");
            gardenCalendar.setPlant(bacopaPlant);
    
            addButton.setOnClickListener(v -> {
                // we suppose here data are valid !!
                int start = Integer.parseInt(startEditText.getText().toString());
                int end = Integer.parseInt(endEditText.getText().toString());
                int selectedItem = spinner.getSelectedItemPosition();
                Bar bar = new Bar(start, end, COLORS[selectedItem], selectedItem);
                bacopaPlant.addBar(bar);
    
                gardenCalendar.setPlant(bacopaPlant);
    
    
                //clear values
                startEditText.setText("");
                endEditText.setText("");
    
                Toast.makeText(this, "Bar added successfully !", Toast.LENGTH_SHORT).show();
            });
    
        }
    }
    

    arrays.xml

    <?xml version="1.0" encoding="utf-8"?>
    <resources>
    
        <string-array name="parts">
            <item>Start Inside</item>
            <item>Transplant</item>
            <item>Sow outside</item>
        </string-array>
    
    </resources>
    

    activity_main.xml

    <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="match_parent"
        tools:context=".MainActivity">
    
        <EditText
            android:id="@+id/startEditText"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:hint="Start"
            android:layout_margin="8dp"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    
        <EditText
            android:id="@+id/endEditText"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:hint="End"
            android:layout_margin="8dp"
            app:layout_constraintStart_toEndOf="@id/startEditText"
            app:layout_constraintTop_toTopOf="@id/startEditText"
            app:layout_constraintBottom_toBottomOf="@id/startEditText"/>
    
        <Spinner
            android:id="@+id/spinner"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="8dp"
            app:layout_constraintBottom_toBottomOf="@id/addButton"
            app:layout_constraintEnd_toStartOf="@id/addButton"
            app:layout_constraintTop_toTopOf="@id/addButton"
            app:layout_constraintVertical_bias="0.666" />
    
        <Button
            android:id="@+id/addButton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Add Bar"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toTopOf="parent"/>
    
    
        <com.abdo.mycall.GardenCalendarView
            android:id="@+id/gardenCalendar"
            android:layout_width="match_parent"
            android:layout_height="100dp"
            android:layout_marginTop="24dp"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/addButton" />
    
    </androidx.constraintlayout.widget.ConstraintLayout>