UI Responsive y Adaptación a Diferentes Pantallas

Por: Artiko
defolduiresponsivemobilepantallasgui

UI Responsive y Adaptación a Diferentes Pantallas

Crear interfaces que se adapten perfectamente a cualquier dispositivo móvil es fundamental para el éxito de tu juego. En esta guía aprenderás a implementar UI responsive en Defold.

Conceptos Fundamentales

Resoluciones y Densidades de Pantalla

Resoluciones Comunes

-- screen_resolutions.lua
local M = {}

M.COMMON_RESOLUTIONS = {
    -- iPhone
    {name = "iPhone 5", width = 320, height = 568, ratio = 16/9},
    {name = "iPhone 6/7/8", width = 375, height = 667, ratio = 16/9},
    {name = "iPhone 6+/7+/8+", width = 414, height = 736, ratio = 16/9},
    {name = "iPhone X/XS", width = 375, height = 812, ratio = 19.5/9},
    {name = "iPhone XR", width = 414, height = 896, ratio = 19.5/9},
    {name = "iPhone 12/13/14", width = 390, height = 844, ratio = 19.5/9},
    {name = "iPhone 12/13/14 Pro Max", width = 428, height = 926, ratio = 19.5/9},

    -- Android
    {name = "Android Small", width = 320, height = 480, ratio = 3/2},
    {name = "Android Medium", width = 360, height = 640, ratio = 16/9},
    {name = "Android Large", width = 411, height = 731, ratio = 16/9},
    {name = "Android XL", width = 412, height = 892, ratio = 18.5/9},

    -- Tablets
    {name = "iPad", width = 768, height = 1024, ratio = 4/3},
    {name = "iPad Pro", width = 834, height = 1194, ratio = 4/3},
    {name = "Android Tablet", width = 800, height = 1280, ratio = 16/10}
}

function M.get_device_info()
    local width = tonumber(sys.get_config("display.width"))
    local height = tonumber(sys.get_config("display.height"))
    local ratio = width / height

    return {
        width = width,
        height = height,
        ratio = ratio,
        is_tablet = width > 600 or height > 600
    }
end

return M

Sistema de Anchors y Pivots

Configuración de Anchors

-- anchor_system.script
local M = {}

-- Tipos de anchor predefinidos
M.ANCHOR_TYPES = {
    TOP_LEFT = {anchor = gui.ANCHOR_NONE, pivot = gui.PIVOT_NW},
    TOP_CENTER = {anchor = gui.ANCHOR_TOP, pivot = gui.PIVOT_N},
    TOP_RIGHT = {anchor = gui.ANCHOR_NONE, pivot = gui.PIVOT_NE},

    CENTER_LEFT = {anchor = gui.ANCHOR_LEFT, pivot = gui.PIVOT_W},
    CENTER = {anchor = gui.ANCHOR_CENTER, pivot = gui.PIVOT_CENTER},
    CENTER_RIGHT = {anchor = gui.ANCHOR_RIGHT, pivot = gui.PIVOT_E},

    BOTTOM_LEFT = {anchor = gui.ANCHOR_NONE, pivot = gui.PIVOT_SW},
    BOTTOM_CENTER = {anchor = gui.ANCHOR_BOTTOM, pivot = gui.PIVOT_S},
    BOTTOM_RIGHT = {anchor = gui.ANCHOR_NONE, pivot = gui.PIVOT_SE}
}

function M.set_anchor(node, anchor_type)
    local config = M.ANCHOR_TYPES[anchor_type]
    if config then
        gui.set_anchor(node, config.anchor)
        gui.set_pivot(node, config.pivot)
    end
end

function M.position_at_anchor(node, anchor_type, offset_x, offset_y)
    M.set_anchor(node, anchor_type)

    local screen_width = tonumber(sys.get_config("display.width"))
    local screen_height = tonumber(sys.get_config("display.height"))

    local x, y = 0, 0

    if anchor_type == "TOP_LEFT" then
        x, y = offset_x, screen_height - offset_y
    elseif anchor_type == "TOP_RIGHT" then
        x, y = screen_width - offset_x, screen_height - offset_y
    elseif anchor_type == "BOTTOM_LEFT" then
        x, y = offset_x, offset_y
    elseif anchor_type == "BOTTOM_RIGHT" then
        x, y = screen_width - offset_x, offset_y
    end

    gui.set_position(node, vmath.vector3(x, y, 0))
end

return M

Layout Responsivo

1. Sistema de Grid Flexible

