← Volver al listado de tecnologías

Widgets y Layouts

Por: SiempreListo
kivywidgetslayoutsui

Jerarquía de Widgets

Widget (base)
├── Layout
│   ├── BoxLayout
│   ├── GridLayout
│   ├── FloatLayout
│   ├── RelativeLayout
│   ├── AnchorLayout
│   ├── StackLayout
│   └── PageLayout
└── Widgets de UI
    ├── Label, Button
    ├── TextInput
    ├── Image
    ├── Slider, Switch
    └── ...

Layouts Principales

BoxLayout

Organiza widgets en fila o columna.

BoxLayout:
    orientation: 'vertical'  # o 'horizontal'
    spacing: 10
    padding: 20

    Button:
        text: 'Botón 1'
    Button:
        text: 'Botón 2'
    Button:
        text: 'Botón 3'

GridLayout

Organiza en cuadrícula.

GridLayout:
    cols: 3           # Columnas fijas
    rows: 2           # Filas fijas (opcional)
    spacing: 5
    padding: 10

    Button:
        text: '1'
    Button:
        text: '2'
    Button:
        text: '3'
    Button:
        text: '4'

FloatLayout

Posicionamiento libre con coordenadas.

FloatLayout:
    Button:
        text: 'Centro'
        size_hint: 0.3, 0.2
        pos_hint: {'center_x': 0.5, 'center_y': 0.5}

    Button:
        text: 'Esquina'
        size_hint: 0.2, 0.1
        pos_hint: {'right': 1, 'top': 1}

AnchorLayout

Ancla widgets a posiciones específicas.

AnchorLayout:
    anchor_x: 'center'  # 'left', 'center', 'right'
    anchor_y: 'center'  # 'top', 'center', 'bottom'

    Button:
        text: 'Centrado'
        size_hint: 0.5, 0.3

StackLayout

Apila widgets con wrap automático.

StackLayout:
    orientation: 'lr-tb'  # left-right, top-bottom
    spacing: 5

    Button:
        text: 'A'
        size_hint: 0.3, 0.2
    Button:
        text: 'B'
        size_hint: 0.3, 0.2
    # Continúa en siguiente fila si no cabe

RelativeLayout

Como FloatLayout pero posiciones relativas al padre.

RelativeLayout:
    Button:
        text: 'Relativo'
        pos_hint: {'x': 0.1, 'y': 0.1}
        size_hint: 0.3, 0.2

Size Hints

Controlan el tamaño relativo de widgets.

PropiedadDescripción
size_hint(ancho, alto) como fracción del padre
size_hint_xSolo ancho relativo
size_hint_ySolo alto relativo
BoxLayout:
    Button:
        text: '30%'
        size_hint_x: 0.3
    Button:
        text: '70%'
        size_hint_x: 0.7

Tamaño Fijo

Button:
    size_hint: None, None
    size: 200, 50
    # o
    width: 200
    height: 50

Pos Hints (FloatLayout)

ClaveDescripción
x, yPosición desde esquina inferior izquierda
right, topPosición desde esquina superior derecha
center_x, center_yCentro del widget
FloatLayout:
    Button:
        text: 'Superior Derecha'
        size_hint: 0.3, 0.1
        pos_hint: {'right': 1, 'top': 1}

    Button:
        text: 'Centro'
        size_hint: 0.4, 0.2
        pos_hint: {'center_x': 0.5, 'center_y': 0.5}

    Button:
        text: 'Inferior Izquierda'
        size_hint: 0.3, 0.1
        pos_hint: {'x': 0, 'y': 0}

Widgets de Entrada

TextInput

TextInput:
    hint_text: 'Placeholder'
    multiline: False
    password: True
    input_filter: 'int'  # Solo números
    max_chars: 10

Slider

BoxLayout:
    orientation: 'vertical'

    Slider:
        id: slider
        min: 0
        max: 100
        value: 50
        step: 1

    Label:
        text: str(int(slider.value))

Switch

