How to create a step slider

What do you want to achieve?

Create a step slider with range from 0 to 100 with step 10.

0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100

A function like this would be great:
lv_slider_set_step(slider, 10);

What have you tried so far?

Code below.
Code works, but while my finger is in knob, events are called continuously, even without move the finger.

Code to reproduce

static void fan_speed_slider_event_cb(lv_event_t * e)
{
    lv_obj_t* slider = lv_event_get_target(e);
    int32_t current_value = lv_slider_get_value(slider);
    
    int32_t step = 10; // Define your desired step here
    int32_t new_value = (current_value / step) * step;

    // Optionally, handle rounding up or down based on proximity
    if (current_value % step >= step / 2)
    {
        new_value += step;
    }

    lv_slider_set_value(slider, new_value, LV_ANIM_OFF);
    
    lv_event_stop_bubbling(e);    

    printf("\n");
    ESP_LOGI(UI_HOME_SCREEN_TAG, "current_value = %ld", current_value);
    ESP_LOGI(UI_HOME_SCREEN_TAG, "new_value = %ld", new_value);
}

Screenshot and/or video

Environment

  • MCU/MPU/Board:
    ESP32-S3

  • LVGL version: See lv_version.h
    V9.3.0

Hi

If you want to give the user the “feeling” of only 11 steps, set the max value to 11

lv_slider_set_max_value(slider, 10);

And in event callback LV_EVENT_VALUE_CHANGED, multiply the value by 10 to use internally, the user will only be able to change between 11 steps (0 to 10).

This function “lv_slider_set_max_value(slider, 10)” does not exist in lvgl v9.3.0.

