ESP32 with a 4.0" flickering menu

Hello, A friend of mine who knows C language but not the way arduino language structure is trying to help me to a build a menu with swipe touch. It is a esp32 with a 4.0" TFT LCD screen. What happens is that the screen flickers when swiping to the second menu and flickers a lot and really slow trying to go to the second menu and a third. Not sure what to do I'm posting the code see what others think and maybe figure out a way to fix this problem. Please community I need more pairs of eyes on this help?

#include <TFT_eSPI.h>
#include <SPI.h>

TFT_eSPI tft = TFT_eSPI();

// Two row sprites (safe memory)
TFT_eSprite rowSprites[2] = { TFT_eSprite(&tft), TFT_eSprite(&tft) };

// Temporary sprite for smooth page animation (HALF screen only)
TFT_eSprite transitionSprite = TFT_eSprite(&tft);

#define BOX_W 70
#define BOX_H 70
#define GAP_X 20
#define GAP_Y 30
#define TEXT_H 20

int page = 0;
int cursor = 0;

// Touch swipe variables
int touchStartX = 0;
int touchEndX = 0;
const int SWIPE_THRESHOLD = 50; // pixels

void setup() {
  Serial.begin(115200);
  delay(200);

  tft.init();
  tft.setRotation(1); // landscape
  tft.fillScreen(TFT_BLACK);

  // Create sprites for each row
  for (int i = 0; i < 2; i++) {
    rowSprites[i].createSprite(tft.width(), BOX_H + TEXT_H);
    rowSprites[i].setSwapBytes(true);
  }

  // Create transition sprite for smooth slide (only 320x240)
  transitionSprite.createSprite(320, 240);
  transitionSprite.setSwapBytes(true);

  drawMenu();
}

void loop() {
  // Serial input for debugging
  if (Serial.available()) {
    char c = Serial.read();

    if (c == 'L' || c == 'l') moveLeft();
    if (c == 'R' || c == 'r') moveRight();
    if (c == 'U' || c == 'u') moveUp();
    if (c == 'D' || c == 'd') moveDown();

    drawMenu();
  }

  // --- Touch swipe detection ---
  uint16_t x, y;
  if (tft.getTouch(&x, &y)) {
    touchStartX = x;
    delay(50);
    while (tft.getTouch(&x, &y)) delay(10);
    touchEndX = x;

    int dx = touchEndX - touchStartX;

    if (dx > SWIPE_THRESHOLD) {
      prevPageSmooth(); // smooth animation
    }
    else if (dx < -SWIPE_THRESHOLD) {
      nextPageSmooth(); // smooth animation
    }
  }
}

// ===========================================================
// Draw menu normally (no animation)
// ===========================================================
void drawMenu() {
  tft.fillScreen(TFT_BLACK);

  int startX = 20;
  int startY = 20;

  for (int row = 0; row < 2; row++) {
    rowSprites[row].fillSprite(TFT_BLACK);
    rowSprites[row].setTextColor(TFT_WHITE, TFT_BLACK);
    rowSprites[row].setTextDatum(TC_DATUM);

    for (int col = 0; col < 4; col++) {
      int i = row * 4 + col;
      int x = col * (BOX_W + GAP_X);

      uint16_t color = (i == cursor) ? TFT_YELLOW : TFT_BLUE;
      rowSprites[row].fillRect(x, 0, BOX_W, BOX_H, color);

      char label[20];
      sprintf(label, "Demo %d", page * 8 + i + 1);
      rowSprites[row].drawString(label, x + BOX_W / 2, BOX_H + 5, 2);
    }

    rowSprites[row].pushSprite(0, startY + row * (BOX_H + GAP_Y + TEXT_H));
  }

  tft.setTextDatum(MC_DATUM);
  tft.setTextColor(TFT_WHITE, TFT_BLACK);
  tft.drawString("Page:", tft.width() - 50, 20, 2);
  tft.drawNumber(page + 1, tft.width() - 50, 50, 4);
}