Grid Layout Manager

-- grid_layout.lua
local M = {}

function M.create_grid(parent_node, rows, cols, padding, spacing)
    local parent_size = gui.get_size(parent_node)
    local available_width = parent_size.x - (padding.left + padding.right) - (spacing.x * (cols - 1))
    local available_height = parent_size.y - (padding.top + padding.bottom) - (spacing.y * (rows - 1))

    local cell_width = available_width / cols
    local cell_height = available_height / rows

    local grid = {
        parent = parent_node,
        rows = rows,
        cols = cols,
        cell_width = cell_width,
        cell_height = cell_height,
        padding = padding,
        spacing = spacing,
        cells = {}
    }

    return grid
end

function M.add_to_grid(grid, node, row, col, span_rows, span_cols)
    span_rows = span_rows or 1
    span_cols = span_cols or 1

    local start_x = grid.padding.left + (col - 1) * (grid.cell_width + grid.spacing.x)
    local start_y = grid.padding.top + (row - 1) * (grid.cell_height + grid.spacing.y)

    local width = grid.cell_width * span_cols + grid.spacing.x * (span_cols - 1)
    local height = grid.cell_height * span_rows + grid.spacing.y * (span_rows - 1)

    gui.set_position(node, vmath.vector3(start_x, start_y, 0))
    gui.set_size(node, vmath.vector3(width, height, 0))

    grid.cells[row .. "_" .. col] = {
        node = node,
        row = row,
        col = col,
        span_rows = span_rows,
        span_cols = span_cols
    }
end

function M.update_grid(grid)
    local parent_size = gui.get_size(grid.parent)
    local available_width = parent_size.x - (grid.padding.left + grid.padding.right) - (grid.spacing.x * (grid.cols - 1))
    local available_height = parent_size.y - (grid.padding.top + grid.padding.bottom) - (grid.spacing.y * (grid.rows - 1))

    grid.cell_width = available_width / grid.cols
    grid.cell_height = available_height / grid.rows

    -- Reposicionar todos los elementos
    for key, cell in pairs(grid.cells) do
        M.add_to_grid(grid, cell.node, cell.row, cell.col, cell.span_rows, cell.span_cols)
    end
end

return M

2. Safe Area y Notch Support

Safe Area Manager

-- safe_area.script
local M = {}

function M.get_safe_area()
    local screen_width = tonumber(sys.get_config("display.width"))
    local screen_height = tonumber(sys.get_config("display.height"))

    -- Valores por defecto
    local safe_area = {
        top = 0,
        bottom = 0,
        left = 0,
        right = 0
    }

    -- Detectar iPhone X y modelos posteriores
    local sys_info = sys.get_sys_info()
    if sys_info.system_name == "iPhone OS" then
        local device_model = sys_info.device_model

        -- iPhone X, XS, XR, 11, 12, 13, 14 series
        if string.find(device_model, "iPhone1[0-9],") or
           string.find(device_model, "iPhone11,") or
           string.find(device_model, "iPhone12,") or
           string.find(device_model, "iPhone13,") or
           string.find(device_model, "iPhone14,") then

            if screen_height > screen_width then
                -- Portrait
                safe_area.top = 44
                safe_area.bottom = 34
            else
                -- Landscape
                safe_area.left = 44
                safe_area.right = 44
            end
        end
    end

    return safe_area
end

function M.apply_safe_area_to_node(node, margin_top, margin_bottom, margin_left, margin_right)
    local safe_area = M.get_safe_area()
    local screen_width = tonumber(sys.get_config("display.width"))
    local screen_height = tonumber(sys.get_config("display.height"))

    margin_top = margin_top or 0
    margin_bottom = margin_bottom or 0
    margin_left = margin_left or 0
    margin_right = margin_right or 0

    local new_width = screen_width - safe_area.left - safe_area.right - margin_left - margin_right
    local new_height = screen_height - safe_area.top - safe_area.bottom - margin_top - margin_bottom
    local new_x = safe_area.left + margin_left
    local new_y = safe_area.bottom + margin_bottom

    gui.set_position(node, vmath.vector3(new_x, new_y, 0))
    gui.set_size(node, vmath.vector3(new_width, new_height, 0))
end

return M

Adaptación por Orientación

1. Detector de Orientación

Orientation Manager

-- orientation_manager.script
local M = {}

