I Introduction

Ici un deuxième exemple montrant comment créer son propre composant sous Compose avec Kotlin.

L’idée de ce composant est de pouvoir renseigner rapidement une valeur. Pour cela, ce composant sera composé :

  • d’un Slider permettant un choix rapide de la valeur ;
  • de 2 boutons sur les côtés du Slider pour ajuster finement la valeur (par incrément ou décrément) ;
  • d’un affichage de la valeur au dessus du curseur de sélection.

Il sera également possible de définir une valeur manuellement en cliquant l’affichage de la valeur.

Enfin il sera intégré des contrôles comme :

  • valeur min et max ;
  • la valeur de l’incrément ;
  • affichage en rouge quand la valeur saisie manuellement dépasse les valeurs min et max ;
  • le contrôle de la saisie.

II Notre composant

II.1 présentation générale du composant

Ci-dessous l’aspect général du composant et ses principales fonctionnalités :

II.2 Paramètres d’entrée

Ce composant aura les paramètres d’entrée suivants, tous définis par défaut :

/**
 * Composant personnalisé utilisant un Slider et bouton + et - pour changer la valeur
 *   Affichage de la valeur au dessus du curseur et possibilité de modifier la valeur manuellement
 *
 * @param increment_valeur Tableau de 2 valeurs indiquant plage min et max
 * @param valeur_slider valeur par defaut du slider
 * @param intervalle_valeur valeur d'incrément/décrément via les boutons + et -
 * @param width_slider largeur du composant
 * @param callback_retour_valeur fonction de retour une fois que la valeur change
 */
