Lopaka Basics 08: UI sensível ao toque com LVGL

Isso parece um aplicativo em um celular, mas roda em um microcontrolador com uma telinha sensível ao toque. Você gira o dial com um dedo, desliza de quente para frio, toca em um botão e o dial varre para um novo valor.
Neste episódio, você vai ver como um framework de UI chamado LVGL transforma uma tela pequena em uma interface sensível ao toque, e o que torna ele diferente das bibliotecas gráficas dos episódios anteriores. Depois você vai organizar a tela no Lopaka, adicionar algumas linhas do seu próprio código para os toques e executar ele em um ESP32 simulado com um painel de toque real.
O que você vai construir: uma tela sensível ao toque de 240×320 que controla uma luz da sala de estar: um dial de brilho com um número grande, um controle deslizante Warm ↔ Cool, um interruptor de energia e dois botões de cena, Relax e Focus. Do que você precisa: um navegador e o simulador online do episódio 1. Uma placa real é opcional: um ESP32 e um display ILI9341 de 2,8 polegadas com um painel de toque capacitivo FT6206.
Uma caneta ou um conjunto de peças
Uma biblioteca gráfica desenha uma imagem de um botão
Até agora, desenhamos cada tela com uma biblioteca gráfica como U8g2 ou Adafruit GFX. Uma biblioteca gráfica é uma caneta: um retângulo aqui, um texto ali. Um botão desenhado assim é só uma imagem de um botão.
O toque também é problema seu. Quando um dedo pousa na tela, seu código precisa ler as coordenadas, conferir se elas caem dentro do retângulo (x > 124 && x < 224 && y > 274 …), desenhar o botão no visual pressionado dele e redesenhar a tela, tudo à mão.
Um framework de UI oferece objetos reais
LVGL é um framework de UI. Você não desenha um botão, você cria um: lv_button_create(). A partir daí, o botão é um objeto que conhece o próprio tamanho, as próprias cores e o próprio estado.
O LVGL lê o toque, encontra o objeto sob o dedo, mostra ele pressionado e diz ao seu código o que aconteceu: "Focus was clicked". Ele também redesenha só a parte da tela que mudou, e não a imagem inteira.

Etapa 1: um novo projeto LVGL
Clique em + New Project e configure a caixa de diálogo:
- Platform: LVGL (v9).
- Title: "Living Room".
- Screen Size: ative Custom e digite W
240, H320. A tela fica em pé, como um celular. - Background Color:
#0E1116, um preto azulado escuro.
Clique em Salvar. A cor de fundo é a primeira linha do código gerado:
lv_obj_set_style_bg_color(lv_screen_active(), lv_color_hex(0x0E1116), LV_PART_MAIN);lv_screen_active() é o próprio objeto da tela, e cada widget que você adicionar vai morar nele.
Etapa 2: adicione os widgets
Em um projeto LVGL, a barra de ferramentas guarda widgets reais: ao lado da ferramenta de seleção e da pintura ficam text (Aa), Text Area, Panel, Button, Switch, Slider, Checkbox e Arc. Clique em uma ferramenta e o widget aparece na tela. Depois defina a posição, o tamanho e as cores dele no inspetor à direita.
O dial
Escolha a ferramenta Arc. Defina X 30, Y 52, W 180 e H 180, depois Value 70. Defina Fill color como #2196F3 e Back como #2A3039.