M.ORIENTATION = {
    PORTRAIT = "portrait",
    LANDSCAPE = "landscape",
    PORTRAIT_UPSIDE_DOWN = "portrait_upside_down",
    LANDSCAPE_LEFT = "landscape_left",
    LANDSCAPE_RIGHT = "landscape_right"
}

function init(self)
    self.current_orientation = M.get_orientation()
    self.orientation_callbacks = {}

    -- Suscribirse a cambios de ventana
    window.set_listener(function(self, event, data)
        if event == window.WINDOW_EVENT_RESIZED then
            M.check_orientation_change(self)
        end
    end)
end

function M.get_orientation()
    local width = tonumber(sys.get_config("display.width"))
    local height = tonumber(sys.get_config("display.height"))

    if height > width then
        return M.ORIENTATION.PORTRAIT
    else
        return M.ORIENTATION.LANDSCAPE
    end
end

function M.check_orientation_change(self)
    local new_orientation = M.get_orientation()

    if new_orientation ~= self.current_orientation then
        local old_orientation = self.current_orientation
        self.current_orientation = new_orientation

        -- Notificar a todos los callbacks registrados
        for _, callback in ipairs(self.orientation_callbacks) do
            callback(new_orientation, old_orientation)
        end

        msg.post(".", "orientation_changed", {
            new_orientation = new_orientation,
            old_orientation = old_orientation
        })
    end
end

function M.register_callback(callback)
    table.insert(self.orientation_callbacks, callback)
end

function M.is_portrait()
    return M.get_orientation() == M.ORIENTATION.PORTRAIT
end

function M.is_landscape()
    return M.get_orientation() == M.ORIENTATION.LANDSCAPE
end

return M

2. Layout Adaptive por Orientación

Adaptive Layout

-- adaptive_layout.gui_script
local orientation_manager = require "main.orientation_manager"
local grid_layout = require "main.grid_layout"

function init(self)
    self.portrait_layout = {}
    self.landscape_layout = {}

    -- Configurar layouts por orientación
    self:setup_portrait_layout()
    self:setup_landscape_layout()

    -- Aplicar layout inicial
    self:apply_current_layout()

    -- Registrar callback para cambios de orientación
    orientation_manager.register_callback(function(new_orientation, old_orientation)
        self:apply_current_layout()
    end)
end

function setup_portrait_layout(self)
    -- Layout vertical para portrait
    self.portrait_layout = {
        header = {
            anchor = "TOP_CENTER",
            size = {width = "100%", height = 120},
            position = {x = 0, y = -20}
        },
        main_content = {
            anchor = "CENTER",
            size = {width = "90%", height = "60%"},
            position = {x = 0, y = -40}
        },
        bottom_bar = {
            anchor = "BOTTOM_CENTER",
            size = {width = "100%", height = 100},
            position = {x = 0, y = 20}
        }
    }
end

function setup_landscape_layout(self)
    -- Layout horizontal para landscape
    self.landscape_layout = {
        header = {
            anchor = "TOP_CENTER",
            size = {width = "100%", height = 80},
            position = {x = 0, y = -10}
        },
        main_content = {
            anchor = "CENTER",
            size = {width = "70%", height = "70%"},
            position = {x = 0, y = 0}
        },
        side_panel = {
            anchor = "CENTER_RIGHT",
            size = {width = "25%", height = "70%"},
            position = {x = -20, y = 0}
        },
        bottom_bar = {
            anchor = "BOTTOM_CENTER",
            size = {width = "100%", height = 60},
            position = {x = 0, y = 10}
        }
    }
end

function apply_current_layout(self)
    local layout = orientation_manager.is_portrait() and self.portrait_layout or self.landscape_layout

    for element_name, config in pairs(layout) do
        local node = gui.get_node(element_name)
        if node then
            self:apply_layout_config(node, config)
        end
    end

    -- Animación suave de transición
    gui.animate(gui.get_node("root"), gui.PROP_COLOR, vmath.vector4(1,1,1,1), gui.EASING_OUTQUAD, 0.3)
end

function apply_layout_config(self, node, config)
    local screen_width = tonumber(sys.get_config("display.width"))
    local screen_height = tonumber(sys.get_config("display.height"))

    -- Calcular tamaño
    local width = config.size.width
    local height = config.size.height

    if type(width) == "string" and string.find(width, "%%") then
        width = screen_width * (tonumber(string.sub(width, 1, -2)) / 100)
    end

    if type(height) == "string" and string.find(height, "%%") then
        height = screen_height * (tonumber(string.sub(height, 1, -2)) / 100)
    end

    gui.set_size(node, vmath.vector3(width, height, 0))

    -- Aplicar anchor y posición
    if config.anchor then
        anchor_system.set_anchor(node, config.anchor)
    end

    if config.position then
        local pos = gui.get_position(node)
        pos.x = pos.x + config.position.x
        pos.y = pos.y + config.position.y
        gui.set_position(node, pos)
    end