// ===========================================================
// Smooth animation for NEXT page (slide left)
// ===========================================================
void nextPageSmooth() {
  if (page >= 2) return;

  int oldPage = page;
  int newPage = page + 1;

  // Render NEW PAGE into transition sprite
  transitionSprite.fillSprite(TFT_BLACK);
  renderPageToSprite(newPage);

  // Slide animation
  for (int x = 320; x >= 0; x -= 20) {
    drawMenu(); // draw old page
    transitionSprite.pushSprite(x, 80); // slide new page in
  }

  page = newPage;
  cursor = 0;
  drawMenu();
}

// ===========================================================
// Smooth animation for PREVIOUS page (slide right)
// ===========================================================
void prevPageSmooth() {
  if (page <= 0) return;

  int oldPage = page;
  int newPage = page - 1;

  // Render NEW PAGE into transition sprite
  transitionSprite.fillSprite(TFT_BLACK);
  renderPageToSprite(newPage);

  // Slide animation
  for (int x = -320; x <= 0; x += 20) {
    drawMenu(); // draw old page
    transitionSprite.pushSprite(x, 80); // slide new page in
  }

  page = newPage;
  cursor = 0;
  drawMenu();
}

// ===========================================================
// Draws menu content (two rows) INTO transition sprite
// ===========================================================
void renderPageToSprite(int pg) {
  int startY = 0; // inside sprite (0–240)

  for (int row = 0; row < 2; row++) {
    // temp sprite for row
    rowSprites[row].fillSprite(TFT_BLACK);
    rowSprites[row].setTextColor(TFT_WHITE, TFT_BLACK);
    rowSprites[row].setTextDatum(TC_DATUM);

    for (int col = 0; col < 4; col++) {
      int i = row * 4 + col;
      int x = col * (BOX_W + GAP_X);

      uint16_t color = TFT_BLUE; // cursor highlight ignored in transition
      rowSprites[row].fillRect(x, 0, BOX_W, BOX_H, color);

      char label[20];
      sprintf(label, "Demo %d", pg * 8 + i + 1);
      rowSprites[row].drawString(label, x + BOX_W/2, BOX_H + 5, 2);
    }

    rowSprites[row].pushToSprite(&transitionSprite, 0, startY + row * (BOX_H + GAP_Y + TEXT_H));
  }
}

// ===========================================================
// Cursor movement
// ===========================================================
void moveLeft() {
  if (cursor % 4 == 0) {
    if (page > 0) { page--; cursor = 3; }
  } else cursor--;
}

void moveRight() {
  if (cursor % 4 == 3) {
    if (page < 2) { page++; cursor = 0; }
  } else cursor++;
}

void moveUp() { if (cursor >= 4) cursor -= 4; }
void moveDown() { if (cursor < 4) cursor += 4; }

The flicker comes from repeatedly clearing the whole screen and redrawing every element

➜ Each loop of the animation calls drawMenu(), which fills the screen black and redraws everything, so the display alternates between full-screen clears and sprite pushes fast enough to show visible flickering.

You need to get rid of the full-screen clears and drawing only the changed areas to remove the flicker.

Yes like he said only update the areas that need changed, as I've run into this myself.

Be careful with memory (RAM) as well.
When drawing a full screen you won't believe how much memory that takes especially on a 4" screen. It is good to start with smaller displays then work your way up. I learned that the hard way tooπŸ™ƒ
I use a ESP32-S3-WROOM-1-N16R8 for displays after learning the limitations of other models. This one has more RAM you can access, and it's not very expensive
Also make sure your Arduino board settings are correct when uploading code. They matter, a lot! You know, enable PSRAM, etc. as according to your version of esp32.
Using a S3 with high speed SPI helps with speed. The user setup.h file needs some special modifications to make it work though.
Have fun😁

Sorry for the update. Taking are of a friend who had a stroke and Just an update to @J-M-L and @r_hatton Here is the updated code he did. It seems to be a little better flickering still there but much less then before. Can you please let me know if he's on the right track or still off?

#include <TFT_eSPI.h>
#include <SPI.h>

TFT_eSPI tft = TFT_eSPI();

// Two row sprites (safe memory)
TFT_eSprite rowSprites[2] = { TFT_eSprite(&tft), TFT_eSprite(&tft) };