BoxLayout:
    Switch:
        id: switch
        active: True

    Label:
        text: 'ON' if switch.active else 'OFF'

Spinner (Dropdown)

Spinner:
    text: 'Selecciona'
    values: ['Opción 1', 'Opción 2', 'Opción 3']
    on_text: print(self.text)

CheckBox

BoxLayout:
    CheckBox:
        id: check
        active: False

    Label:
        text: 'Acepto términos' if check.active else 'No acepto'

Widgets de Visualización

Image

Image:
    source: 'ruta/imagen.png'
    allow_stretch: True
    keep_ratio: True

AsyncImage (carga remota)

AsyncImage:
    source: 'https://ejemplo.com/imagen.jpg'

ProgressBar

ProgressBar:
    max: 100
    value: 75

Widgets Contenedores

ScrollView

ScrollView:
    do_scroll_x: False
    do_scroll_y: True

    GridLayout:
        cols: 1
        size_hint_y: None
        height: self.minimum_height

        # Widgets que exceden la pantalla
        Button:
            text: 'Item 1'
            size_hint_y: None
            height: 50

TabbedPanel

TabbedPanel:
    do_default_tab: False

    TabbedPanelItem:
        text: 'Tab 1'
        BoxLayout:
            Label:
                text: 'Contenido Tab 1'

    TabbedPanelItem:
        text: 'Tab 2'
        BoxLayout:
            Label:
                text: 'Contenido Tab 2'

Accordion

Accordion:
    orientation: 'vertical'

    AccordionItem:
        title: 'Sección 1'
        Label:
            text: 'Contenido 1'

    AccordionItem:
        title: 'Sección 2'
        Label:
            text: 'Contenido 2'
from kivy.uix.popup import Popup
from kivy.uix.label import Label

popup = Popup(
    title='Alerta',
    content=Label(text='Mensaje'),
    size_hint=(0.8, 0.4)
)
popup.open()

Widgets Personalizados

from kivy.uix.boxlayout import BoxLayout
from kivy.properties import StringProperty

class Card(BoxLayout):
    titulo = StringProperty('')
    descripcion = StringProperty('')
<Card>:
    orientation: 'vertical'
    padding: 10
    spacing: 5

    canvas.before:
        Color:
            rgba: 0.2, 0.2, 0.2, 1
        RoundedRectangle:
            pos: self.pos
            size: self.size
            radius: [10]

    Label:
        text: root.titulo
        font_size: '18sp'
        bold: True
        size_hint_y: 0.3

    Label:
        text: root.descripcion
        font_size: '14sp'
        size_hint_y: 0.7

Testing de Layouts

# test_layouts.py
import unittest
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.gridlayout import GridLayout
from kivy.uix.button import Button

class TestLayouts(unittest.TestCase):
    def test_boxlayout_orientacion(self):
        box = BoxLayout(orientation='vertical')
        self.assertEqual(box.orientation, 'vertical')

    def test_gridlayout_columnas(self):
        grid = GridLayout(cols=3)
        for i in range(6):
            grid.add_widget(Button(text=str(i)))
        self.assertEqual(len(grid.children), 6)

    def test_size_hint(self):
        btn = Button(size_hint=(0.5, 0.5))
        self.assertEqual(btn.size_hint, (0.5, 0.5))

    def test_add_remove_widget(self):
        box = BoxLayout()
        btn = Button()
        box.add_widget(btn)
        self.assertIn(btn, box.children)
        box.remove_widget(btn)
        self.assertNotIn(btn, box.children)

if __name__ == '__main__':
    unittest.main()

Probar en Dispositivo con ADB

# Ver jerarquía de views (útil para debug de layouts)
adb shell dumpsys activity top | grep -A 20 "View Hierarchy"

# Activar mostrar límites de layout en dispositivo
adb shell setprop debug.layout true
adb shell service call activity 1599295570

# Capturar pantalla para verificar layout
adb exec-out screencap -p > layout_test.png

# Ver rendimiento de UI
adb shell dumpsys gfxinfo com.ejemplo.miapp

Resumen