I Introduction
L’idée est de voir comment créer son propre composant à partir de composants classiques de « Compose ».
Pour cela on souhaite :
- une zone de saisie pour renseigner un chiffre ;
- un bouton permettant d’incrémenter le chiffre ;
- un bouton permettant de décrémenter le chiffre ;
- un contrôle sur la saisie (que des chiffres…).
L’idée est de voir comment créer son propre composant à partir de composants classiques de « Compose ».
II Utilisation du Composable « OutlinedTextField »
Nous allons utiliser un Composable de type « OutlinedTextField » qui peut intégrer des composants dans plusieurs zones, notamment avant et après le texte de saisie, comme on peut le voir dans l’image ci-dessous :

A savoir que ce composable propose les paramètres suivants :

III Notre cas
III.1 Présentation composant
Notre composant sera de la forme suivante :

III.2 Code du composant
Le code du Composable sera le suivant :
/**
* Composant permettant d'incrémenter/décrémenter un chiffre avec possibilité de modifier manuellemet la valeur
* @param valeur_initiale valeur qu'affichera le composant au premier affichage
* @param valeur_label libellé de la zone de saisie
* @param callback_retour_valeur fonction retour qui donnera la valeur du composant
*/
@Composable
fun Texte_saisie( valeur_initiale:String = "0",
valeur_label:String="",
callback_retour_valeur : ( (valeur:Int)->Unit )?=null
) {
// Pour mémoriser la valeur renseignée par l'utilisateur
var valeur_saisie by remember { mutableStateOf(valeur_initiale)}
OutlinedTextField(
// Enregistrement changement texte
value = valeur_saisie, // valeur en cours
onValueChange = {
valeur_saisie = it // dès changement, on modifie la valeur en cours
valeur_saisie = valeur_saisie.replace("[^0-9,\\.]".toRegex(),"")
},
// Fait en sorte que le sorte que le texte soit centré horizontalement
textStyle = LocalTextStyle.current.copy(textAlign = TextAlign.Center),
// Change la couleur de de fond
colors = TextFieldDefaults.textFieldColors(backgroundColor = Color(0xBEBCDC97)),
// on force à une seule ligne
singleLine = true,
//Espacement , taille, sortie de focus
modifier = Modifier
.padding(start = 20.dp, end = 10.dp)
.defaultMinSize(
minWidth = 2.dp,
minHeight = 2.dp
) // bof ... prend une autre taille min par defaut
.width(160.dp) // influe sur la taille mais fixe les choses ...
.onFocusChanged {
if (!it.isFocused) {
// retour valeur
try {
callback_retour_valeur?.invoke(valeur_saisie.toInt())
} catch (e:Exception) {
}
}
}
,
// Nom du champ
// si valeur non renseignée, affichera dans la zone valeur
// si valeur renseignée, cette zone sera placée en haut à gauche en miniature
// au dessus de la zone de valeur
label = {
Row() {
//Icon(imageVector = Icons.Rounded.Favorite , contentDescription = null )
Text(text = valeur_label)
}
},
// Place quand valeur non renseignée et focus (emplacement de la zone valeur à renseigner)
// --> Attention la hauteur prendra la hauteur de ce texte si texte plus long que le label
placeholder = {
//Text(text = "Texte quand valeur non renseignée avec une grande description de la mort qui tue")
},
// Place avant le texte de saisie
leadingIcon = {
Icon(
imageVector = Icons.Rounded.Remove ,
contentDescription = null,
modifier = Modifier.clickable {
valeur_saisie = Decremente_valeur(valeur_saisie)
// renvoie valeur
try {
callback_retour_valeur?.invoke(valeur_saisie.toInt())
} catch (e:Exception) {
}
}
)
},
// Place après le texte de saisie
trailingIcon = {
Icon(
imageVector = Icons.Rounded.Add ,
contentDescription = null,
modifier = Modifier.clickable {
valeur_saisie = Incremente_valeur(valeur_saisie)
// renvoie valeur
try {
callback_retour_valeur?.invoke(valeur_saisie.toInt())
} catch (e:Exception) {
}
}
)
},
// Force la possibilité de ne pas changer la valeur
//readOnly = true
// Définit le type de clavier à utiliser (chiffre)
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number,imeAction = ImeAction.Default),
)
}
III.3 Compréhension graphique par blocs de code
Ci-dessous les principaux morceaux de code qui permettent d’obtenir le composant souhaité :