@Composable
fun slider_XH(
    intervalle_valeur :ClosedFloatingPointRange<Float> = 0f..25f,
    valeur_slider: Float = 1f,
    increment_valeur : Float = 0.1f,
    width_slider : Dp = 115.dp,
    callback_retour_valeur : ( (valeur_str:String)->Unit )?=null
) {

II.3 Structure générale « Compose » du composant

La structure générale du composant sera la suivante :

@Composable
fun slider_XH(
    intervalle_valeur :ClosedFloatingPointRange<Float> = 0f..25f,
    valeur_slider: Float = 1f,
    increment_valeur : Float = 0.1f,
    width_slider : Dp = 115.dp,
    callback_retour_valeur : ( (valeur_str:String)->Unit )?=null
) {

    Column() {
        Row( verticalAlignment = Alignment.CenterVertically) {

            Icon(
                imageVector = Icons.Rounded.Remove ,
                contentDescription = null,
                
            )
            Box() {
                BasicTextField(
                    value = text,
                    onValueChange = {                      
                        },
                )
                Slider(
                    // valeur du slider
                    value = position_slider,
                )
            }
            Icon(
                imageVector = Icons.Rounded.Add ,
                contentDescription = null,         
            )
        }
    }
}

Le « Slider » et le « BasicTextField » sont intégrés dans une « Box ». Ceci pour pouvoir :

  • bouger la position du « BasicTextField » plus facilement en horizontal (à chaque que la valeur du Slider change),
  • de même pour descendre légèrement le slider en vertical (une fois).

II.4 En détail …

II.4.1 les variables « globales » du composant

Concernant les variables « globales » utilisées par les composants », elles seront les suivantes :

@Composable
fun slider_XH(
    intervalle_valeur :ClosedFloatingPointRange<Float> = 0f..25f,
    valeur_slider: Float = 1f,
    increment_valeur : Float = 0.1f,
    width_slider : Dp = 115.dp,
    callback_retour_valeur : ( (valeur_str:String)->Unit )?=null
) {
    val top_element = 8.dp // hauteur pour laisser de la place entre le texte curseur et le slider + icônes

    var position_slider by remember { mutableStateOf(valeur_slider) } // position du slider
    var text by remember { mutableStateOf(get_valeur_pour_slider(position_slider.toString(), intervalle_valeur,increment_valeur).toString()) } // texte du BasicTextField (pour modification manuelle)
    var is_text_erreur by remember { mutableStateOf(false) }   // erreur sur saisie valeur (pour affichage texte en rouge)
    var largeur_texte_label: Dp by remember { mutableStateOf(0.dp) } // largeur BasicTextField pour positionnement en vertical

    val densite_locale = LocalDensity.current
    var activity = LocalContext.current as Activity
    
    // .......

II.4.2 Code du bouton « – »

Nous allons voir la partie du code qui régit le bouton gauche qui permettra de décrémenter la valeur du « Slider »

Ce qui donne :

           
            // .......

            Icon(
                imageVector = Icons.Rounded.Remove ,                // image de l'icône
                contentDescription = null,
                modifier = Modifier
                    .padding(top = top_element)                     // ajustement pour se caler avec le Slider
                    .clickable {                                    // gestion du click sur l'icône
                        position_slider -= increment_valeur // Décrémente
                        // vérifie que la valeur ne dépasse les bornes min et max
                        position_slider = get_valeur_pour_slider(position_slider.toString(),intervalle_valeur,increment_valeur)
                        // mise en forme du texte qui indiquera la valeur
                        text = position_slider.toString().removeSuffix(".0")
                        // indique aux autres composants qu'il y a une erreur de valeur (pour BasicTextField)
                        is_text_erreur = !is_valeur_slider_correcte(text, intervalle_valeur)
                        // Renvoi nouvelle valeur (si pas d'erreur)
                        if (!is_text_erreur) {
                            if (callback_retour_valeur != null) {
                                callback_retour_valeur(text)
                            }
                        }
                        cacher_clavier_slider(activity) // Cacher clavier
                    }
            )

Dans le code ci-dessus il est fait appel à trois fonctions.

La première fonction s’assure d’avoir une valeur dans l’intervalle donné ou 0.f dans le pire des cas.

/**
 * Vérifie que la valeur à traiter soit bien dans un intervalle donné
 *Si non, renvoie dans les limites min ou ax de l'intervalle
 * @param valeur Valeur à traiter
 * @param intervalle Intervalle dans lequel la valeur doit être située
 * @return donnée [Float] qui sera dans le bon intervalle donné par [intervalle]
 */
fun get_valeur_pour_slider(valeur:String, intervalle :ClosedFloatingPointRange<Float>, increment_valeur: Float ) : Float {
    var val_retour : Float = 0f

    if (valeur.isBlank())  return val_retour

    try {
        val_retour = valeur.toFloat()
    } catch (e : Exception) {
        return 0f
    }

    val valeur_a_traiter = valeur.toFloat()
    if (valeur_a_traiter > intervalle.endInclusive) return intervalle.endInclusive
    if (valeur_a_traiter<intervalle.start) return intervalle.start
    
    var increment_str = increment_valeur.toString()
    increment_str = increment_str.replaceBefore(".","0")
    val nouveau_increment = increment_str.toFloat()

    var nb_centieme = 1
    if (nouveau_increment<1f) nb_centieme=10
    if (nouveau_increment<0.1f) nb_centieme=100
    if (nouveau_increment<0.01f) nb_centieme=1000
    if (nouveau_increment==0f) nb_centieme=1
    
    val_retour= (val_retour*nb_centieme).roundToInt() / (nb_centieme.toFloat())
    
    return val_retour
}

La deuxième fonction indique juste si la valeur est bien dans l’intervalle souhaité :

/**
 * Indique si la valeur renseignée est correcte par rapport aux valeurs min max initiales
 */
fun is_valeur_slider_correcte(valeur:String, intervalle :ClosedFloatingPointRange<Float>):Boolean {
    var val_retour : Boolean = true

    if (valeur.isBlank())  return false
    // test si valeur de type float
    try {
        val test = valeur.toFloat()
    } catch (e : Exception) {
        return false
    }
    // test si dans intervalle
    val valeur_a_traiter = valeur.toFloat()
    if (valeur_a_traiter > intervalle.endInclusive) return false
    if (valeur_a_traiter < intervalle.start) return false

    return val_retour
}

Enfin la troisième fonction s’occupe de cacher le clavier :

/**
 *  Permet de cacher le clavier
 */
fun cacher_clavier_slider(activity:Activity) {
    val imm: InputMethodManager = activity.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager
    var view: View? = activity.currentFocus
    if (view == null) {
        view = View(activity);
    }
    imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
}

II.4.3 Code du bouton « + »

Le code est quasiment identique à celui du bouton « – » , seule change la ligne qui concerne l’incrément de la valeur …

            ///////////////////////////////////
            // Icône droite (+) clickable
            Icon(
                imageVector = Icons.Rounded.Add ,
                contentDescription = null,
                modifier = Modifier
                    .padding(top = top_element)
                    .clickable {
                        position_slider += increment_valeur
                        position_slider = get_valeur_pour_slider(position_slider.toString(),intervalle_valeur,increment_valeur )
                        text = position_slider.toString().removeSuffix(".0")
                        is_text_erreur = !is_valeur_slider_correcte(text, intervalle_valeur)
                        if (!is_text_erreur) {
                            if (callback_retour_valeur != null) {
                                callback_retour_valeur(text)
                            }
                        }
                        cacher_clavier_slider(activity) // Cacher clavier
                    }
            )

II.4.4 Code du « Box » qui va contenir le « BasicTextField » et le Slider

Cela va donc concerner cette zone :

Pour la structure générale qui est la suivante :

            Box() {
                BasicTextField(
                    value = text,
                    onValueChange = {                      
                        },
                )
                Slider(
                    // valeur du slider
                    value = position_slider,
                )
            }

Par défaut les composants « Slider » et « BasicTextField » ne sont pas présentés comme on le voit au dessus mais en superposition comme ci-dessous :

Ainsi, il va falloir:

  • décaler le « Slider » vers le bas ainsi que les boutons pour que le tout soit aligné,
  • positionner le « BasicTextFied » sur la même position latérale que la puce du « Slider »

Nous aurons donc un début de code dédié à la position du « BasicTextField » comme suivant :

            Box() {

                // Calcul intermédiaire pour définir offset texte à afficher

                // on décale en fonction de la grosseur du curseur
                //   10.dp correspond à ThumbRadius dans Slider.kt -> marge entre la box et le track
                val decalage_texte_a_afficher = 12.dp
                // décalage final pour positionner l'élément texte
                val decalage_texte_final_a_afficher = decalage_texte_a_afficher -( largeur_texte_label/2)
                // Callage vis à vis du curseur
                val ratio_slider : Float    = (position_slider-intervalle_valeur.start)/(intervalle_valeur.endInclusive - intervalle_valeur.start)
                //  offset_slider = taille du slider * ratio séléection du slider
                val offset_slider : Dp = (width_slider-20.dp).times(ratio_slider) // pourqoi le -20dp ???
                // Callage final
                var decalage_slider : Dp = decalage_texte_final_a_afficher + offset_slider
                if (decalage_slider<5.dp) {decalage_slider=5.dp}
                val offset_x = decalage_slider

                //....

II.4.5 Code du « TextBasicField »

Tout avant le bloc du composant, il est défini une variable permettant d’indiquer si le texte doit être affiché en rouge en cas d’erreur détecté lors d’une saisie manuelle :

                //////////////////////////////
                //  Basic TextField
                var coul_texte by remember {mutableStateOf( Color.Unspecified) }
                if (is_text_erreur) {
                    coul_texte = Color.Red
                } else {
                    coul_texte = Color.Unspecified
                }

Et il vient le code du composant :

                BasicTextField(
                    value = text,
                    onValueChange = {
                        text = traite_saisie_slider(text,it)   // permet de rendre éditable le composant + contrôle saisie
                        // met à jour la position du slider en fonction de la taille du texte
                        position_slider = get_valeur_pour_slider(text, intervalle_valeur,increment_valeur)
                        // indique si erreur detectée dans la saisie (pour affichage en rouge...)
                        is_text_erreur =  !is_valeur_slider_correcte(text, intervalle_valeur)
                        // ici pas de renvoi de valeur (on passe par la vailidation du clavier)
                        },
                    textStyle = LocalTextStyle.current.copy(color = coul_texte),
                    modifier = Modifier
                        .width(IntrinsicSize.Min)    // taille minimale de la zone de texte
                        .padding(start = offset_x)   // positionnement du composant en fonction du curseur du Slider (via Offset)
                        .background(color = Color.Transparent)
                        .onGloballyPositioned {
                            // Récupération taille de la zone de texte (pour calcul offset et placement au milieu du curseur du slider
                            largeur_texte_label = with(densite_locale) { it.size.width.toDp() }
                        }
                       ,
                    singleLine = true,  // une seule ligne
                    // Clavier numérique seulement
                    keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number,imeAction = ImeAction.Default),
                    // Clavier : action si appui sur 'OK' du clavier numérique
                    keyboardActions = KeyboardActions(
                        onDone = {
                            // renvoi valeur si correcte seulement
                            is_text_erreur =  !is_valeur_slider_correcte(text, intervalle_valeur)
                            if (!is_text_erreur) {
                                if (callback_retour_valeur != null) {
                                    callback_retour_valeur(text)
                                }
                            }
                            // cacher le clavier
                            cacher_clavier_slider(activity)
                        }
                    )
                )

Plusieurs fonctions sont utilisées.

Celle qui permet de vérifier la saisie d’un nombre réel, positif ou négatif :

/**
 * vérifie forme texte saisie pour un nombre reel signé ou pas
 *  prend en compte la saisie caractère par caractère
 *    "-"     --> OK
 *    "-0"    --> OK
 *    "-0."   --> OK
 *    "-0.1"  --> OK
 *    "-0.1.  --> KO
 * @param ancien_texte
 * @param texte
 * @return
 */
fun traite_saisie_slider(ancien_texte:String, texte:String):String {
    var texte_traite = texte

    // Remplacement caractère
    texte_traite = texte_traite.replace(",",".")

    // pattern pour test nombre réél au fur et à mesure (ex 10.25 ou -12.78)
    var regex = "^((-?)(-?\\d+\\.?\\d*)?)".toRegex() // gère la séquence manuelle de saisie au fur et à mesure

    // Si ça ne matche pas alors renvoi ancien texte
    if (!texte_traite.matches(regex)) {
        if (!texte_traite.equals("")) {
            texte_traite = ancien_texte
        }
    }
    return texte_traite
}

Grâce à cette fonction nous pouvons avoir le contrôle utilisateur suivant :

II.4.6 Code du « Slider »

Rien de particulier dans l’écriture du Slider : récupération de la valeur du slider dans variable « globale » pour être utilisée par d’autres composants, définition de la plage min et max et définition de la largeur et position haut du Slider :

                Slider(
                    // valeur du slider
                    value = position_slider,
                    // enregistrement de la veleur du slider
                    onValueChange = {
                        position_slider = get_valeur_pour_slider(it.toString(), intervalle_valeur,increment_valeur)
                        text = position_slider.toString().removeSuffix(".0")
                        is_text_erreur =!is_valeur_slider_correcte(text, intervalle_valeur)

                        // Renvoi valeur
                        if (!is_text_erreur) {
                            if (callback_retour_valeur != null) {
                                callback_retour_valeur(text)
                            }
                        }
                        // Cacher clavier
                        cacher_clavier_slider(activity)
                    },
                    // Intervalle de valeur min max
                    valueRange= intervalle_valeur ,
                    // largeur du slider
                    modifier = Modifier
                        .padding(top = top_element)
                        .width(width_slider)
                )

III Code complet

Ci dessous le code complet de l’activité qui permet d’obtenir :

  • 2 sliders sur une ligne,
  • 1 slider qui prend la place d’une ligne et qui renseigne le texte placé en dessous.

Exemple du rendu de l’activité :

Code complet de l’activité :

package com.example.slider_XH

import android.app.Activity
import android.os.Bundle
import android.util.Log
import android.view.KeyEvent
import android.view.View
import android.view.inputmethod.InputMethodManager
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Add
import androidx.compose.material.icons.rounded.Remove
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.key.onKeyEvent
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.example.textview_test1.ui.theme.Textview_test1Theme
import kotlin.math.roundToInt

class test_SliderActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            Textview_test1Theme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colors.background
                ) {
                    Sommaire_slider()
                }
            }
        }
    }
}