// Single transition sprite (width = screen width, height = area for two rows)
TFT_eSprite transitionSprite = TFT_eSprite(&tft);

#define BOX_W 70
#define BOX_H 70
#define GAP_X 20
#define GAP_Y 30
#define TEXT_H 20

int page = 0;
int cursor = 0;

// Touch swipe variables
int touchStartX = 0;
int touchEndX = 0;
const int SWIPE_THRESHOLD = 50; // pixels

// Animation tuning
const int ANIM_STEP = 16;    // pixels per frame (smaller = smoother/slower)
const int ANIM_DELAY = 12;   // ms between frames

// area where rows are drawn (top of animated region)
const int CONTENT_TOP = 20;  // corresponds to drawMenu() startY

void setup() {
  Serial.begin(115200);
  delay(200);

  tft.init();
  tft.setRotation(1); // landscape for your 320x480 (-> 480x320)
  tft.fillScreen(TFT_BLACK);

  // Create sprites for each row; width = screen width (so pushSprite(0,y) is easy)
  for (int i = 0; i < 2; i++) {
    rowSprites[i].createSprite(tft.width(), BOX_H + TEXT_H);
    rowSprites[i].setSwapBytes(true);
  }

  // Create transition sprite sized to full width and exactly the two-rows height
  int transW = tft.width();            // usually 480 in landscape
  int transH = (BOX_H + GAP_Y + TEXT_H) * 2 - GAP_Y; // exactly cover two rows area
  // Calculation note: two rows stacked: rowHeight = BOX_H + GAP_Y + TEXT_H,
  // placing two rows yields 2*rowHeight - GAP_Y overlap removal; above works with your layout
  transitionSprite.createSprite(transW, transH);
  transitionSprite.setSwapBytes(true);

  drawMenu(); // initial draw
}

void loop() {
  // Serial input for debugging/navigation
  if (Serial.available()) {
    char c = Serial.read();

    if (c == 'L' || c == 'l') moveLeft();
    if (c == 'R' || c == 'r') moveRight();
    if (c == 'U' || c == 'u') moveUp();
    if (c == 'D' || c == 'd') moveDown();

    drawMenu();
  }

  // --- Touch swipe detection ---
  uint16_t x, y;
  if (tft.getTouch(&x, &y)) {
    // Read initial X
    touchStartX = x;
    delay(40); // small debounce
    // Wait until finger is lifted (blocking) β€” keeps logic simple
    while (tft.getTouch(&x, &y)) delay(8);
    touchEndX = x;

    int dx = touchEndX - touchStartX;
    if (dx > SWIPE_THRESHOLD) {
      prevPageSmooth(); // smooth animation (to previous)
    }
    else if (dx < -SWIPE_THRESHOLD) {
      nextPageSmooth(); // smooth animation (to next)
    }
  }
}

// ===========================================================
// Draw menu normally (no animation) - draws OLD/active page
// ===========================================================
void drawMenu() {
  tft.fillScreen(TFT_BLACK);

  int startX = 20;
  int startY = CONTENT_TOP;

  for (int row = 0; row < 2; row++) {
    rowSprites[row].fillSprite(TFT_BLACK);
    rowSprites[row].setTextColor(TFT_WHITE, TFT_BLACK);
    rowSprites[row].setTextDatum(TC_DATUM);

    for (int col = 0; col < 4; col++) {
      int i = row * 4 + col;
      int x = col * (BOX_W + GAP_X);

      uint16_t color = (i == cursor) ? TFT_YELLOW : TFT_BLUE;
      rowSprites[row].fillRect(x, 0, BOX_W, BOX_H, color);

      char label[20];
      sprintf(label, "Demo %d", page * 8 + i + 1);
      rowSprites[row].drawString(label, x + BOX_W / 2, BOX_H + 5, 2);
    }

    // push the row sprite to the screen at the right Y
    int pushY = startY + row * (BOX_H + GAP_Y + TEXT_H);
    rowSprites[row].pushSprite(0, pushY);
  }

  // Page indicator
  tft.setTextDatum(MC_DATUM);
  tft.setTextColor(TFT_WHITE, TFT_BLACK);
  tft.drawString("Page:", tft.width() - 50, 20, 2);
  tft.drawNumber(page + 1, tft.width() - 50, 50, 4);
}