IV Code complet de l’activité intégrant ce composant personnalisé
Ainsi le code complet :
package com.example.textview_test1
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.example.textview_test1.ui.theme.Textview_test1Theme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Textview_test1Theme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
//color = MaterialTheme.colors.error
) {
Sommaire()
}
}
}
}
}
/**
* Composant permettant d'incrémenter/décrémenter un chiffre avec possibilité de modifier manuellemet la valeur
* @param valeur_initiale valeur qu'affichera le composant au premier affichage
* @param valeur_label libellé de la zone de saisie
* @param callback_retour_valeur fonction retour qui donnera la valeur du composant
*/
@Composable
fun Texte_saisie( valeur_initiale:String = "0",
valeur_label:String="",
callback_retour_valeur : ( (valeur:Int)->Unit )?=null
) {
// Pour mémoriser la valeur renseignée par l'utilisateur
var valeur_saisie by remember { mutableStateOf(valeur_initiale)}
OutlinedTextField(
// Enregistrement changement texte
value = valeur_saisie, // valeur en cours
onValueChange = {
valeur_saisie = it // dès changement, on modifie la valeur en cours
valeur_saisie = valeur_saisie.replace("[^0-9,\\.]".toRegex(),"")
},
// Fait en sorte que le sorte que le texte soit centré horizontalement
textStyle = LocalTextStyle.current.copy(textAlign = TextAlign.Center),
// Change la couleur de de fond
colors = TextFieldDefaults.textFieldColors(backgroundColor = Color(0xBEBCDC97)),
// on force à une seule ligne
singleLine = true,
//Espacement , taille, sortie de focus
modifier = Modifier
.padding(start = 20.dp, end = 10.dp)
.defaultMinSize(
minWidth = 2.dp,
minHeight = 2.dp
) // bof ... prend une autre taille min par defaut
.width(160.dp) // influe sur la taille mais fixe les choses ...
.onFocusChanged {
if (!it.isFocused) {
// retour valeur
try {
callback_retour_valeur?.invoke(valeur_saisie.toInt())
} catch (e:Exception) {
}
}
}
,
// Nom du champ
// si valeur non renseignée, affichera dans la zone valeur
// si valeur renseignée, cette zone sera placée en haut à gauche en miniature
// au dessus de la zone de valeur
label = {
Row() {
//Icon(imageVector = Icons.Rounded.Favorite , contentDescription = null )
Text(text = valeur_label)
}
},
// Place quand valeur non renseignée et focus (emplacement de la zone valeur à renseigner)
// --> Attention la hauteur prendra la hauteur de ce texte si texte plus long que le label
placeholder = {
//Text(text = "Texte quand valeur non renseignée avec une grande description de la mort qui tue")
},
// Place avant le texte de saisie
leadingIcon = {
Icon(
imageVector = Icons.Rounded.Remove ,
contentDescription = null,
modifier = Modifier.clickable {
valeur_saisie = Decremente_valeur(valeur_saisie)
// renvoie valeur
try {
callback_retour_valeur?.invoke(valeur_saisie.toInt())
} catch (e:Exception) {
}
}
)
},
// Place après le texte de saisie
trailingIcon = {
Icon(
imageVector = Icons.Rounded.Add ,
contentDescription = null,
modifier = Modifier.clickable {
valeur_saisie = Incremente_valeur(valeur_saisie)
// renvoie valeur
try {
callback_retour_valeur?.invoke(valeur_saisie.toInt())
} catch (e:Exception) {
}
}
)
},
// Force la possibilité de ne pas changer la valeur
//readOnly = true
// Définit le type de clavier à utiliser (chiffre)
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number,imeAction = ImeAction.Default),
)
}
/**
* Incrémente une valeur de type String
* @param valeur
* @return Valeur sous forme de String
*/
fun Incremente_valeur(valeur:String):String {
var val_int = 0
try {
val_int = valeur.toInt()
val_int++
} catch (e:Exception) {
}
return val_int.toString()
}
/**
* Décrémente une valeur de type String
* @param valeur
* @return Valeur sous forme de String
*/
fun Decremente_valeur(valeur:String):String {
var val_int = 0
try {
val_int = valeur.toInt()
val_int--
} catch (e:Exception) {
}
return val_int.toString()
}
/**
* Construction générale de l'activité
*/
@Composable
fun Sommaire() {
var valeur_texte_saisie1 by remember { mutableStateOf(0) }
Column() {
Text(text = "Test Textview")
Spacer(modifier = Modifier.height(20.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center
) {
Texte_saisie(
valeur_label = "Année Min",
callback_retour_valeur = {
// récupère la valeur saisie (manuellement ou via les boutons)
valeur_texte_saisie1 = it
}
)
Spacer(modifier = Modifier.width(10.dp))
Texte_saisie(valeur_label = "Année Max")
}
Spacer(modifier = Modifier.height(8.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center
) {
Texte_saisie(valeur_label = "Début taux")
Spacer(modifier = Modifier.width(10.dp))
Texte_saisie(valeur_label = "Fin Taux")
}
Spacer(modifier = Modifier.height(8.dp))
Row (
modifier=Modifier.fillMaxWidth()
) {
Text(text = "Valeur : " + valeur_texte_saisie1.toString())
}
}
}
@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
Textview_test1Theme {
Sommaire()
}
}
Aperçu de l’activité :

KOTLIN : personnaliser ses composants