/**
 * Vérifie que la valeur à traiter soit bien dans un intervalle donné
 *Si non, renvoie dans les limites min ou ax de l'intervalle
 * @param valeur Valeur à traiter
 * @param intervalle Intervalle dans lequel la valeur doit être située
 * @return donnée [Float] qui sera dans le bon intervalle donné par [intervalle]
 */
fun get_valeur_pour_slider(valeur:String, intervalle :ClosedFloatingPointRange<Float>, increment_valeur: Float ) : Float {
    var val_retour : Float = 0f

    if (valeur.isBlank())  return val_retour

    try {
        val_retour = valeur.toFloat()
    } catch (e : Exception) {
        return 0f
    }

    val valeur_a_traiter = valeur.toFloat()
    if (valeur_a_traiter > intervalle.endInclusive) return intervalle.endInclusive
    if (valeur_a_traiter<intervalle.start) return intervalle.start

    var increment_str = increment_valeur.toString()
    increment_str = increment_str.replaceBefore(".","0")
    val nouveau_increment = increment_str.toFloat()

    var nb_centieme = 1
    if (nouveau_increment<1f) nb_centieme=10
    if (nouveau_increment<0.1f) nb_centieme=100
    if (nouveau_increment<0.01f) nb_centieme=1000
    if (nouveau_increment==0f) nb_centieme=1

    val_retour= (val_retour*nb_centieme).roundToInt() / (nb_centieme.toFloat())

    return val_retour
}

