El ScrollView
en Android te permite albergar una jerarquía de views con el fin de desplazar su contenido a lo largo de la pantalla, cuando sus dimensiones exceden el tamaño de la misma.
El usuario hará scroll a través de un gesto de swipe vertical u horizontal para revelar la información limitada por el tamaño del contenedor.
Ejemplo De ScrollView En Android
En este tutorial verás con ejemplos como usar scroll vertical, scroll horizontal y como anidación de scrolls. La siguiente imagen muestra la App de ilustración creada para comprender los conocimientos expuestos:
Descargar el proyecto Android Studio desde siguiente enlace:
Comencemos con el primer caso de scrolling.
Scrolling Vertical
Si la información que deseas proyectar verticalmente es más grande que la orientación de tu pantalla móvil, entonces envuelve el ViewGroup
del contenido con un objeto de la clase ScrollView
:
ScrollView
soporta un solo hijo como contexto de desplazamiento, por lo que la estructura general de tu diseño debe encontrarse en él.
Puedes añadir este elemento desde Android Studio yendo a Palette > Containers > ScrollView:
O recubre tu ViewGroup principal desde el layout con la etiqueta <ScrollView>
. El ejemplo que usamos para representar desplazamiento vertical se ve así:
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/scrollView"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
tools:context=".MainActivity">
<TextView
android:id="@+id/child_scroll"
android:layout_width="0dp"
android:layout_height="256dp"
android:background="@color/blue2"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/headline"
android:layout_width="0dp"
android:layout_height="40dp"
android:layout_marginTop="16dp"
android:background="@color/blue3"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/child_scroll" />
<TextView
android:id="@+id/indicator_1"
android:layout_width="0dp"
android:layout_height="80dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:background="@color/orange"
app:layout_constraintEnd_toStartOf="@+id/indicator_2"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/headline" />
<TextView
android:id="@+id/indicator_2"
android:layout_width="0dp"
android:layout_height="80dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:background="@color/orange2"
app:layout_constraintEnd_toStartOf="@+id/indicator_3"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/indicator_1"
app:layout_constraintTop_toBottomOf="@+id/headline" />
<TextView
android:id="@+id/indicator_3"
android:layout_width="0dp"
android:layout_height="80dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:background="@color/orange3"
app:layout_constraintEnd_toStartOf="@+id/indicator_4"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/indicator_2"
app:layout_constraintTop_toBottomOf="@+id/headline" />
<TextView
android:id="@+id/indicator_4"
android:layout_width="0dp"
android:layout_height="80dp"
android:layout_marginTop="16dp"
android:background="@color/orange4"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/indicator_3"
app:layout_constraintTop_toBottomOf="@+id/headline" />
<TextView
android:id="@+id/body_text"
android:layout_width="0dp"
android:layout_height="512dp"
android:layout_marginTop="16dp"
android:background="@color/blue4"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/indicator_1" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
El diseño anterior usa un tamaño de 512dp
en el último elemento body_text
con el fin de desbordar la capacidad de la pantalla. Al conectar elementos en el editor de layouts en Android Studio verás una zona punteada consciente del exceso:
Es necesario que uses wrap_content
en android:layout_height
del contenedor directo para ajustar la zona al scrolling.
Scrolling Horizontal
El mismo criterio aplica para los contenidos que abruman horizontalmente la pantalla del dispositivo. Usa la clase HorizontalScrollView
para proveer desplazamiento entre los límites del contenedor:
Al igual que ScrollView
, HorizontalScrollView
hereda de la clase FrameLayout
, por lo que la jerarquía a scrollear debe ser parte de un solo nodo.
Para lograr el resultado visto en el ejemplo de la imagen anterior, el layout ha sido recubierto por el desplazador horizontal de la siguiente manera:
<?xml version="1.0" encoding="utf-8"?>
<HorizontalScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/scroll"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:padding="16dp">
<TextView
android:id="@+id/piece1"
android:layout_width="90dp"
android:layout_height="90dp"
android:background="@color/blue1"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/block2"
android:layout_width="180dp"
android:layout_height="0dp"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:background="@color/blue2"
app:layout_constraintBottom_toBottomOf="@+id/piece1"
app:layout_constraintStart_toEndOf="@+id/piece1"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/block3"
android:layout_width="40dp"
android:layout_height="0dp"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:background="@color/blue3"
app:layout_constraintBottom_toBottomOf="@+id/piece1"
app:layout_constraintStart_toEndOf="@+id/block2"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/block4"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:background="@color/orange1"
app:layout_constraintStart_toEndOf="@+id/block3"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/block5"
android:layout_width="40dp"
android:layout_height="0dp"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:layout_marginTop="16dp"
android:background="@color/orange2"
app:layout_constraintBottom_toBottomOf="@+id/block3"
app:layout_constraintStart_toEndOf="@+id/block3"
app:layout_constraintTop_toBottomOf="@+id/block4" />
<TextView
android:id="@+id/block6"
android:layout_width="80dp"
android:layout_height="0dp"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:background="@color/orange3"
app:layout_constraintBottom_toBottomOf="@+id/block4"
app:layout_constraintStart_toEndOf="@+id/block4"
app:layout_constraintTop_toTopOf="@+id/block4"
app:layout_constraintVertical_bias="0.0" />
<TextView
android:id="@+id/block7"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@color/orange4"
app:layout_constraintBottom_toBottomOf="@+id/block5"
app:layout_constraintEnd_toEndOf="@+id/block8"
app:layout_constraintStart_toStartOf="@+id/block6"
app:layout_constraintTop_toTopOf="@+id/block5" />
<TextView
android:id="@+id/block8"
android:layout_width="80dp"
android:layout_height="40dp"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:layout_marginBottom="5dp"
android:background="@color/blue3"
app:layout_constraintBottom_toTopOf="@+id/block7"
app:layout_constraintStart_toEndOf="@+id/block6"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0" />
</androidx.constraintlayout.widget.ConstraintLayout>
</HorizontalScrollView>
El ViewGroup directo del HorizontalScrollView
debe asignar wrap_content
a su atributo android:layout_width
para mantener el dinamismo del deslizamiento interno.
Anidar Scrolls Con NestedScrollView
En el caso que requieras añadir un ScrollView
dentro de otro, transiciona directamente al uso de la clase NestedScrollView
. En nuestra app de ejemplo proyectamos un scroll vertical secundario que habita en el scroll principal:
Este componente puede actuar como padre o hijo en una jerarquía de scrolling. Además de que habilita los patrones de scrolling propuestos en el Material Design.
El siguiente layout muestra la solución con un NestedScrollView
haciendo de nodo principal del layout y otro como hijo anidado:
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/parent_scroll"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp">
<androidx.core.widget.NestedScrollView
android:id="@+id/child_scroll"
android:layout_width="match_parent"
android:layout_height="180dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/piece1"
android:layout_width="match_parent"
android:layout_height="90dp"
android:background="@color/blue1" />
<TextView
android:id="@+id/piece2"
android:layout_width="match_parent"
android:layout_height="90dp"
android:layout_marginTop="4dp"
android:background="@color/blue2" />
<TextView
android:id="@+id/piece3"
android:layout_width="match_parent"
android:layout_height="90dp"
android:layout_marginTop="4dp"
android:background="@color/blue3" />
<TextView
android:id="@+id/piece4"
android:layout_width="match_parent"
android:layout_height="90dp"
android:layout_marginTop="4dp"
android:background="@color/orange4" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
<TextView
android:id="@+id/headline"
android:layout_width="0dp"
android:layout_height="120dp"
android:layout_marginTop="16dp"
android:background="@color/orange1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/child_scroll" />
<TextView
android:id="@+id/body_text"
android:layout_width="0dp"
android:layout_height="512dp"
android:layout_marginTop="16dp"
android:background="@color/orange2"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/headline" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>
Como ves, el hijo directo del scroll padre es un ConstraintLayout
. Y el del child_scroll
es un LineaLayout
que expone cuatro piezas verticales.
Detectar Cambios De Scroll
El NestedScrollView
permite procesar los eventos de cambio de posición de los views que hacen parte del contenido desplazable. Por ejemplo, si deseamos cambiar el título de la Toolbar
con las coordenadas y desplegar un Toast
en el momento en que se llega al final del desplazamiento:
Este resultado se consigue con usando un observador OnScrollChangeListener
sobre el NestedScrollView
. Sobrescribe el método onScrollChange()
con las acciones a realizar cuando se detecte el cambio. Los parámetros que recibe son:
v
:NestedScrollView
al que le cambio la posición de scrollscrollX
,scrollY
: Valores actuales de x e yoldScrollX
,oldScrollY
: Valores previos en x e y
Teniendo esto claro, actualizar el título de la Toolbar
lo conseguirnos modificando la propiedad title
de la actividad con una plantilla de string que contenga a scrollX
y scrollY
:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val parentScroll: NestedScrollView = findViewById(R.id.parent_scroll)
parentScroll.setOnScrollChangeListener(
OnScrollChangeListener { v, scrollX, scrollY, _, _ ->
title = "Scroll ($scrollX, $scrollY)"
if (!v.canScrollVertically(1))
Toast.makeText(this, "Final", Toast.LENGTH_SHORT).show()
})
}
}
Por otro lado, podemos usar canScrollVertically()
para averiguar si el NestedScrollView
puede seguir desplazando el contenido hacia abajo (pasa un entero positivo) o arriba (pasa un entero negativo).
Como necesitamos determinar el final, entonces negamos la salida del método y construimos el Toast
al cumplirse.
Scrollear Hasta Una Posición Específica
Supongamos que deseamos desplazar lentamente alguno de nuestros layouts de ejemplos sin que el usuario realice alguna acción:
¿Cómo lograrlo?
Usa el método smoothScrollBy()
para indicar el desplazamiento en pixeles que se aplicarán programáticamente:
class MainActivity : AppCompatActivity() {
private val scope = MainScope()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val scrollView: ScrollView = findViewById(R.id.scrollView)
scrollView.doOnAttach {
beginToScroll(scrollView)
}
}
private fun beginToScroll(scrollView: ScrollView) {
scope.launch {
val scrollUnits = 50
while (scrollView.canScrollVertically(1)) {
scrollView.smoothScrollBy(0, scrollUnits)
delay(100)
}
}
}
}
Luego de obtener la referencia del ScrollView, usamos la función de extensión doOnAttach()
del framework. Esta operación nos permite iniciar una acción sobre el scroll view cuando esté preparado.
En este caso será la ejecución del método beginToScroll()
, el cual ejecuta una corrutina cuyo fin es desplazar el control hasta que ya no sea posible (canScrollVertically()
). Obviamente usamos delay()
para para que el efecto sea visible.
Nota: También existe el método smoothScrollTo()
para asignar la posición absoluta en vez del incremento. A su vez, existen variaciones de estos métodos sin «smooth», las cuales desplazan de inmediato.
Ocultar Barras De Scroll
Al desplazar el contenido de un widget de scroll se presentan barras de color gris para indicar el estado y avance del scroll:
Para ocultarlas usa las propiedades mutables isHorizontalScrollBarEnabled
y isVerticalScrollBarEnabled
en false
para deshabilitarlas.
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val scrollView: HorizontalScrollView = findViewById(R.id.scroll)
scrollView.isHorizontalScrollBarEnabled = false
}
}
O el atributo XML android:scrollbars
con none
:
<HorizontalScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/scroll"
android:layout_width="match_parent"
android:scrollbars="none"
android:layout_height="wrap_content">
Más Contenidos Android
Únete Al Discord De Develou
Si tienes problemas con el código de este tutorial, preguntas, recomendaciones o solo deseas discutir sobre desarrollo Android conmigo y otros desarrolladores, únete a la comunidad de Discord de Develou y siéntete libre de participar como gustes. ¡Te espero!