Wowww… sorry about that, i get the function name from the “documentation” since I have done something similar to this (having a slider that did “steps” since i only wanted the user to select 4~5 values, but didn’t check my “old” code, just went to the docs to check it…
It seems this function was added because of xml but in some way it’s on the Docs for release 9.3.

For LVGL Version (9.3) you can use lv_slider_set_range, in your case:

lv_slider_set_range(obj, 0, 10);

It will give the “feeling” to the user that only 11 values are possible… and then you have to multiply the slider value by 10 to have the correct value to use internally.

It’s not “steps”, but something closer to that…

My code is working now, but i will try to simplify it.

The problem is that callback is called many times before i release the finger from knob.

I think this happens because I call the function lv_slider_set_value() inside callback function.

Well if you set the slider with:

lv_slider_set_range(obj, 0, 10);

in the Callback event, you don’t need the logic you had in the first post…

You would only need:

static void fan_speed_slider_event_cb(lv_event_t * e)
{
    lv_obj_t* slider = lv_event_get_target(e);
    int32_t current_value = lv_slider_get_value(slider);
   
   int32_t _realValue = current_value * 10; //we have the slider configured to go from 0 to 10, that is equals to 0 to 100 in 10 values step. Use this value to control other stuff, set it to a global variable, whatever....
   
    lv_event_stop_bubbling(e);    

    printf("\n");
    ESP_LOGI(UI_HOME_SCREEN_TAG, "current_value = %ld", current_value);
    ESP_LOGI(UI_HOME_SCREEN_TAG, "new_value = %ld", new_value);
}

For the user, the slider will only have the 11 steps. and you only receive an event callback when the slider moves between each value (if event setup to LV_EVENT_VALUE_CHANGED). No need to “update” the slider value here

The downside, is that if in your code, you get the slider value to control something (let’s say display brightness), you need to multiply that value by 10 first to get the correct value, an option is on the event callback set a global variable with the correct value and use that variable in the remaining code instead of fetching the slider value directly.

Nice.

Now i understand your concept.

I will test in the hardware tomorrow.

Thank’s.

Two more questions.

  1. How do I get the coordinates of the center of the slider knob ?

  2. When clicking on the slider, outside of the knob, the knob only goes to the click position when the finger is released.
    Is there a way to do this by pressing the slider, instead of releasing the slider ?

I don’t think there is a direct way to get that, maybe getting the slider current coordinates and together with the “min” / “max” / “current” value calculate that.

It seems the knob / value is only processed on the events LV_EVENT_PRESSING (if there is drag) and in events LV_EVENT_RELEASED/LV_EVENT_PRESS_LOST.

You can create an event handler to capture the “LV_EVENT_PRESSED” event, and “force” a new “LV_EVENT_RELEASED” to the slider object…

//Add the event callback for LV_EVENT_PRESSED
lv_obj_add_event_cb(slider_obj, action_slider_test_pressed, LV_EVENT_PRESSED, (void *)0);

//Process the event callback
void action_slider_test_pressed(lv_event_t *e)
{
  lv_event_code_t code = lv_event_get_code(e);
  if (code == LV_EVENT_PRESSED)
  {
    printf("Slider pressed: %ld\n", lv_slider_get_value(lv_event_get_target_obj(e)));
    lv_obj_send_event(lv_event_get_target_obj(e), LV_EVENT_RELEASED, e);
  }
}

This seems to do the trick…

Hi,

The solution “lv_obj_send_event(lv_event_get_target_obj(e), LV_EVENT_RELEASED, e)”, It worked for moving the knob to the finger position when pressing the slider outside the knob, but then the value label above the knob no longer shows because I hide the value label when releasing the finger.

// Registra o callback para o evento de mudanca de valor.
    lv_obj_add_event_cb(slider, fan_speed_slider_event_cb, LV_EVENT_VALUE_CHANGED, NULL);

    // Registra o callback para mostrar o label em cima do knob.
    lv_obj_add_event_cb(slider, fan_speed_slider_show_value_label_event_cb, LV_EVENT_PRESSED, slider_value);
    lv_obj_add_event_cb(slider, fan_speed_slider_show_value_label_event_cb, LV_EVENT_VALUE_CHANGED, slider_value);

    // Registrar o callback para esconder o label de cima do knob.
    lv_obj_add_event_cb(slider, fan_speed_slider_hide_value_label_event_cb, LV_EVENT_RELEASED, slider_value);

    // Registrar o callback para mover o knob ao pressionar o slider ao inves de soltar o slider.
    lv_obj_add_event_cb(slider, fan_speed_slider_pressed_action, LV_EVENT_PRESSED, NULL);

Well, in that case you need to add some extra logic to your app… something that signals the event callback for “LV_EVENT_RELEASED” that it was an internal “call”. Easy way (and with some possible issues), global variable set to “true” on “LV_EVENT_PRESSED”, so that “LV_EVENT_RELEASED” knows that is not supposed to hide the label, and putting the flag to “false” for next interaction, if the callback is the same function (with if for the event code) the variable can even be local to the function defined as static…

Don’t think you can add “user_data” when calling the LV_EVENT_RELEASED, but maybe???

Here in examples:
https://docs.lvgl.io/9.3/examples.html

In Arc - Simple Arc, label comes with the knob, using only the function:
/Rotate the label to the current position of the arc/
lv_arc_rotate_obj_to_angle(arc, label, 25);

I imagine a linear slider would be much simpler to implement.

There is also this slider flag as an option.
This flag only allows the slider to move by clicking on the knob.

lv_obj_add_flag(slider, LV_OBJ_FLAG_ADV_HITTEST);

A very stupid/crude implementation…

static void update_slider_label(lv_obj_t *slider, bool pressed)
{
  if (slider_label == NULL)
    return;

  uint32_t _value = lv_slider_get_value(slider);
  // Cast to lv_slider_t object
  lv_slider_t *slider_obj = (lv_slider_t *)slider;
  // Get the Knob Area

  printf("Pressing Point: %d,%d\n", slider_obj->pressed_point.x, slider_obj->pressed_point.y);
  // Calculate the Center position of Knob, slider_obj->right_knob_area
  int32_t knob_x = 0;
  if (pressed == true)
  {
     knob_x = slider_obj->pressed_point.x;
  }
  else
  {
    knob_x = (slider_obj->right_knob_area.x1 + slider_obj->right_knob_area.x2) / 2;
  }
  int32_t knob_y = (slider_obj->right_knob_area.y1 + slider_obj->right_knob_area.y2) / 2;
  printf("Slider value: %ld, Knob center: %d,%d\n", _value, knob_x, knob_y);
  // Move the label to the knob position, put it 30px above the knob center
  if (slider_label)
  {
    lv_obj_set_pos(slider_label, knob_x - (lv_obj_get_width(slider_label) / 2), knob_y - 30 - (lv_obj_get_height(slider_label) / 2));
    lv_label_set_text_fmt(slider_label, "%ld", _value);
    lv_obj_update_layout(slider_label);
    printf("Showing label at %d,%d\n", lv_obj_get_x(slider_label), lv_obj_get_y(slider_label));
    // Show label...
    //lv_obj_clear_flag(slider_label, LV_OBJ_FLAG_HIDDEN);
  }
  else
  {
    printf("No slider label!\n");
  }
}

void action_slider_test_pressed(lv_event_t *e)
{
  lv_event_code_t code = lv_event_get_code(e);
  if (code == LV_EVENT_PRESSED)
  {

    printf("Slider pressed: %ld\n", lv_slider_get_value(lv_event_get_target_obj(e)));
    lv_obj_send_event(lv_event_get_target_obj(e), LV_EVENT_RELEASED, e);
  }
  else if (code == LV_EVENT_RELEASED)
  {
    printf("Slider released: %ld\n", lv_slider_get_value(lv_event_get_target_obj(e)));
    // Get user Data
    uint32_t v = (uintptr_t)lv_event_get_user_data(e);
    if (lv_obj_has_flag(slider_label, LV_OBJ_FLAG_HIDDEN) == false)
    {
      printf("Slider released while label visible: %ld\n", lv_slider_get_value(lv_event_get_target_obj(e)));
      lv_obj_add_flag(slider_label, LV_OBJ_FLAG_HIDDEN);
    }
    else
    {
      printf("Slider released while label hidden: %ld\n", lv_slider_get_value(lv_event_get_target_obj(e)));
      lv_obj_remove_flag(slider_label, LV_OBJ_FLAG_HIDDEN);
    }

    update_slider_label(lv_event_get_target_obj(e), true);
  }
  else if (code == LV_EVENT_VALUE_CHANGED)
  {
    printf("Slider value changed: %ld\n", lv_slider_get_value(lv_event_get_target_obj(e)));
    update_slider_label(lv_event_get_target_obj(e), false);
  }
}

Create a label object on the current screen, and enable LV_OBJ_FLAG_HIDDEN.

You need to add this include for it to work:

#include "lvgl.h"
#include "lvgl_private.h"

Recording 2025-10-09 214221

Another option and myabe “better” is to subcribe to events like LV_EVENT_DRAW_MAIN… Maybe LV_EVENT_DRAW_MAIN_END that should be called when the knob is already “drawn” to get the current knob position.

And in reality you don’t even need to add the lvgl_private.h since this calculations can be made using the public API:

  • You know the position and size of the slider
  • You know the value of the slider
  • You can “calculate” the position of the knob…

Something like this:

  lv_obj_t *slider = lv_event_get_target(e);
  int32_t value = lv_slider_get_value(slider);
  printf("Slider value: %d\n", value);
  //Print Pos and Size of slider
  lv_area_t area;
  lv_obj_get_coords(slider, &area);
  printf("Slider pos: (%d,%d) size: (%d,%d)\n", area.x1, area.y1, lv_area_get_width(&area), lv_area_get_height(&area));
  //Position the label at slider Knob position
  //Calculate the knob position based on the value... don't forget to include "padding"/"margin" that may be associated to the slider / knob....
  int32_t slider_width = lv_area_get_width(&area);
  int32_t knob_x = area.x1 + (value * slider_width) / lv_slider_get_max_value(slider);
  int32_t knob_y = (area.y1 + area.y2) / 2;            // Vertically center the label
  //Set the label position
  lv_obj_set_pos(objects.label_slider, knob_x - lv_obj_get_width(objects.label_slider) / 2, knob_y - lv_area_get_height(&area));
  //Set the label text, (don't forget to multiply the value if using a different scale...)
  lv_label_set_text_fmt(objects.label_slider, "%03d", value);

My slider with the label at the top is already working with the “lv_obj_add_flag(slider, LV_OBJ_FLAG_ADV_HITTEST)” flag (I wish I didn’t have to use it, but… ).
It even shows the text when touch the slider knob and hides the text when release the sider knob.

I would like the center of the text to be well aligned with the center of the slider knob, throughout the slider range.

Now the precision is good when pressing the knob ( when releasing the knob and pressing again, the label is on top of the knob with very good precision ).

When dragging the knob, the label is rendered in the previous position ( I think the callback is called before the end of rendering / update the new knob position data; the callback should be called after the end of the widget rendering, I think ).

I am using #include “src/lvgl_private.h”

    // Register the callback to show the label above the knob.
    lv_obj_add_event_cb(slider, fan_speed_slider_show_value_label_event_cb, LV_EVENT_PRESSED, text_container);
    lv_obj_add_event_cb(slider, fan_speed_slider_show_value_label_event_cb, LV_EVENT_VALUE_CHANGED, text_container);

    // Register the callback to hide the label above the knob.
    lv_obj_add_event_cb(slider, fan_speed_slider_hide_value_label_event_cb, LV_EVENT_RELEASED, text_container);

////////////////////////////////////////////////////////////////////////////////////////////
// Show slider label callback.

static void fan_speed_slider_show_value_label_event_cb(lv_event_t* e)
{
    lv_obj_t* slider = lv_event_get_target(e);
    lv_obj_t* text_container = (lv_obj_t*)lv_event_get_user_data(e);        
    lv_obj_t* label = lv_obj_get_child(text_container, 0);   
        
    int32_t current_value = lv_slider_get_value(slider);
    static const int32_t step = 10;
    int32_t new_value = current_value * step; 
    
    if (new_value == 0)
    {
        lv_label_set_text(label, "OFF");
    }
    else if (new_value == 100)
    {
        lv_label_set_text(label, "FULL");
    }
    else
    {
        char text[5];
        snprintf(text, sizeof(text), "%ld%%", new_value);
        lv_label_set_text(label, text);
    }
   
////////////////////////////////////////////// 
 
    // Força o container a se ajustar ao novo texto ANTES de calcular o alinhamento
    lv_obj_update_layout(text_container); 

//////////////////////////////////////////////

    // FORÇA A ATUALIZAÇÃO DO LAYOUT DO SLIDER
    // Isso garante que 'right_knob_area' seja atualizada para o novo valor ANTES de ser lida.
    lv_obj_update_layout(slider); 
    
    // 1. Acessa a estrutura privada do slider (casting para lv_slider_t)
    lv_slider_t *slider_obj = (lv_slider_t *)slider;

    // 2. Calcula o centro do KNOB (coordenadas relativas ao slider, ou seja, absolutas)
    //    O LVGL armazena as coordenadas do knob em `right_knob_area`.

    // Posição X do centro do knob: Média de x1 e x2
    lv_coord_t knob_center_x_abs = (slider_obj->right_knob_area.x1 + slider_obj->right_knob_area.x2) / 2;

    // Posição Y do centro do knob: Média de y1 e y2
    lv_coord_t knob_center_y_abs = (slider_obj->right_knob_area.y1 + slider_obj->right_knob_area.y2) / 2;


    // --- CORREÇÃO DE COORDENADAS: ABSOLUTAS -> RELATIVAS AO CONTAINER ---

    // Se o seu 'text_container' for filho do mesmo pai que o slider,
    // precisamos subtrair a posição desse pai para obter as coordenadas relativas.

    lv_obj_t* container = lv_obj_get_parent(slider); 
    lv_area_t container_coords;
    lv_obj_get_coords(container, &container_coords); 

    // Coordenadas FINAIS RELATIVAS ao container
    lv_coord_t knob_center_x_rel = knob_center_x_abs - container_coords.x1;
    lv_coord_t knob_center_y_rel = knob_center_y_abs - container_coords.y1;

    // --- REPOSICIONAMENTO ---        

    // X: Centraliza o container sobre o centro X do knob
    lv_obj_set_x(text_container, knob_center_x_rel - lv_obj_get_width(text_container) / 2);

    // Y: Posição vertical correta (centralizada na altura do knob, movida para cima pelo offset)
    const lv_coord_t Y_OFFSET = 75; 
    lv_obj_set_y(text_container, knob_center_y_rel - (lv_obj_get_height(text_container) / 2) - Y_OFFSET);

//////////////////////////////////////////////

    lv_obj_remove_flag(text_container, LV_OBJ_FLAG_HIDDEN);

    ESP_LOGI(UI_HOME_SCREEN_TAG, "Show Slider Label");
}

////////////////////////////////////////////////////////////////////////////////////////////
// Hide slider label callback.

static void fan_speed_slider_hide_value_label_event_cb(lv_event_t* e)
{
    lv_obj_t* text_container = (lv_obj_t*)lv_event_get_user_data(e);        
            
    lv_obj_add_flag(text_container, LV_OBJ_FLAG_HIDDEN);      
    
    ESP_LOGI(UI_HOME_SCREEN_TAG, "Hide Slider Label");    
}

I think that was why i had this in the label update function:

The pressed_point appears to stay fixed with the first press, does not update when “moving”, but in the other situations that is based on the event LV_VALUE_CHANGED i could use the right_knob_area.

But if you use my second snippet, calculating the knob position based on the value (use it in the event LV_VALUE_CHANGED) and slider dimensions (don’t forget to include padding/margins in calculation) that “issue” disappears and you don’t need to use the “lv_private.h”, is using just public API.

But this was a “general” ideia to show it was possible, probably not the best way… Would say second method of using public API is better…

Only replace the line, in my function callback above:

lv_obj_update_layout(slider);

and put:

lv_refr_now(NULL);

Did the trick.

It’s just a shame that need to use the flag:
lv_obj_add_flag(slider, LV_OBJ_FLAG_ADV_HITTEST);

In that case you are forcing a full display refresh (refreshing everything that may be invalid). And probably my use of lv_obj_update_layout may not have been correct, probably a lv_obj_refresh_ext_draw_size(slider); would have been the correct call…

But if it works, great…

lv_obj_refresh_ext_draw_size(slider); doesn’t work.