// ===========================================================
// Render a full page (both rows) into the provided sprite
// This writes rows at internal y = 0 and rowHeight
// ===========================================================
void renderPageToSprite(int pg, TFT_eSprite &s) {
  s.fillSprite(TFT_BLACK);
  s.setTextColor(TFT_WHITE, TFT_BLACK);
  s.setTextDatum(TC_DATUM);

  // Row height inside sprite
  int rowHeight = BOX_H + GAP_Y + TEXT_H;
  for (int row = 0; row < 2; row++) {
    int baseY = row * rowHeight; // inside sprite

    for (int col = 0; col < 4; col++) {
      int i = row * 4 + col;
      int x = 20 + col * (BOX_W + GAP_X); // note same startX offset as drawMenu
      uint16_t color = TFT_BLUE; // do not show cursor in transition β€” optional
      s.fillRect(x, baseY, BOX_W, BOX_H, color);

      char label[20];
      sprintf(label, "Demo %d", pg * 8 + i + 1);
      s.drawString(label, x + BOX_W/2, baseY + BOX_H + 5, 2);
    }
  }

  // Page indicator inside sprite (optional)
  s.setTextDatum(MC_DATUM);
  s.drawString("Page:", s.width() - 50, 8, 2);
  s.drawNumber(pg + 1, s.width() - 50, 30, 4);
}

// ===========================================================
// Smooth animation for NEXT page (slide left) - optimized
// ===========================================================
void nextPageSmooth() {
  if (page >= 2) return;

  int newPage = page + 1;

  // 1) Draw current (old) page once
  drawMenu();

  // 2) Render new page into transition sprite (once)
  renderPageToSprite(newPage, transitionSprite);

  // 3) Slide new page sprite from right -> left over the old page
  int sw = tft.width();
  for (int xpos = sw; xpos >= 0; xpos -= ANIM_STEP) {
    transitionSprite.pushSprite(xpos - transitionSprite.width(), CONTENT_TOP);
    // pushing at (xpos - spriteWidth) so at first xpos=sw it sits offscreen right
    delay(ANIM_DELAY);
  }

  // ensure final position (covering content area)
  transitionSprite.pushSprite(0, CONTENT_TOP);

  // 4) commit new page state
  page = newPage;
  cursor = 0;

  // 5) redraw stable menu (clean up any edge artifacts)
  drawMenu();
}

// ===========================================================
// Smooth animation for PREVIOUS page (slide right) - optimized
// ===========================================================
void prevPageSmooth() {
  if (page <= 0) return;

  int newPage = page - 1;

  // draw current (old) page once
  drawMenu();

  // render new page into transition sprite (once)
  renderPageToSprite(newPage, transitionSprite);

  // slide new page sprite from left -> right over the old page
  int sw = tft.width();
  for (int xpos = -transitionSprite.width(); xpos <= 0; xpos += ANIM_STEP) {
    transitionSprite.pushSprite(xpos, CONTENT_TOP);
    delay(ANIM_DELAY);
  }

  // ensure final position
  transitionSprite.pushSprite(0, CONTENT_TOP);

  // update state and redraw stable menu
  page = newPage;
  cursor = 0;
  drawMenu();
}

// ===========================================================
// Cursor movement
// ===========================================================
void moveLeft() {
  if (cursor % 4 == 0) {
    if (page > 0) { page--; cursor = 3; }
  } else cursor--;
}

void moveRight() {
  if (cursor % 4 == 3) {
    if (page < 2) { page++; cursor = 0; }
  } else cursor++;
}

void moveUp() { if (cursor >= 4) cursor -= 4; }
void moveDown() { if (cursor < 4) cursor += 4; }

what kind of flicker do you see now ?

@J-M-L Let me try to take a video but it will be on my google drive.

can't you post to youTube ? (I don't download stuff)