Um dial de 180 pixels em uma tela de 240 pixels deixa 30 pixels de cada lado, então X 30 centraliza ele. Os outros três números vêm com o arco:
- Start
0e End270tornam a trilha três quartos de um círculo. - Rotate
135gira ele para a abertura ficar na parte inferior. O LVGL conta os ângulos no sentido horário a partir da direita, então o arco começa no canto inferior esquerdo e termina no canto inferior direito. - Value
70é quanto da trilha está preenchido, em uma escala de 0 a 100.
Fill color pinta a parte preenchida e Back, o resto da trilha.
Rótulos, um interruptor, um controle deslizante e dois botões
Adicione o resto da tela. Dê a cada widget as cores dele no inspetor enquanto avança:
- O número: um texto
70%em branco (#FFFFFF) dentro do dial, em X104, Y150. - O título: um texto
Living roomem branco no canto superior esquerdo. - Um interruptor no canto superior direito, 52×28 pixels, com Fill color
#2196F3e Back#3A404A. Checked está ativado: a luz começa ligada. - Um controle deslizante sob o dial, 208 pixels de largura e 8 de altura, com Fill color
#2196F3. - Dois botões na parte inferior,
RelaxeFocus, 100×36 pixels cada. Defina R como17, a cor do texto como#FFFFFFe Back como#2A3039. Um raio de quase metade da altura transforma cada botão em uma pílula. - Três rótulos pequenos em cinza (
#8A93A0):Brightnesssob o número,WarmeCoolacima das duas extremidades do controle deslizante.
Cada widget aparece no meio da tela quando você adiciona ele, e os campos X e Y movem ele para o lugar.
Etapa 3: nomeie as camadas
Cada widget é uma camada no painel Camadas, e o nome da camada vira o nome da variável no código. Clique duas vezes em uma camada e dê aos seis de que você vai precisar no seu código nomes curtos:
| Camada | Nome | No código |
|---|---|---|
| the arc | dial | arc_dial |
the 70% text | level | label_level |
| the switch | power | switch_power |
| the slider | warmth | slider_warmth |
| the Relax button | relax | button_relax |
| the Focus button | focus | button_focus |
O Lopaka coloca o tipo do widget na frente do nome. O título e os rótulos cinza mantêm os nomes padrão deles; nada faz referência a eles.
Tudo é um objeto
O LVGL vê a tela que você montou como um conjunto de objetos. Tudo na tela é um objeto, e os objetos moram em uma árvore. A tela guarda o dial, o número, o controle deslizante, o interruptor e os botões. Cada botão guarda o próprio rótulo, então os dois se movem juntos.

Estilos e estados
Cada objeto tem estilos: cor, raio, fonte. Os estilos podem depender do estado do objeto. O mesmo botão Focus parece diferente por padrão, enquanto está pressionado e quando está desativado, e o interruptor muda quando está marcado. O LVGL alterna entre esses visuais sozinho.

Eventos
Por fim, eventos. Quando o controle deslizante se move, ele envia um evento VALUE_CHANGED. Quando um botão é tocado, ele envia CLICKED. Sua função executa e decide o que acontece depois: on_warmth() tinge o dial, on_focus() envia ele para 100%.
O Lopaka escreve os objetos e os estilos deles. Os eventos são seus.

Etapa 4: seu código
O que o Lopaka gera
Abra o painel de código. Cada widget é uma chamada de criação, seguida da posição, do tamanho e das cores dele:
// [BEGIN lopaka generated]
lv_obj_set_style_bg_color(lv_screen_active(), lv_color_hex(0x0E1116), LV_PART_MAIN);
// arcLvgl: dial
lv_obj_t * arc_dial = lv_arc_create(lv_screen_active());
lv_obj_set_pos(arc_dial, 30, 52);
lv_obj_set_size(arc_dial, 180, 180);
lv_arc_set_rotation(arc_dial, 135);
lv_arc_set_bg_angles(arc_dial, 0, 270);
lv_arc_set_value(arc_dial, 70);
lv_obj_set_style_arc_color(arc_dial, lv_color_hex(0x2196F3), LV_PART_INDICATOR);
lv_obj_set_style_arc_color(arc_dial, lv_color_hex(0x2A3039), LV_PART_MAIN);
// string: level
lv_obj_t * label_level = lv_label_create(lv_screen_active());
lv_obj_set_style_text_color(label_level, lv_color_hex(0xFFFFFF), LV_PART_MAIN);
lv_obj_set_pos(label_level, 104, 132);
lv_label_set_text(label_level, "70%");
// string: string 3
lv_obj_t * label_string_3 = lv_label_create(lv_screen_active());
lv_obj_set_style_text_color(label_string_3, lv_color_hex(0xFFFFFF), LV_PART_MAIN);
lv_obj_set_pos(label_string_3, 16, 14);
lv_label_set_text(label_string_3, "Living room");
// switch: power
lv_obj_t * switch_power = lv_switch_create(lv_screen_active());
lv_obj_set_pos(switch_power, 172, 12);
lv_obj_set_size(switch_power, 52, 28);
lv_obj_add_state(switch_power, LV_STATE_CHECKED);
lv_obj_set_style_bg_color(switch_power, lv_color_hex(0x2196F3), LV_PART_INDICATOR | LV_STATE_CHECKED);
lv_obj_set_style_bg_color(switch_power, lv_color_hex(0x3A404A), LV_PART_MAIN);
// slider: warmth
lv_obj_t * slider_warmth = lv_slider_create(lv_screen_active());
lv_slider_set_value(slider_warmth, 50, LV_ANIM_OFF);
lv_obj_set_pos(slider_warmth, 16, 252);
lv_obj_set_size(slider_warmth, 208, 8);
lv_obj_set_style_bg_color(slider_warmth, lv_color_hex(0x2196F3), LV_PART_INDICATOR);
lv_obj_set_style_bg_color(slider_warmth, lv_color_hex(0x2196F3), LV_PART_KNOB);
lv_obj_set_style_bg_color(slider_warmth, lv_color_hex(0x2196F3), LV_PART_MAIN);
// button: relax
lv_obj_t * button_relax = lv_button_create(lv_screen_active());
lv_obj_set_pos(button_relax, 16, 274);
lv_obj_set_size(button_relax, 100, 36);
lv_obj_set_style_radius(button_relax, 17, LV_PART_MAIN);
lv_obj_set_style_bg_color(button_relax, lv_color_hex(0x2A3039), LV_PART_MAIN);
lv_obj_t * button_relax_label = lv_label_create(button_relax);
lv_label_set_text(button_relax_label, "Relax");
lv_obj_center(button_relax_label);
// button: focus
lv_obj_t * button_focus = lv_button_create(lv_screen_active());
lv_obj_set_pos(button_focus, 124, 274);
lv_obj_set_size(button_focus, 100, 36);
lv_obj_set_style_radius(button_focus, 17, LV_PART_MAIN);
lv_obj_set_style_bg_color(button_focus, lv_color_hex(0x2A3039), LV_PART_MAIN);
lv_obj_t * button_focus_label = lv_label_create(button_focus);
lv_label_set_text(button_focus_label, "Focus");
lv_obj_center(button_focus_label);
// string: string 8
lv_obj_t * label_string_8 = lv_label_create(lv_screen_active());
lv_obj_set_style_text_color(label_string_8, lv_color_hex(0x8A93A0), LV_PART_MAIN);
lv_obj_set_pos(label_string_8, 84, 188);
lv_label_set_text(label_string_8, "Brightness");
// string: string 9
lv_obj_t * label_string_9 = lv_label_create(lv_screen_active());
lv_obj_set_style_text_color(label_string_9, lv_color_hex(0x8A93A0), LV_PART_MAIN);
lv_obj_set_pos(label_string_9, 16, 226);
lv_label_set_text(label_string_9, "Warm");
// string: string 10
lv_obj_t * label_string_10 = lv_label_create(lv_screen_active());
lv_obj_set_style_text_color(label_string_10, lv_color_hex(0x8A93A0), LV_PART_MAIN);
lv_obj_set_pos(label_string_10, 194, 226);
lv_label_set_text(label_string_10, "Cool");
// [END lopaka generated]Este é o código com Wrapper function desativado, como vamos precisar dele daqui a pouco. Alguns detalhes nele:
- Partes. Um arco tem uma trilha (
LV_PART_MAIN) e uma parte preenchida (LV_PART_INDICATOR); um controle deslizante e um interruptor também têm um botão (LV_PART_KNOB). Cada chamada de estilo nomeia a parte que ela pinta. A cor do interruptor se aplica aLV_PART_INDICATOR | LV_STATE_CHECKED, a parte preenchida enquanto o interruptor está ligado: um estilo para um estado. - O botão e o rótulo dele.
lv_label_create(button_relax)cria o rótulo dentro do botão, e não na tela, elv_obj_center()mantém ele no meio. Essa é a árvore de objetos da explicação, em código. - Por que o número diz 132.
lv_obj_set_pos()posiciona um objeto pelo canto superior esquerdo dele. No inspetor do Lopaka, o Y de um texto é a parte inferior da linha dele, como em cada plataforma. A linha de texto tem 18 pixels de altura, então Y150no inspetor vira132no código. - Padrões ficam de fora. O Lopaka deixa de fora estilos que correspondem aos próprios padrões do LVGL. É por isso que os botões não têm uma linha para o texto branco deles: branco já é a cor padrão do texto de um botão.
Desative a função contenedora
Com Wrapper function ativado, o Lopaka envolve o código em void drawScreen_1(void) { … }. Desative ele: então as linhas geradas ficam dentro de uma função que você mesmo escreve, com seu próprio código acima e abaixo delas. O Lopaka mantém o que você digita fora dos marcadores BEGIN e END e atualiza só a parte entre eles quando você muda o design.
Seu código acima dos marcadores
Acima de // [BEGIN lopaka generated], digite a configuração do display e algumas funções pequenas:
#include "touch_display.h"
lv_obj_t *dial, *level, *warmth;
void show_level(void *obj, int32_t v) {
lv_arc_set_value(dial, v);
lv_label_set_text_fmt(level, "%d%%", v);
}
void tint(int w) {
lv_color_t warm = lv_color_hex(0xFF8A3D), white = lv_color_hex(0xFFF1DC), cool = lv_color_hex(0x4FC3F7);
lv_color_t c = w < 50 ? lv_color_mix(white, warm, w * 255 / 50)
: lv_color_mix(cool, white, (w - 50) * 255 / 50);
lv_obj_set_style_arc_color(dial, c, LV_PART_INDICATOR);
lv_obj_set_style_bg_color(dial, c, LV_PART_KNOB);
}
void set_warmth(void *obj, int32_t w) {
lv_slider_set_value(warmth, w, LV_ANIM_OFF);
tint(w);
}
void scene(int to_level, int to_warmth) {
lv_anim_t a;
lv_anim_init(&a);
lv_anim_set_duration(&a, 700);
lv_anim_set_path_cb(&a, lv_anim_path_ease_out);
lv_anim_set_exec_cb(&a, show_level);
lv_anim_set_values(&a, lv_arc_get_value(dial), to_level);
lv_anim_start(&a);
lv_anim_set_exec_cb(&a, set_warmth);
lv_anim_set_values(&a, lv_slider_get_value(warmth), to_warmth);
lv_anim_start(&a);
}
void on_dial(lv_event_t *e) { show_level(NULL, lv_arc_get_value(dial)); }
void on_warmth(lv_event_t *e) { tint(lv_slider_get_value(warmth)); }
void on_relax(lv_event_t *e) { scene(30, 10); }
void on_focus(lv_event_t *e) { scene(100, 90); }
void on_power(lv_event_t *e) {
lv_obj_t *sw = lv_event_get_target_obj(e);
bool on = lv_obj_has_state(sw, LV_STATE_CHECKED);
lv_obj_t *scr = lv_screen_active();
for (uint32_t i = 0; i < lv_obj_get_child_count(scr); i++) {
lv_obj_t *o = lv_obj_get_child(scr, i);
if (o == sw) continue;
lv_obj_set_state(o, LV_STATE_DISABLED, !on);
lv_obj_set_style_opa(o, on ? LV_OPA_COVER : LV_OPA_30, 0);
}
}
void build_ui() {show_level()move o dial e escreve o número:%d%%imprime o valor seguido de um sinal de porcentagem.tint()transforma a posição do controle deslizante, de 0 a 100, em uma cor: laranja quente em 0, branco quente em 50, azul frio em 100.lv_color_mix()mistura duas cores; o último argumento dele, de 0 a 255, diz quanto da primeira cor pegar.scene()anima o dial e o controle deslizante para novos valores em 700 milissegundos, desacelerando no fim. Relax vai para 30% e quente, Focus para 100% e frio.on_power()executa quando o interruptor muda. Com a luz apagada, ele desativa cada outro objeto na tela e esmaece ele para 30%.
A última linha abre build_ui(), a função em que o código gerado agora fica.
Seu código abaixo dos marcadores
Abaixo de // [END lopaka generated], feche a função e adicione os estilos, os eventos e os usuais setup() e loop():
dial = arc_dial;
level = label_level;
warmth = slider_warmth;
// styles
lv_obj_set_style_text_font(level, &lv_font_montserrat_48, 0);
lv_obj_align(level, LV_ALIGN_TOP_MID, 0, 116);
lv_obj_set_style_bg_color(warmth, lv_color_hex(0xFF8A3D), LV_PART_MAIN);
lv_obj_set_style_bg_opa(warmth, LV_OPA_COVER, LV_PART_MAIN);
lv_obj_set_style_bg_grad_color(warmth, lv_color_hex(0x4FC3F7), LV_PART_MAIN);
lv_obj_set_style_bg_grad_dir(warmth, LV_GRAD_DIR_HOR, LV_PART_MAIN);
lv_obj_set_style_bg_opa(warmth, LV_OPA_TRANSP, LV_PART_INDICATOR);
lv_obj_set_style_bg_color(warmth, lv_color_white(), LV_PART_KNOB);
tint(lv_slider_get_value(warmth));
// events
lv_obj_add_event_cb(arc_dial, on_dial, LV_EVENT_VALUE_CHANGED, NULL);
lv_obj_add_event_cb(slider_warmth, on_warmth, LV_EVENT_VALUE_CHANGED, NULL);
lv_obj_add_event_cb(switch_power, on_power, LV_EVENT_VALUE_CHANGED, NULL);
lv_obj_add_event_cb(button_relax, on_relax, LV_EVENT_CLICKED, NULL);
lv_obj_add_event_cb(button_focus, on_focus, LV_EVENT_CLICKED, NULL);
}
void setup() {
display_begin();
build_ui();
}
void loop() {
lv_timer_handler();
delay(5);
}
As três primeiras linhas copiam os objetos gerados para as variáveis globais, para as funções acima conseguirem alcançar eles. As variáveis geradas existem só dentro de build_ui().
Estilos. Uma linha de estilo torna o número grande: lv_font_montserrat_48 é a fonte Montserrat do LVGL em 48 pixels. Um número maior também é mais largo, então lv_obj_align() centraliza ele no topo do dial em vez de deixar ele no canto superior esquerdo antigo. As linhas seguintes pintam a trilha do controle deslizante como um gradiente de quente para frio, escondem a parte preenchida dele e tornam o botão branco.
Eventos. Uma linha por widget conecta ele a uma função: lv_obj_add_event_cb(object, function, event, NULL). O dial atualiza o número, o controle deslizante tinge o dial, os botões trocam de cena e o interruptor esmaece a tela. O dial, o controle deslizante e o interruptor enviam LV_EVENT_VALUE_CHANGED; os botões enviam LV_EVENT_CLICKED.
O loop. lv_timer_handler() é onde o LVGL faz o trabalho dele: ele lê o toque, envia os eventos, executa as animações e redesenha o que mudou. Chame ele a cada poucos milissegundos e mantenha loop() livre de delays longos, ou a tela para de reagir.
Etapa 5: copie o código
Clique em Copiar acima do código. Você recebe o sketch inteiro: seu código, a parte gerada entre os marcadores, e setup() e loop().
Etapa 6: execute ele
O arquivo do display
O sketch começa com #include "touch_display.h". Esse arquivo conecta o LVGL a este display e ao chip de toque dele. Ele é o mesmo para cada projeto com este display, então você escreve ele uma vez:
// Display + touch glue for LVGL: ILI9341 (SPI) + FT6206 (I2C), 240x320 portrait.
#include <lvgl.h>
#include <Adafruit_ILI9341.h>
#include <Adafruit_FT6206.h>
Adafruit_ILI9341 tft(23, 21, 22); // CS, DC, RST
Adafruit_FT6206 touch;
#define DRAW_BUF_SIZE (240 * 160 * 2) // half the screen
static uint8_t *draw_buf;
static void flush(lv_display_t *d, const lv_area_t *a, uint8_t *px) {
uint32_t w = a->x2 - a->x1 + 1, h = a->y2 - a->y1 + 1;
tft.startWrite();
tft.setAddrWindow(a->x1, a->y1, w, h);
tft.writePixels((uint16_t *)px, w * h);
tft.endWrite();
lv_display_flush_ready(d);
}
static void read_touch(lv_indev_t *i, lv_indev_data_t *data) {
if (touch.touched()) {
TS_Point p = touch.getPoint();
data->point.x = 240 - p.x; // the touch panel is mirrored
data->point.y = 320 - p.y;
data->state = LV_INDEV_STATE_PRESSED;
} else {
data->state = LV_INDEV_STATE_RELEASED;
}
}
void display_begin() {
pinMode(5, OUTPUT);
digitalWrite(5, HIGH); // backlight
SPI.begin(18, 17, 19); // SCK, MISO, MOSI
Wire.begin(4, 16); // SDA, SCL
tft.begin();
tft.setRotation(0);
touch.begin(40);
lv_init();
lv_tick_set_cb(millis);
lv_display_t *disp = lv_display_create(240, 320);
lv_display_set_flush_cb(disp, flush);
draw_buf = (uint8_t *)malloc(DRAW_BUF_SIZE);
lv_display_set_buffers(disp, draw_buf, NULL, DRAW_BUF_SIZE, LV_DISPLAY_RENDER_MODE_PARTIAL);
lv_indev_t *indev = lv_indev_create();
lv_indev_set_type(indev, LV_INDEV_TYPE_POINTER);
lv_indev_set_read_cb(indev, read_touch);
}Ele tem dois trabalhos:
flush()envia pixels para o display. O LVGL não mantém a tela inteira na memória: uma imagem completa de 240×320 com dois bytes por pixel ocuparia 153.600 bytes. Ele desenha em um buffer de metade da tela, 240 × 160 × 2 = 76.800 bytes, e entrega uma área de cada vez. Quando só o número muda, só aquela área é desenhada e enviada.read_touch()diz ao LVGL onde está o dedo. O chip FT6206 informa o ponto de toque. Neste painel, as coordenadas dele vão na direção oposta às do display, então240 - p.xe320 - p.yinvertem elas.
lv_tick_set_cb(millis) dá ao LVGL um relógio para as animações dele, e touch.begin(40) define quão firme um toque precisa ser para contar.
O arquivo de configurações
O LVGL lê as configurações dele de um arquivo chamado lv_conf.h. Pegue lv_conf_template.h da biblioteca LVGL, salve ele como lv_conf.h e mude duas linhas:
#if 1 /* Set this to "1" to enable content */#define LV_FONT_MONTSERRAT_48 1A primeira linha ativa o arquivo. A segunda adiciona a fonte de 48 pixels para o número grande: o LVGL compila só as fontes que você ativa, e o modelo ativa só Montserrat 14, a fonte padrão de cada rótulo.
Execute ele no simulador
No simulador online do episódio 1, crie um novo projeto ESP32 e adicione o display ILI9341 Cap Touch: uma tela ILI9341 com um painel de toque FT6206. Ligue ele ao ESP32:
- VCC ao 3V3, GND ao GND, LED (o backlight) ao 5,
- CS ao 23, D/C ao 21, RST ao 22,
- SCK ao 18, MOSI ao 19, MISO ao 17,
- SDA ao 4 e SCL ao 16 para o painel de toque.
Adicione as bibliotecas lvgl, Adafruit ILI9341 e Adafruit FT6206 Library. Crie dois arquivos novos, touch_display.h e lv_conf.h, com o conteúdo acima. Cole o código do Lopaka em sketch.ino e pressione Play.
Quando ele estiver montado, o mouse vira um dedo. Gire o dial: o número acompanha. Deslize de quente para frio: o dial muda de cor. Toque em Focus: o dial varre para 100%. Depois desligue a luz, e o resto da tela esmaece.

Em uma placa real
O mesmo sketch roda em um ESP32 real com um módulo ILI9341 de 2,8 polegadas. Na Arduino IDE, instale as mesmas três bibliotecas e escolha a placa ESP32 Dev Module. O sketch ocupa cerca de metade da flash e 27% da RAM; 64 KB dessa RAM é a própria memória do LVGL para objetos e estilos. Três coisas para conferir:
- Onde
lv_conf.hvai. Na Arduino IDE, ele não vai ao lado do sketch. Coloque ele na sua pastalibraries, ao lado da pastalvgl, e não dentro dela. Se o LVGL não conseguir encontrar ele, a fonte de 48 pixels falta e o sketch não compila. - O chip de toque. Muitos módulos ILI9341 de 2,8 polegadas de baixo custo têm um painel de toque resistivo com um chip XPT2046 em vez do FT6206 capacitivo. Para esses,
read_touch()precisa de uma biblioteca para o XPT2046; o resto do sketch continua o mesmo. - A direção do toque. Se um toque pousar no lado errado da tela, seu painel não é espelhado como este. Use
p.xep.ydireto emread_touch(), ou espelhe só um deles.
Quando o LVGL vale a pena
O LVGL precisa de memória. A própria documentação dele pede pelo menos 32 KB de RAM. Um Arduino UNO tem 2 KB, então o LVGL sozinho precisa de 16 vezes o UNO inteiro.

Um ESP32 dá ao seu sketch 320 KB de RAM, e esta interface usa 27% dela. Escolha a ferramenta pelo trabalho:
- Alguns números em um OLED? Uma biblioteca gráfica basta.
- Toque, widgets e animação? Isso é LVGL.

Recapitulação
- Objetos, não pixels: o LVGL monta uma tela a partir de objetos como arcos, controles deslizantes e botões. Você cria um botão em vez de desenhar ele.
- Estilos para cada estado: cores, raio e fontes decidem como um objeto parece por padrão, enquanto está pressionado, marcado ou desativado.
- Eventos conectam um toque ao seu código: uma linha
lv_obj_add_event_cbpor widget. O Lopaka organiza os widgets, e você escreve os eventos.
Próximo episódio: Beyond Arduino. Um design, muitas plataformas, do ESPHome ao MicroPython e Flipper Zero. Leia o episódio 9 ou assista no YouTube.
Pronto para testar? Abra o Lopaka e organize sua própria tela sensível ao toque.