/*
TFT Date and Time Picker
Date and Time picker for touch TFT LCD displays that let users
select a date and a specific time value to adjust TimeLib clock
Parts needed:
Ardunio UNO
AZ-Delivery 2.4 TFT LCD Touch Display Arduino Shield
This example code is in the public domain.
Modified 12 10 2020
By Enrique Albertos
https://www.hackster.io/javagoza/arduino-date-and-time-picker-daa2fe
*/
#include "MCUFRIEND_kbv.h"
#include <TouchScreen.h>
#include <TimeLib.h>
MCUFRIEND_kbv tft;
#define LOWFLASH (defined(__AVR_ATmega328P__) && defined(MCUFRIEND_KBV_H_))
#define countof(a) (sizeof(a) / sizeof(a[0]))
#define PRIMARY_COLOR 0x4A11
#define PRIMARY_LIGHT_COLOR 0x7A17
#define PRIMARY_DARK_COLOR 0x4016
#define PRIMARY_TEXT_COLOR 0x7FFF
// WIDGETS ID
#define HOUR_ID 0
#define TIME_SEPARATOR1_ID 1
#define MINUTE_ID 2
#define TIME_SEPARATOR2_ID 3
#define SECOND_ID 4
#define YEAR_ID 5
#define DATE_SEPARATOR1_ID 6
#define MONTH_ID 7
#define DATE_SEPARATOR2_ID 8
#define DAY_ID 9
#define TIME_SET_ID 10
#define TIME_CANCEL_ID 11
#define TIME_BUTTON_ID 12
#define DATE_BUTTON_ID 13
#define WIDGETS_NO 14
// Aligment modes for buttons and labels
#define ALIGN_LEFT 0
#define ALIGN_CENTER 1
#define ALIGN_RIGHT 2
// base size for up and down spinner triangles
#define UP_DOWN_WIDTH 24
// Touch screen presure threshold
#define MINPRESSURE 200
#define MAXPRESSURE 1000
// UI Items IDs linked to date spinner controls
const uint16_t dateSpinnerWidgets[] = {YEAR_ID, DATE_SEPARATOR1_ID, MONTH_ID, DATE_SEPARATOR2_ID, DAY_ID};
// UI Items IDs linked to time spinner controls
const uint16_t timeSpinnerWidgets[] = {HOUR_ID, TIME_SEPARATOR1_ID, MINUTE_ID, TIME_SEPARATOR2_ID, SECOND_ID};
// Touch screen calibration
const int16_t XP = 8, XM = A2, YP = A3, YM = 9; //240x320 ID=0x9341
const int16_t TS_LEFT = 122, TS_RT = 929, TS_TOP = 77, TS_BOT = 884;
char format2d[] = "%02d";
char format4d[] = "%04d";
char colon[] = ":";
char dash[] = "-";
char SET_LABEL[] = "SET";
char CANCEL_LABEL[] = "CANCEL";
char DEFAULT_DATE[] = "2020-09-01";
char DEFAULT_TIME[] = "12:00:00";
const TouchScreen ts = TouchScreen(XP, YP, XM, YM, 300);
typedef struct RectRegion {
int16_t x1, x2, y1, y2;
} RectRegion;
typedef struct DialogSize_type {
int16_t x, y, width, height;
} DialogSize_type;
// Defintion of an UI Element
struct UiElement_type;
// Action callback to respond to events
typedef void (*Action)(struct UiElement_type* caller, int x, int y) ;
// Paint callback to respond to paint messages
typedef void (*Paint)(struct UiElement_type* self);
typedef struct UiElement_type {
int16_t x; // horizontal position of the label
int16_t y; // vertical position of the label
int16_t width; // widht of the label
int16_t height; // height of the label
uint16_t fontsize; // size of the font label
bool visible; // is visble the element
int16_t value; // int value of the element
int16_t minValue; // minimum admisible value
int16_t maxValue; // maximum admisible value
bool enabled; // is enabled
bool pressed; // is pressed
char* text; // label text
char* format; // format for the value
Action onTap; // callback when tapped
Paint paint; // paint callback
RectRegion hitbox; // hit box region to check tap is inside the element
uint16_t foreColor; // forground color for the element
uint16_t backgroundColor; // background color for the element
} UiElement_type;
// array of ui elements
UiElement_type uiWidgets[WIDGETS_NO];
DialogSize_type dialogSize;
//////////////////////////////////////////////////////////////
// ARDUINO SETUP
//////////////////////////////////////////////////////////////
void setup()
{
initTft(tft);
dialogSize = {0, 0, tft.width(), tft.height()};
clearDialog(dialogSize);
createDateTimePickerDialog(dialogSize);
paintDateTimePickerDialog();
}
//////////////////////////////////////////////////////////////
// ARDUINO LOOP
//////////////////////////////////////////////////////////////
int secondsOld = -1;
int selection = -1;
void loop(void)
{
selection = readUiSelection(selection);
const int secondsNew = second();
if ( !(secondsNew == secondsOld)) {
refreshTime();
}
secondsOld = secondsNew;
}
//////////////////////////////////////////////////////////////
// TFT SETUP
//////////////////////////////////////////////////////////////
void initTft(MCUFRIEND_kbv &tft) {
tft.reset();
uint16_t ID = tft.readID();
tft.begin(ID);
tft.setRotation(2);
}
//////////////////////////////////////////////////////////////
// Screen Painting methods
//////////////////////////////////////////////////////////////
/**
Print a text in forecolor over a filled box with background color.
Rectangle size is calculated to include the whole text without margins
@param x horizontal coordinate in points left upper corner
@param y vertical coordinate in points left upper corner
@param fontsize font size of the text to print
@param foreColor forecolor of the text to print
@param backgroundColor color of the filled rect
@return void
*/
void drawBoxedString(const uint16_t x, const uint16_t y, const char* string, const uint16_t fontsize, const uint16_t foreColor, const uint16_t backgroundColor) {
tft.setTextSize(fontsize);
int16_t x1, y1;
uint16_t w, h;
tft.getTextBounds(string, x, y, &x1, &y1, &w, &h);
tft.fillRect(x, y, w, h, backgroundColor);
tft.setCursor(x, y);
tft.setTextColor(foreColor);
tft.print(string);
}
/**
Paint an ui label element
@param label pointer to an ui label type element to be drawn in the screen
@return void
*/
void paintLabel(UiElement_type *label) {
if (label->visible) {
if (label->pressed) {
drawBoxedString(label->x, label->y, label->text, label->fontsize, label->backgroundColor, label->foreColor);
} else {
drawBoxedString(label->x, label->y, label->text, label->fontsize, label->foreColor, label->backgroundColor);
}
}
}
/**
Paint an ui spinner element
@param label pointer to an ui spinner type element to be drawn in the screen
@return void
*/
void paintSpinner(UiElement_type * spinner) {
tft.setTextSize(spinner->fontsize);
int16_t x1, y1;
uint16_t wChar, hChar;
char buffer[5];
snprintf(buffer, sizeof(buffer), spinner->format, spinner->value);
spinner->text = buffer;
tft.getTextBounds(buffer, spinner->x, spinner->y, &x1, &y1, &wChar, &hChar);
paintLabel(spinner);
int xUpDown = spinner->x + (wChar - UP_DOWN_WIDTH) / 2;
tft.fillTriangle( xUpDown + 2, spinner->y - 4,
xUpDown + UP_DOWN_WIDTH / 2, spinner->y - UP_DOWN_WIDTH / 2 ,
xUpDown + UP_DOWN_WIDTH - 3, spinner->y - 4 ,
spinner->foreColor);
tft.fillTriangle( xUpDown + 2 , spinner->y + UP_DOWN_WIDTH + 3,
xUpDown + UP_DOWN_WIDTH / 2, spinner->y + UP_DOWN_WIDTH + UP_DOWN_WIDTH / 2 ,
xUpDown + UP_DOWN_WIDTH - 2 , spinner->y + UP_DOWN_WIDTH + 3,
spinner->foreColor);
}
/**
Paint all the visible ui elements on the screen
@param label pointer to an ui spinner type element to be drawn in the screen
@return void
*/
void paintDateTimePickerDialog(void) {
for (int i = 0; i < WIDGETS_NO ; i++) {
if (uiWidgets[i].visible == true) {
uiWidgets[i].paint(&uiWidgets[i]);
} else {
tft.fillRect(uiWidgets[i].hitbox.x1,
uiWidgets[i].hitbox.y1,
uiWidgets[i].hitbox.x2 - uiWidgets[i].hitbox.x1,
uiWidgets[i].hitbox.y2 - uiWidgets[i].hitbox.y1, uiWidgets[i].backgroundColor);
}
}
}
/**
Clear the screen to the default backgrounds
@param void
@return void
*/
void clearDialog(DialogSize_type dialogSize) {
tft.fillRect(dialogSize.x, dialogSize.y, dialogSize.width, dialogSize.height, PRIMARY_DARK_COLOR );
tft.fillRect(dialogSize.x, dialogSize.y, dialogSize.width, 30, PRIMARY_COLOR);
}
//////////////////////////////////////////////////////////////
// UI ELEMENTS CREATION
/////////////////////////////////////////////////////////////
/**
Create a label widget neither nor editable not tapable
@param label the ui element
@param x horizontal coordinate in points left upper corner
@param y vertical coordinate in points left upper corner
@param text label text to print
@param fontsize font size of the text to print
@return void
*/
void createLabelWidget(UiElement_type * label, const int16_t x , const int16_t y, char * text, const uint16_t fontsize) {
tft.setTextSize(fontsize);
int16_t x1, y1;
uint16_t wChar, hChar;
tft.getTextBounds(text, 0, 0, &x1, &y1, &wChar, &hChar);
label->x = x;
label->y = y;
label->width = wChar;
label->height = hChar;
label->visible = true;
label->value = 0;
label->minValue = 0;
label->maxValue = 0;
label->text = text;
label->fontsize = fontsize;
label->paint = paintLabel;
label->hitbox.x1 = x;
label->hitbox.y1 = y;
label->hitbox.x2 = x + wChar;
label->hitbox.y2 = y + hChar;
label->foreColor = PRIMARY_TEXT_COLOR;
label->backgroundColor = PRIMARY_DARK_COLOR;
label->onTap = NULL ;
label->enabled = false;
}
/**
Create a spinner widget editable and tapable
@param spinner the ui element
@param x horizontal coordinate in points left upper corner
@param y vertical coordinate in points left upper corner
@param fontsize font size of the text to print
@param value current value
@param minValue minimum settable value included
@param maxValue maximum settable value included
@param format c format string to format value as string
@param onTap onTap event callback Action function pointer
@return void
*/
void createSpinnerWidget(UiElement_type* spinner, const int16_t x ,
const int16_t y, const uint16_t fontsize, const int16_t value,
const int16_t minValue, const int16_t maxValue, char* format, Action onTap) {
tft.setTextSize(fontsize);
int16_t x1, y1;
uint16_t wChar, hChar;
char buffer[5];
snprintf(buffer, sizeof(buffer), format, value);
spinner->text = buffer;
tft.getTextBounds(buffer, x, y, &x1, &y1, &wChar, &hChar);
spinner->x = x;
spinner->y = y;
spinner->visible = true;
spinner->value = value;
spinner->minValue = minValue;
spinner->maxValue = maxValue;
spinner->enabled = true;
spinner->onTap = onTap ;
spinner->fontsize = fontsize;
spinner->paint = paintSpinner;
spinner->format = format;
spinner->width = wChar;
spinner->height = hChar;
spinner->hitbox.x1 = x;
spinner->hitbox.y1 = y - hChar;
spinner->hitbox.x2 = x + wChar;
spinner->hitbox.y2 = y + 2 * hChar;
spinner->foreColor = PRIMARY_TEXT_COLOR;
spinner->backgroundColor = PRIMARY_DARK_COLOR;
}
/**
Create a button widget tapable
@param button the ui element
@param x horizontal coordinate in points left upper corner
@param y vertical coordinate in points left upper corner
@param fontsize font size of the text to print
@param text text value to print
@param onTap onTap event callback Action function pointer
@param backgroundColor backgrouncolor
@return void
*/
void createButtonWidget(UiElement_type* button, const int16_t x ,
const int16_t y, char* text, const uint16_t fontsize, Action onTap,
const uint16_t backgroundColor) {
createLabelWidget( button, x , y, text, fontsize);
button->onTap = onTap ;
button->enabled = true;
button->backgroundColor = backgroundColor;
}
//////////////////////////////////////////////////////////////
// UI WIDGETS
/////////////////////////////////////////////////////////////
/**
Create a time selector widget, creates the five elements of the widget
hour spinner
separator label 1
minute spinner
separator label 2
second spinner
@param x horizontal coordinate in points left upper corner
@param y vertical coordinate in points left upper corner
@param fontsize font size of the text to print
@return void
*/
void createTimeSelectorWidget(const int16_t x , const int16_t y, uint16_t fontsize) {
int xOffset = 0 ;
createSpinnerWidget(&uiWidgets[HOUR_ID], x , y, fontsize, 12, 0, 23, format2d, &spinnerOnTapEvent);
xOffset = xOffset + uiWidgets[HOUR_ID].width;
createLabelWidget(&uiWidgets[TIME_SEPARATOR1_ID], x + xOffset, y, colon, fontsize);
xOffset = xOffset + uiWidgets[TIME_SEPARATOR1_ID].width;
createSpinnerWidget(&uiWidgets[MINUTE_ID], x + xOffset , y, fontsize, 0, 0, 59, format2d, &spinnerOnTapEvent);
xOffset += uiWidgets[MINUTE_ID].width;
createLabelWidget(&uiWidgets[TIME_SEPARATOR2_ID], x + xOffset, y, colon, fontsize);
xOffset += uiWidgets[TIME_SEPARATOR2_ID].width;
createSpinnerWidget(&uiWidgets[SECOND_ID], x + xOffset , y, fontsize, 0, 0, 59, format2d, &spinnerOnTapEvent);
}
/**
Create a date selector widget, creates the five elements of the widget
year spinner
separator label 1
month spinner
separator label 2
day spinner
@param x horizontal coordinate in points left upper corner
@param y vertical coordinate in points left upper corner
@param fontsize font size of the text to print
@return void
*/
void createDateSelectorWidget(const int16_t x , const int16_t y, uint16_t fontsize) {
int xOffset = 0 ;
createSpinnerWidget(&uiWidgets[YEAR_ID], x , y, fontsize, 2020, 2020, 2099, format4d, &spinnerOnTapEvent);
xOffset += uiWidgets[YEAR_ID].width;
createLabelWidget(&uiWidgets[DATE_SEPARATOR1_ID], x + xOffset, y, dash, fontsize);
xOffset += uiWidgets[DATE_SEPARATOR1_ID].width;
createSpinnerWidget(&uiWidgets[MONTH_ID], x + xOffset , y, fontsize, 1, 1, 12, format2d, &spinnerOnTapEvent);
xOffset += uiWidgets[MONTH_ID].width;
createLabelWidget(&uiWidgets[DATE_SEPARATOR2_ID], x + xOffset , y, dash, fontsize);
xOffset += uiWidgets[DATE_SEPARATOR2_ID].width;
createSpinnerWidget(&uiWidgets[DAY_ID], x + xOffset , y, fontsize, 1, 1, 31, format2d, &spinnerOnTapEvent);
}
/**
Create a button element aligned. LEFT, CENTER or RIGHT
@param id index of the ui element to use
@param labelText test to print
@param onTapEvent on tap event action callback function pointer
@param fontsize font size of the text to print
@param yBottom y vertical coordinate in points lower corners
@param align where to align the button
@param backgroundColor background color of the button
@return void
*/
void createAlignedButtonWidget(const int16_t id, char* labelText, Action onTapEvent, const uint16_t fontsize, const int16_t yBottom, const uint16_t align, const uint16_t backgroundColor) {
tft.setTextSize(fontsize);
int16_t x1, y1;
uint16_t wChar, hChar;
tft.getTextBounds(labelText, 0, 0, &x1, &y1, &wChar, &hChar);
int16_t x, y;
y = yBottom - hChar;
if (align == ALIGN_RIGHT) {
x = dialogSize.width - wChar ;
} else if (align == ALIGN_LEFT) {
x = 0 ;
} else {
x = (dialogSize.width - wChar) / 2 ;
}
createButtonWidget(&uiWidgets[id], x, y, labelText, fontsize, onTapEvent, backgroundColor );
}
//////////////////////////////////////////////////////////////
// UI DIALOG
/////////////////////////////////////////////////////////////
void createDateTimePickerDialog(DialogSize_type dialogSize)
{
const int fontsize = 3;
tft.setTextSize(fontsize);
int16_t x1, y1;
uint16_t wChar, hChar;
tft.getTextBounds("0", 0, 0, &x1, &y1, &wChar, &hChar);
createTimeSelectorWidget((dialogSize.width - wChar * 8) / 2, dialogSize.height / 2 - hChar * 3, fontsize );
createDateSelectorWidget((dialogSize.width - wChar * 10) / 2, dialogSize.height / 2 - hChar * 3 + 100, fontsize);
createAlignedButtonWidget(TIME_SET_ID, SET_LABEL, setButtonOnTapEvent, 3, dialogSize.height, ALIGN_RIGHT, PRIMARY_DARK_COLOR );
createAlignedButtonWidget(TIME_CANCEL_ID, CANCEL_LABEL, cancelButtonOnTapEvent, 3, dialogSize.height, ALIGN_LEFT, PRIMARY_DARK_COLOR );
createAlignedButtonWidget(TIME_BUTTON_ID, DEFAULT_TIME, timeButtonOnTapEvent, 2, 20, ALIGN_RIGHT, PRIMARY_COLOR );
createAlignedButtonWidget(DATE_BUTTON_ID, DEFAULT_DATE, timeButtonOnTapEvent, 2, 20, ALIGN_LEFT, PRIMARY_COLOR );
}
//////////////////////////////////////////////////////////////
// CHANGE VISIBILITY METHODS
//////////////////////////////////////////////////////////////
/**
Changes the visible state for all the date spinner components
@param visible the new state
@return void
*/
void setDateSpinnerVisible(const bool visible) {
for (uint16_t i = 0; i < countof(dateSpinnerWidgets); i++) {
uiWidgets[dateSpinnerWidgets[i]].visible = visible;
}
}
/**
Changes the visible state for all the time spinner components
@param visible the new state
@return void
*/
void setTimeSpinnerVisible(const bool visible) {
for (uint16_t i = 0; i < countof(timeSpinnerWidgets); i++) {
uiWidgets[timeSpinnerWidgets[i]].visible = visible;
}
}
/**
Changes the visible state for cancel button element
@param visible the new state
@return void
*/
void setCancelButtonVisible(const bool visible) {
uiWidgets[TIME_CANCEL_ID].visible = visible;
}
/**
Changes the visible state for set button element
@param visible the new state
@return void
*/
void setSetButtonVisible(const bool visible) {
uiWidgets[TIME_SET_ID].visible = visible;
}
//////////////////////////////////////////////////////////////
// TIME AND DATE REFRESH METHODS
//////////////////////////////////////////////////////////////
/**
Returns the current time formatted as hh:mm:ss
@param void
@return the formateted current time
*/
char* strTime(void) {
static char ret[20]; //local static variable
snprintf_P(ret, countof(ret), PSTR("%02u:%02u:%02u"), hour(), minute(), second());
return ret;
}
/**
Returns the current date formatted as yyyy-mm-dd
@param void
@return the formateted current date
*/
char* strDate(void) {
static char ret[20]; //local static variable
snprintf_P(ret, countof(ret), PSTR("%04u-%02u-%02u"), year(), month(), day());
return ret;
}
/**
refresh the time button label with the current time
@param void
@return void
*/
void refreshTime(void) {
uiWidgets[TIME_BUTTON_ID].text = strTime();
uiWidgets[TIME_BUTTON_ID].paint(&uiWidgets[TIME_BUTTON_ID]);
}
/**
refresh the date button label with the current date
@param void
@return void
*/
void refreshDate(void) {
uiWidgets[DATE_BUTTON_ID].text = strDate();
uiWidgets[DATE_BUTTON_ID].paint(&uiWidgets[DATE_BUTTON_ID]);
}
//////////////////////////////////////////////////////////////
// ON TAP EVENT CALLBACKS
//////////////////////////////////////////////////////////////
void spinnerOnTapEvent(UiElement_type * spinner, const int x, const int y) {
int selectorHeight = (spinner->hitbox.y2 - spinner->hitbox.y1) / 3;
if ( x >= spinner->hitbox.x1
&& x <= spinner->hitbox.x2
&& y >= spinner->hitbox.y1
&& y <= spinner->hitbox.y2 - 2 * selectorHeight ) {
//increase value
if (spinner->value < spinner->maxValue) {
spinner->value = spinner->value + 1;
} else {
// circular rollout
spinner->value = spinner->minValue;
}
spinner->paint(spinner);
} else if ( x >= spinner->hitbox.x1
&& x <= spinner->hitbox.x2
&& y >= spinner->hitbox.y1 - selectorHeight
&& y <= spinner->hitbox.y2 ) {
// decrease value
if (spinner->value > spinner->minValue) {
spinner->value = spinner->value - 1;
} else {
// circular rollout
spinner->value = spinner->maxValue;
}
spinner->paint(spinner);
}
}
void timeButtonOnTapEvent(UiElement_type * spinner, const int16_t x, const int16_t y) {
uiWidgets[HOUR_ID].value = hour();
uiWidgets[MINUTE_ID].value = minute();
uiWidgets[SECOND_ID].value = second();
uiWidgets[DAY_ID].value = day();
uiWidgets[MONTH_ID].value = month();
uiWidgets[YEAR_ID].value = year();
setTimeSpinnerVisible(true);
setDateSpinnerVisible(true);
setCancelButtonVisible(true);
setSetButtonVisible(true);
paintDateTimePickerDialog();
}
void cancelButtonOnTapEvent(UiElement_type * spinner, const int16_t x, const int16_t y) {
setTimeSpinnerVisible(false);
setDateSpinnerVisible(false);
setCancelButtonVisible(false);
setSetButtonVisible(false);
clearDialog(dialogSize);
paintDateTimePickerDialog();
}
void setButtonOnTapEvent(UiElement_type * spinner, const int16_t x, const int16_t y) {
setTime( uiWidgets[HOUR_ID].value, uiWidgets[MINUTE_ID].value, uiWidgets[SECOND_ID].value,
uiWidgets[DAY_ID].value, uiWidgets[MONTH_ID].value, uiWidgets[YEAR_ID].value);
refreshDate();
refreshTime();
}
//////////////////////////////////////////////////////////////
// READ UI SELECTION
//////////////////////////////////////////////////////////////
/*
Checks if the user is selecting any of the visible enabled ui elements
The onTap callback of the selected element is called and it set as pressed
@param lastSelected the last selection
@return the new selection
*/
int readUiSelection(const int16_t lastSelected ) {
int16_t xpos, ypos; //screen coordinates
TSPoint tp = ts.getPoint(); //tp.x, tp.y are ADC values
// if sharing pins, you'll need to fix the directions of the touchscreen pins
pinMode(XM, OUTPUT);
pinMode(YP, OUTPUT);
// we have some minimum pressure we consider 'valid'
// pressure of 0 means no pressing!
if (tp.z > MINPRESSURE && tp.z < MAXPRESSURE) {
xpos = map(tp.x, TS_RT, TS_LEFT, 0, tft.width());
ypos = map(tp.y, TS_BOT, TS_TOP, 0, tft.height());
// are we in buttons area ?
for (int i = 0; i < WIDGETS_NO; i++) {
if ( uiWidgets[i].enabled == true && uiWidgets[i].visible == true) {
if ( xpos >= uiWidgets[i].hitbox.x1 && xpos <= uiWidgets[i].hitbox.x2
&& ypos >= uiWidgets[i].hitbox.y1 && ypos <= uiWidgets[i].hitbox.y2) {
if (lastSelected != i && lastSelected >= 0 ) { // unselect last selection
uiWidgets[lastSelected].pressed = false;
uiWidgets[lastSelected].paint(&uiWidgets[lastSelected]);
}
uiWidgets[i].pressed = true;
uiWidgets[i].paint(&uiWidgets[i]);
uiWidgets[i].onTap(&uiWidgets[i], xpos, ypos);
// debounce tap
delay(120);
return i;
}
}
}
}
if (lastSelected >= 0 && uiWidgets[lastSelected].pressed) {
// unselect last selection
uiWidgets[lastSelected].pressed = false;
uiWidgets[lastSelected].paint(&uiWidgets[lastSelected]);
}
return -1;
}
Comments