end

Escalado de UI

1. Escalado Inteligente

Scale Manager

-- scale_manager.lua
local M = {}

M.SCALE_MODES = {
    FIT = "fit",           -- Ajustar manteniendo proporción
    FILL = "fill",         -- Llenar toda la pantalla
    STRETCH = "stretch",   -- Estirar sin mantener proporción
    LETTERBOX = "letterbox" -- Barras negras para mantener proporción
}

function M.calculate_scale(target_width, target_height, scale_mode)
    local screen_width = tonumber(sys.get_config("display.width"))
    local screen_height = tonumber(sys.get_config("display.height"))

    local scale_x = screen_width / target_width
    local scale_y = screen_height / target_height

    local scale = 1
    local offset_x, offset_y = 0, 0

    if scale_mode == M.SCALE_MODES.FIT then
        scale = math.min(scale_x, scale_y)

    elseif scale_mode == M.SCALE_MODES.FILL then
        scale = math.max(scale_x, scale_y)

    elseif scale_mode == M.SCALE_MODES.STRETCH then
        -- No usar escala uniforme, aplicar diferentes escalas por eje
        return {
            scale_x = scale_x,
            scale_y = scale_y,
            offset_x = 0,
            offset_y = 0
        }

    elseif scale_mode == M.SCALE_MODES.LETTERBOX then
        scale = math.min(scale_x, scale_y)
        offset_x = (screen_width - target_width * scale) / 2
        offset_y = (screen_height - target_height * scale) / 2
    end

    return {
        scale = scale,
        offset_x = offset_x,
        offset_y = offset_y
    }
end

function M.apply_scale_to_node(node, scale_info)
    if scale_info.scale then
        gui.set_scale(node, vmath.vector3(scale_info.scale, scale_info.scale, 1))
    else
        gui.set_scale(node, vmath.vector3(scale_info.scale_x, scale_info.scale_y, 1))
    end

    local pos = gui.get_position(node)
    pos.x = pos.x + scale_info.offset_x
    pos.y = pos.y + scale_info.offset_y
    gui.set_position(node, pos)
end

return M

2. Escalado de Texto Dinámico

Dynamic Text Scaler

-- text_scaler.lua
local M = {}

function M.auto_fit_text(text_node, max_width, max_height, min_font_size, max_font_size)
    min_font_size = min_font_size or 12
    max_font_size = max_font_size or 48

    local text = gui.get_text(text_node)
    local font = gui.get_font(text_node)

    -- Búsqueda binaria para encontrar el tamaño óptimo
    local low = min_font_size
    local high = max_font_size
    local best_size = min_font_size

    while low <= high do
        local mid = math.floor((low + high) / 2)
        gui.set_font_size(text_node, mid)

        local metrics = gui.get_text_metrics(font, text, max_width, true, mid)

        if metrics.width <= max_width and metrics.height <= max_height then
            best_size = mid
            low = mid + 1
        else
            high = mid - 1
        end
    end

    gui.set_font_size(text_node, best_size)
    return best_size
end

function M.scale_text_for_device(text_node, base_size)
    local screen_width = tonumber(sys.get_config("display.width"))
    local base_width = 375 -- iPhone 6/7/8 como referencia

    local scale_factor = screen_width / base_width
    local new_size = math.floor(base_size * scale_factor)

    -- Limitar el rango de tamaños
    new_size = math.max(12, math.min(48, new_size))

    gui.set_font_size(text_node, new_size)
    return new_size
end

return M

Componentes UI Responsivos

1. Botón Responsivo

Responsive Button

-- responsive_button.gui_script
local scale_manager = require "main.scale_manager"
local text_scaler = require "main.text_scaler"

function init(self)
    self.button_node = gui.get_node("button")
    self.label_node = gui.get_node("button_label")
    self.icon_node = gui.get_node("button_icon")

    -- Configuración base
    self.base_size = gui.get_size(self.button_node)
    self.base_font_size = gui.get_font_size(self.label_node)

    self:setup_responsive_behavior()
end

