El componente SideBarContainer de HarmonyOS NEXT es una herramienta fundamental para diseñar interfaces que requieren navegación lateral persistente o bajo demanda. En aplicaciones multimedia, como un reproductor de música, este componente permite organizar colecciones de canciones y listas de reproducción de manera accesible sin interrumpir la vista principal de control.
Configuración del SideBarContainer
Este contenedor soporta tres modos de visualización principales que determinan cómo interactúa la barra lateral con el conetnido principal:
- Embed: La barra lateral se integra en el diseño y desplaza el contenido principal para compartir el espacio.
- Overlay: El menú lateral flota sobre el contenido principal, cubriéndolo parcialmente.
- Auto: El sistema decide el modo basado en las dimensiones y la orientación del dispositivo.
Para un reproductor de escritorio o tablet, el modo Embed suele ser el preferido, ya que permite al usuario gestionar su cola de reproducción mientras mantiene el control total de la pista actual.
Estructura Base del Reproductor
A continuación, se presenta una estructura optimizada para un reproductor de audio utilizando ArkTS. Hemos definido estados para controlar la visibilidad del menú, la pista actual y el estado de la reproducción.
@Entry
@Component
struct ReproductorAudio {
@State mostrarMenu: boolean = true
@State pistaActiva: number = 0
@State enReproduccion: boolean = false
@State tiempoTranscurrido: number = 0
@State nombreListaActual: string = 'Favoritos'
build() {
SideBarContainer(SideBarContainerType.Embed) {
// Sección 1: Navegación y Listas
this.ConstructorMenuLateral()
// Sección 2: Panel de Control Principal
this.ConstructorVistaPrincipal()
}
.controlButton({
icons: {
shown: $r('app.media.btn_cerrar_menu'),
hidden: $r('app.media.btn_abrir_menu'),
}
})
.sideBarWidth(280)
.minSideBarWidth(140)
.maxSideBarWidth(450)
.onChange((estado: boolean) => {
this.mostrarMenu = estado
})
}
@Builder ConstructorMenuLateral() {
// Definición del menú lateral
}
@Builder ConstructorVistaPrincipal() {
// Definición del área de reproducción
}
}
Gestión de Datos y Modelos
Para alimentar la interfaz, definimos interfaces claras que representen la estructura de la música y las colecciones.
interface InfoCancion {
titulo: string
interprete: string
portada: Resource
duracionSegundos: number
}
interface InfoAlbum {
titulo: string
imagen: Resource
totalPistas: number
}
// Datos de ejemplo
private catalogoPistas: InfoCancion[] = [
{ titulo: 'Luz de Luna', interprete: 'Artista A', portada: $r('app.media.cover1'), duracionSegundos: 215 },
{ titulo: 'Horizonte Azul', interprete: 'Artista B', portada: $r('app.media.cover2'), duracionSegundos: 198 },
{ titulo: 'Eco Eterno', interprete: 'Artista C', portada: $r('app.media.cover3'), duracionSegundos: 245 }
]
Interfaz del Menú Lateral: Listas y Cola
El menú lateral se divide en secciones jerárquicas: primero las colecciones globales y luego la cola de reproducción actual.
@Builder ConstructorMenuLateral() {
Column() {
Text('Mi Biblioteca')
.fontSize(22)
.fontWeight(FontWeight.Bold)
.margin({ top: 25, bottom: 15, left: 20 })
List() {
// Cabecera de sección
ListItem() {
Text('LISTAS DE REPRODUCCIÓN')
.fontSize(13)
.fontColor('#666666')
.margin({ left: 20, bottom: 10 })
}
// Renderizado de canciones en la cola
ForEach(this.catalogoPistas, (pista: InfoCancion, indice: number) => {
ListItem() {
Row() {
Stack() {
Image(pista.portada)
.width(45)
.height(45)
.borderRadius(6)
if (this.pistaActiva === indice && this.enReproduccion) {
Rect().fill('#80000000').width(45).height(45).borderRadius(6)
// Aquí se podría añadir una animación de ondas
}
}
.margin({ right: 12 })
Column() {
Text(pista.titulo)
.fontSize(15)
.fontColor(this.pistaActiva === indice ? '#007DFF' : '#1A1A1A')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text(pista.interprete)
.fontSize(12)
.fontColor('#999999')
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
}
.padding({ left: 15, right: 15, top: 10, bottom: 10 })
.backgroundColor(this.pistaActiva === indice ? '#F2F2F2' : Color.Transparent)
}
.onClick(() => {
this.pistaActiva = indice
this.tiempoTranscurrido = 0
this.enReproduccion = true
})
})
}
.layoutWeight(1)
}
.width('100%')
.height('100%')
.backgroundColor('#FAFAFA')
}
Contenido Principal: Reproductor y Controles
La vista principal utiliza el espacio restante para mostrar la información detallada de la pista y los controles de transporte.
@Builder ConstructorVistaPrincipal() {
Column() {
// Portada principal
Image(this.catalogoPistas[this.pistaActiva].portada)
.width(320)
.height(320)
.borderRadius(20)
.shadow({ radius: 15, color: '#20000000', offsetX: 0, offsetY: 10 })
.margin({ top: 60 })
Text(this.catalogoPistas[this.pistaActiva].titulo)
.fontSize(28)
.fontWeight(FontWeight.Bold)
.margin({ top: 30 })
Text(this.catalogoPistas[this.pistaActiva].interprete)
.fontSize(18)
.fontColor('#666666')
.margin({ top: 10 })
// Deslizador de progreso
Column() {
Slider({
value: this.tiempoTranscurrido,
min: 0,
max: this.catalogoPistas[this.pistaActiva].duracionSegundos,
style: SliderStyle.InSet
})
.blockColor('#007DFF')
.trackColor('#E5E5E5')
.selectedColor('#007DFF')
.width('85%')
.onChange((val: number) => {
this.tiempoTranscurrido = val
})
Row() {
Text(this.formatearTiempo(this.tiempoTranscurrido))
Blank()
Text(this.formatearTiempo(this.catalogoPistas[this.pistaActiva].duracionSegundos))
}
.width('85%')
.margin({ top: 5 })
}
.margin({ top: 40 })
// Botonera de control
Row() {
Button({ type: ButtonType.Circle }) {
Image($r('app.media.skip_back')).width(30)
}.backgroundColor(Color.Transparent).onClick(() => this.cambiarPista(-1))
Button({ type: ButtonType.Circle }) {
Image(this.enReproduccion ? $r('app.media.pause') : $r('app.media.play')).width(50)
}
.width(80).height(80)
.margin({ left: 30, right: 30 })
.backgroundColor('#007DFF')
.onClick(() => this.enReproduccion = !this.enReproduccion)
Button({ type: ButtonType.Circle }) {
Image($r('app.media.skip_forward')).width(30)
}.backgroundColor(Color.Transparent).onClick(() => this.cambiarPista(1))
}
.margin({ top: 50 })
}
.width('100%')
.height('100%')
}
Lógica de Apoyo
Para que la interfaz sea funcional, se requieren métodos de utilidad que gestionen el formato de tiempo y la lógica de navegación entre pistas.
private formatearTiempo(segundos Totales: number): string {
const min = Math.floor(segundosTotales / 60)
const seg = Math.floor(segundosTotales % 60)
return `${min}:${seg.toString().padStart(2, '0')}`
}
private cambiarPista(direccion: number) {
let nuevoIndice = this.pistaActiva + direccion
if (nuevoIndice < 0) {
nuevoIndice = this.catalogoPistas.length - 1
} else if (nuevoIndice >= this.catalogoPistas.length) {
nuevoIndice = 0
}
this.pistaActiva = nuevoIndice
this.tiempoTranscurrido = 0
this.enReproduccion = true
}
Consideraciones de Diseño
Al implementar este tipo de navegación, es importante considerar:
- Restricciones de Ancho: Utilizar
minSideBarWidthymaxSideBarWidthevita que el usuario colapse visualmente el menú o que este invada demasiado el área principal en pantallas muy grandes. - Retroalimentación Visual: El uso de cambios de color en el fondo (
backgroundColor) y en la fuente para el elemento activo ayuda al usuario a identificar rápidamente qué se está reproduciendo. - Optimización de Texto: En la barra lateral, el espacio es limitado. El uso de
textOverflowcon puntos suspensivos previene que títulos largos rompan la estructura delListItem.