/**
 * Indique si la valeur renseignée est correcte par rapport aux valeurs min max initiales
 */
fun is_valeur_slider_correcte(valeur:String, intervalle :ClosedFloatingPointRange<Float>):Boolean {
    var val_retour : Boolean = true

    if (valeur.isBlank())  return false
    // test si valeur de type float
    try {
        val test = valeur.toFloat()
    } catch (e : Exception) {
        return false
    }
    // test si dans intervalle
    val valeur_a_traiter = valeur.toFloat()
    if (valeur_a_traiter > intervalle.endInclusive) return false
    if (valeur_a_traiter < intervalle.start) return false

    return val_retour
}

/**
 *  Permet de cacher le clavier
 */
fun cacher_clavier_slider(activity:Activity) {
    val imm: InputMethodManager = activity.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager
    var view: View? = activity.currentFocus
    if (view == null) {
        view = View(activity);
    }
    imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
}

/**
 * vérifie forme texte saisie pour un nombre reel signé ou pas
 *  prend en compte la saisie caractère par caractère
 *    "-"     --> OK
 *    "-0"    --> OK
 *    "-0."   --> OK
 *    "-0.1"  --> OK
 *    "-0.1.  --> KO
 * @param ancien_texte
 * @param texte
 * @return
 */
fun traite_saisie_slider(ancien_texte:String, texte:String):String {
    var texte_traite = texte

    // Remplacement caractère
    texte_traite = texte_traite.replace(",",".")

    // pattern pour test nombre réél au fur et à mesure (ex 10.25 ou -12.78)
    var regex = "^((-?)(-?\\d+\\.?\\d*)?)".toRegex() // gère la séquence manuelle de saisie au fur et à mesure

    // Si ça ne matche pas alors renvoi ancien texte
    if (!texte_traite.matches(regex)) {
        if (!texte_traite.equals("")) {
            texte_traite = ancien_texte
        }
    }
    return texte_traite
}