function setup_responsive_behavior(self)
    -- Adaptar según el tamaño de pantalla
    local screen_width = tonumber(sys.get_config("display.width"))
    local device_scale = screen_width / 375 -- Base iPhone 6/7/8

    -- Escalar botón
    local new_width = self.base_size.x * device_scale
    local new_height = self.base_size.y * device_scale
    gui.set_size(self.button_node, vmath.vector3(new_width, new_height, 0))

    -- Escalar texto
    text_scaler.scale_text_for_device(self.label_node, self.base_font_size)

    -- Escalar icono si existe
    if self.icon_node then
        local icon_size = gui.get_size(self.icon_node)
        gui.set_size(self.icon_node, vmath.vector3(
            icon_size.x * device_scale,
            icon_size.y * device_scale,
            0
        ))
    end
end

function on_input(self, action_id, action)
    if action_id == hash("touch") and gui.pick_node(self.button_node, action.x, action.y) then
        if action.pressed then
            -- Efecto visual de presión
            gui.animate(self.button_node, gui.PROP_SCALE, vmath.vector3(0.95, 0.95, 1),
                       gui.EASING_OUTQUAD, 0.1)
        elseif action.released then
            -- Volver al tamaño normal
            gui.animate(self.button_node, gui.PROP_SCALE, vmath.vector3(1, 1, 1),
                       gui.EASING_OUTBACK, 0.2)

            -- Ejecutar acción del botón
            msg.post("main:/controller", "button_pressed", {button_id = "responsive_button"})
        end
        return true
    end
    return false
end

2. Lista Responsiva

Responsive List

-- responsive_list.gui_script
local grid_layout = require "main.grid_layout"

function init(self)
    self.list_container = gui.get_node("list_container")
    self.item_template = gui.get_node("item_template")
    self.items = {}

    -- Ocultar template
    gui.set_enabled(self.item_template, false)

    self:setup_responsive_list()
end

function setup_responsive_list(self)
    local screen_width = tonumber(sys.get_config("display.width"))
    local container_size = gui.get_size(self.list_container)

    -- Calcular cuántos elementos pueden caber por fila
    local item_width = 150
    local spacing = 20
    local cols = math.floor((container_size.x - spacing) / (item_width + spacing))
    cols = math.max(1, cols) -- Mínimo 1 columna

    -- Ajustar el ancho de los elementos para llenar el espacio disponible
    local available_width = container_size.x - (spacing * (cols + 1))
    local adjusted_item_width = available_width / cols

    self.grid_config = {
        cols = cols,
        item_width = adjusted_item_width,
        item_height = 80,
        spacing_x = spacing,
        spacing_y = 20
    }

    print("Responsive list setup: " .. cols .. " columns, item width: " .. adjusted_item_width)
end

function add_item(self, item_data)
    local item_node = gui.clone(self.item_template)
    gui.set_enabled(item_node, true)
    gui.set_parent(item_node, self.list_container)

    -- Configurar contenido del item
    local label_node = gui.get_node("label", item_node)
    if label_node then
        gui.set_text(label_node, item_data.text or "Item")
    end

    -- Calcular posición en grid
    local item_index = #self.items
    local row = math.floor(item_index / self.grid_config.cols)
    local col = item_index % self.grid_config.cols

    local x = self.grid_config.spacing_x + col * (self.grid_config.item_width + self.grid_config.spacing_x)
    local y = -self.grid_config.spacing_y - row * (self.grid_config.item_height + self.grid_config.spacing_y)

    gui.set_position(item_node, vmath.vector3(x, y, 0))
    gui.set_size(item_node, vmath.vector3(self.grid_config.item_width, self.grid_config.item_height, 0))

    table.insert(self.items, {
        node = item_node,
        data = item_data,
        row = row,
        col = col
    })

    return item_node
end

function on_message(self, message_id, message, sender)
    if message_id == hash("orientation_changed") then
        self:setup_responsive_list()
        self:relayout_items()
    end
end

function relayout_items(self)
    for i, item in ipairs(self.items) do
        local row = math.floor((i-1) / self.grid_config.cols)
        local col = (i-1) % self.grid_config.cols

        local x = self.grid_config.spacing_x + col * (self.grid_config.item_width + self.grid_config.spacing_x)
        local y = -self.grid_config.spacing_y - row * (self.grid_config.item_height + self.grid_config.spacing_y)

        gui.animate(item.node, gui.PROP_POSITION, vmath.vector3(x, y, 0), gui.EASING_OUTQUAD, 0.3)
        gui.animate(item.node, gui.PROP_SIZE,
                   vmath.vector3(self.grid_config.item_width, self.grid_config.item_height, 0),
                   gui.EASING_OUTQUAD, 0.3)
    end
