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:
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:
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.
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:
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 :
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>