/**
 * Composant personnalisé utilisant un Slider et bouton + et - pour changer la valeur
 *   Affichage de la valeur au dessus du curseur et possibilité de modifier la valeur manuellement
 *
 * @param increment_valeur Tableau de 2 valeurs indiquant plage min et max
 * @param valeur_slider valeur par defaut du slider
 * @param intervalle_valeur valeur d'incrément/décrément via les boutons + et -
 * @param width_slider largeur du composant
 * @param callback_retour_valeur fonction de retour une fois que la valeur change
 */
@Composable
fun slider_XH(
    intervalle_valeur :ClosedFloatingPointRange<Float> = 0f..25f,
    valeur_slider: Float = 1f,
    increment_valeur : Float = 0.1f,
    width_slider : Dp = 115.dp,
    callback_retour_valeur : ( (valeur_str:String)->Unit )?=null
) {

    ///////////////////////////////////////////////////////////////////////////////////////////
    //  Restant à faire :
    //     Affichage décimale entière sans le 0 --> OK mais pas encore au premier affichage
    //     perte de focus de l'edittext à gérer (disparition clignotement ou puce dans certains cas)

    val top_element = 8.dp // hauteur pour laisser de la place entre le texte curseur et le slider + icônes

    var position_slider by remember { mutableStateOf(valeur_slider) } // position du slider
    var text by remember { mutableStateOf(get_valeur_pour_slider(position_slider.toString(), intervalle_valeur,increment_valeur).toString()) } // texte du BasicTextField (pour modification manuelle)
    var is_text_erreur by remember { mutableStateOf(false) }   // erreur sur saisie valeur (pour affichage texte en rouge)
    var largeur_texte_label: Dp by remember { mutableStateOf(0.dp) } // largeur BasicTextField pour positionnement en vertical

    val densite_locale = LocalDensity.current
    var activity = LocalContext.current as Activity

    // Bonne position en fonction de l'intervalle de données
    position_slider = get_valeur_pour_slider(position_slider.toString(), intervalle_valeur,increment_valeur)

    Column() {
        Row( verticalAlignment = Alignment.CenterVertically) {

            ////////////////////////////////
            // Icône gauche (-) clickable
            Icon(
                imageVector = Icons.Rounded.Remove ,                // image de l'icône
                contentDescription = null,
                modifier = Modifier
                    .padding(top = top_element)                     // ajustement pour se caler avec le Slider
                    .clickable {                                    // gestion du click sur l'icône
                        position_slider -= increment_valeur // Décrémente
                        // vérifie que la valeur ne dépasse les bornes min et max
                        position_slider = get_valeur_pour_slider(position_slider.toString(),intervalle_valeur,increment_valeur)
                        // mise en forme du texte qui indiquera la valeur
                        text = position_slider.toString().removeSuffix(".0")
                        // indique aux autres composants qu'il y a une erreur de valeur (pour BasicTextField)
                        is_text_erreur = !is_valeur_slider_correcte(text, intervalle_valeur)
                        // Renvoi nouvelle valeur (si pas d'erreur)
                        if (!is_text_erreur) {
                            if (callback_retour_valeur != null) {
                                callback_retour_valeur(text)
                            }
                        }
                        cacher_clavier_slider(activity) // Cacher clavier
                    }
            )

            Box() {

                // Calcul intermédiaire pour définir offset texte à afficher

                // on décale en fonction de la grosseur du curseur
                //   10.dp correspond à ThumbRadius dans Slider.kt -> marge entre la box et le track
                val decalage_texte_a_afficher = 12.dp
                // décalage final pour positionner l'élément texte
                val decalage_texte_final_a_afficher = decalage_texte_a_afficher -( largeur_texte_label/2)
                // Callage vis à vis du curseur
                val ratio_slider : Float    = (position_slider-intervalle_valeur.start)/(intervalle_valeur.endInclusive - intervalle_valeur.start)
                //  offset_slider = taille du slider * ratio séléection du slider
                val offset_slider : Dp = (width_slider-20.dp).times(ratio_slider) // pourqoi le -20dp ???
                // Callage final
                var decalage_slider : Dp = decalage_texte_final_a_afficher + offset_slider
                if (decalage_slider<5.dp) {decalage_slider=5.dp}
                val offset_x = decalage_slider

                //////////////////////////////
                //  Basic TextField
                var coul_texte by remember {mutableStateOf( Color.Unspecified) }
                if (is_text_erreur) {
                    coul_texte = Color.Red
                } else {
                    coul_texte = Color.Unspecified
                }

                BasicTextField(
                    value = text,
                    onValueChange = {
                        text = traite_saisie_slider(text,it)   // permet de rendre éditable le composant + contrôle saisie
                        // met à jour la position du slider en fonction de la taille du texte
                        position_slider = get_valeur_pour_slider(text, intervalle_valeur,increment_valeur)
                        // indique si erreur detectée dans la saisie (pour affichage en rouge...)
                        is_text_erreur =  !is_valeur_slider_correcte(text, intervalle_valeur)
                        // ici pas de renvoi de valeur (on passe par la vailidation du clavier)
                        },
                    textStyle = LocalTextStyle.current.copy(color = coul_texte),
                    modifier = Modifier
                        .width(IntrinsicSize.Min)    // taille minimale de la zone de texte
                        .padding(start = offset_x)   // positionnement du composant en fonction du curseur du Slider (via Offset)
                        .background(color = Color.Transparent)
                        .onGloballyPositioned {
                            // Récupération taille de la zone de texte (pour calcul offset et placement au milieu du curseur du slider
                            largeur_texte_label = with(densite_locale) { it.size.width.toDp() }
                        }
                       ,
                    singleLine = true,  // une seule ligne
                    // Clavier numérique seulement
                    keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number,imeAction = ImeAction.Default),
                    // Clavier : action si appui sur 'OK' du clavier numérique
                    keyboardActions = KeyboardActions(
                        onDone = {
                            // renvoi valeur si correcte seulement
                            is_text_erreur =  !is_valeur_slider_correcte(text, intervalle_valeur)
                            if (!is_text_erreur) {
                                if (callback_retour_valeur != null) {
                                    callback_retour_valeur(text)
                                }
                            }
                            // cacher le clavier
                            cacher_clavier_slider(activity)
                        }
                    )
                )

                //////////////////////////
                // le slider
                Slider(
                    // valeur du slider
                    value = position_slider,
                    // enregistrement de la veleur du slider
                    onValueChange = {
                        position_slider = get_valeur_pour_slider(it.toString(), intervalle_valeur,increment_valeur)
                        text = position_slider.toString().removeSuffix(".0")
                        is_text_erreur =!is_valeur_slider_correcte(text, intervalle_valeur)

                        // Renvoi valeur
                        if (!is_text_erreur) {
                            if (callback_retour_valeur != null) {
                                callback_retour_valeur(text)
                            }
                        }
                        // Cacher clavier
                        cacher_clavier_slider(activity)
                    },
                    // Intervalle de valeur min max
                    valueRange= intervalle_valeur ,
                    // largeur du slider
                    modifier = Modifier
                        .padding(top = top_element)
                        .width(width_slider)
                )
            }

            ///////////////////////////////////
            // Icône droite (+) clickable
            Icon(
                imageVector = Icons.Rounded.Add ,
                contentDescription = null,
                modifier = Modifier
                    .padding(top = top_element)
                    .clickable {
                        position_slider += increment_valeur
                        position_slider = get_valeur_pour_slider(position_slider.toString(),intervalle_valeur,increment_valeur )
                        text = position_slider.toString().removeSuffix(".0")
                        is_text_erreur = !is_valeur_slider_correcte(text, intervalle_valeur)
                        if (!is_text_erreur) {
                            if (callback_retour_valeur != null) {
                                callback_retour_valeur(text)
                            }
                        }
                        cacher_clavier_slider(activity) // Cacher clavier
                    }
            )
        }
    }
}