end

Testing y Debug

1. Debug de Resoluciones

Resolution Debugger

-- resolution_debugger.gui_script
function init(self)
    self.debug_enabled = sys.get_config("project.debug", "0") == "1"

    if self.debug_enabled then
        self:create_debug_overlay()
    end
end

function create_debug_overlay(self)
    -- Crear overlay de información
    local overlay = gui.new_box_node(vmath.vector3(0, 0, 0), vmath.vector3(300, 200, 0))
    gui.set_color(overlay, vmath.vector4(0, 0, 0, 0.7))
    gui.set_anchor(overlay, gui.ANCHOR_TOP)
    gui.set_pivot(overlay, gui.PIVOT_NW)

    -- Texto de información
    local info_text = gui.new_text_node(vmath.vector3(10, -10, 0), "")
    gui.set_parent(info_text, overlay)
    gui.set_anchor(info_text, gui.ANCHOR_NONE)
    gui.set_pivot(info_text, gui.PIVOT_NW)
    gui.set_color(info_text, vmath.vector4(1, 1, 1, 1))

    self.debug_overlay = overlay
    self.debug_text = info_text

    self:update_debug_info()
end

function update_debug_info(self)
    if not self.debug_enabled then return end

    local screen_width = tonumber(sys.get_config("display.width"))
    local screen_height = tonumber(sys.get_config("display.height"))
    local ratio = screen_width / screen_height
    local sys_info = sys.get_sys_info()

    local info = string.format(
        "Resolución: %dx%d\n" ..
        "Ratio: %.2f:1\n" ..
        "Plataforma: %s\n" ..
        "Dispositivo: %s\n" ..
        "DPI: %d\n" ..
        "Orientación: %s",
        screen_width, screen_height,
        ratio,
        sys_info.system_name,
        sys_info.device_model,
        sys_info.dpi or 0,
        orientation_manager.get_orientation()
    )

    gui.set_text(self.debug_text, info)
end

function update(self, dt)
    if self.debug_enabled then
        self:update_debug_info()
    end
end

2. Test de Responsive

Responsive Tester

-- responsive_tester.script
function init(self)
    self.test_resolutions = {
        {name = "iPhone 5", width = 320, height = 568},
        {name = "iPhone 8", width = 375, height = 667},
        {name = "iPhone X", width = 375, height = 812},
        {name = "Pixel 3", width = 393, height = 786},
        {name = "iPad", width = 768, height = 1024}
    }

    self.current_test = 1
end

function test_resolution(self, resolution)
    print("Testing resolution: " .. resolution.name .. " (" .. resolution.width .. "x" .. resolution.height .. ")")

    -- Simular cambio de resolución (solo para testing)
    msg.post("@render:", "change_resolution", {
        width = resolution.width,
        height = resolution.height
    })

    -- Notificar a todos los GUI scripts sobre el cambio
    msg.post("@system:", "test_resolution", resolution)
end

function on_input(self, action_id, action)
    if action.pressed then
        if action_id == hash("test_next") then
            self.current_test = self.current_test + 1
            if self.current_test > #self.test_resolutions then
                self.current_test = 1
            end

            self:test_resolution(self.test_resolutions[self.current_test])
        end
    end
end

Mejores Prácticas

1. Checklist de UI Responsiva

  • ✅ Usar anchors y pivots apropiados
  • ✅ Implementar safe area para dispositivos con notch
  • ✅ Probar en múltiples resoluciones
  • ✅ Optimizar para ambas orientaciones
  • ✅ Escalar texto dinámicamente
  • ✅ Usar layouts flexibles en lugar de posiciones fijas

2. Optimizaciones de Rendimiento

  • ✅ Reutilizar nodos GUI cuando sea posible
  • ✅ Usar object pooling para listas dinámicas
  • ✅ Minimizar animaciones simultáneas
  • ✅ Optimizar atlas de texturas para diferentes densidades

3. Testing en Dispositivos Reales

  • ✅ Probar en dispositivos con diferentes tamaños
  • ✅ Verificar legibilidad de texto en pantallas pequeñas
  • ✅ Comprobar áreas táctiles mínimas (44pt en iOS)
  • ✅ Validar comportamiento en orientación landscape

Esta guía te proporciona las herramientas necesarias para crear interfaces verdaderamente responsivas que se adapten perfectamente a cualquier dispositivo móvil.