/**
 * Construction générale de l'activité
 */
@Composable
fun Sommaire_slider() {

    var valeur_texte_saisie1 by remember { mutableStateOf("") }

    Column() {
        Text(text = "Test Slider")
        Spacer(modifier = Modifier.height(42.dp))
        Row (
            modifier=Modifier.fillMaxWidth()
        ) {
            Spacer(modifier = Modifier.width(20.dp))
            slider_XH(valeur_slider = 0f,increment_valeur = 1f)
            Spacer(modifier = Modifier.width(10.dp))
            slider_XH(valeur_slider = 20f)
        }
        Spacer(modifier = Modifier.height(8.dp))
        Row (
            modifier=Modifier.fillMaxWidth()
        ) {
            Spacer(modifier = Modifier.width(20.dp))
            slider_XH(
                valeur_slider = 1.9f,
                increment_valeur = 0.1f,
                width_slider = 300.dp,
                intervalle_valeur = -12f..12f) {

                valeur_texte_saisie1=it
            }
        }
        Spacer(modifier = Modifier.height(16.dp))
        Row() {
            Text(text = " Valeur renseignée : $valeur_texte_saisie1")
        }
    }
}



@Preview(showBackground = true)
@Composable
fun DefaultPreview2() {
    Textview_test1Theme {
        Sommaire_slider()
    }
}
KOTLIN : Personnaliser ses composants (Slider)

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *