Muy buenas, soy Miguel y hoy les traigo otro nuevo post.
Si ha iniciado muchos proyectos de Android, probablemente haya tenido que reinventar la rueda varias veces.
O quizás copió y pegó un código de un proyecto antiguo y dedicó algún tiempo a refactorizarlo para adaptarlo a su nuevo diseño.
Apuesto a que ha realizado o al menos visto una incorporación como esta:
Logotipo, título, texto de información, imagen, botón, indicador de página.
Índice
Hagamos uno – Otra vez
Veamos cómo podemos hacer una incorporación genérica como esta, y luego copiar y pegar todo en nuestros próximos proyectos.
Primero, vamos a necesitar algunas dependencias:
android { buildFeatures { dataBinding = true } } dependencies { //Can't live without these implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion") implementation("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion") //Data binding for viewpager implementation("me.tatarka.bindingcollectionadapter2:bindingcollectionadapter-viewpager2:4.0.0") //Some sweet data binding adapters and livedata extensions maintained by Shortcut 😉 implementation("com.github.shortcut:data-binding:1.0.0") implementation("com.github.c-b-h:lives:1.0.0") }
Haremos una nueva carpeta onboarding
con:
-
OnboardingPage.kt
-
OnboardingFragment.kt
-
OnboardingViewmodel
Y dos diseños en res / layout:
-
Onboarding_page.xml
-
Fragment_onboarding
Empezando por el fragmento
^
Nada especial aquí. Simplemente configurando el enlace y pretendiendo que ya tenemos un viewModel
y un gráfico de navegación.
class OnboardingFragment : Fragment() { private val viewModel: OnboardingViewModel by viewModel() override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? = FragmentOnboardingBinding.inflate(inflater, container, false).apply { lifecycleOwner = viewLifecycleOwner viewModel = this@OnboardingFragment.viewModel }.root override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) viewModel.onboardingComplete.observe(viewLifecycleOwner){ findNavController().navigate(OnboardingFragmentDirections.toMain()) } } }
Clase de modelo
Necesitamos una clase de datos para contener nuestras páginas:
data class OnboardingPage( val title: String, val infoText: String, val image: Drawable?, val buttonText: String )
Diseño de las páginas
Solo dejaremos un estilo muy simple. Puedes cambiarlo como quieras. Siempre que conserve las uniones, como: android:text="@onboardingPage.title"
<?xml version="1.0" encoding="utf-8"?> <layout 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"> <data> <variable name="onboardingPage" type="no.example.onboarding.OnboardingPage" /> </data> <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/onboarding_title" android:layout_width="0dp" android:layout_height="wrap_content" android:text="@{onboardingPage.title}" android:textAlignment="center" app:layout_constraintBottom_toTopOf="@id/onboarding_text" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="1" app:layout_constraintVertical_chainStyle="packed" tools:text="@string/onboarding_1_title" /> <TextView android:id="@+id/onboarding_text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{onboardingPage.text}" android:layout_marginTop="8dp" android:textAlignment="center" app:layout_constraintBottom_toTopOf="@id/onboarding_image" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/onboarding_title" tools:text="@string/onboarding_1_text" /> <ImageView android:id="@+id/onboarding_image" android:layout_width="wrap_content" android:layout_height="0dp" app:layout_constraintHeight_max="200dp" android:layout_marginBottom="16dp" android:padding="24dp" android:src="@{onboardingPage.image}" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/onboarding_text" /> </androidx.constraintlayout.widget.ConstraintLayout> </layout>
ViewModel
Necesitamos una lista de páginas, un enlace para ellas, una función para ir a la página siguiente y una forma de notificar al fragmento que la incorporación está completa, para que podamos navegar a otro lugar.
Probablemente también desee usar alguna librería DI para inyectar un servicio de preferencias y un contexto, si desea obtener recursos y guardar la finalización de la incorporación, pero eso está un poco fuera del alcance aquí.
class OnboardingViewModel( context: Context, private val prefs: PreferenceService ) : ViewModel() { } // To notify the fragment that the onboarding is complete private val _onboardingComplete = MutableLiveData<Boolean>() val onboardingComplete: LiveData<Boolean> get() = _onboardingComplete // To hold the view pager's current page val currentPage = MutableLiveData<Int>() // Some onboarding pages with some placeholder strings, you can add as many as you want. val pages = MutableLiveData( listOf( OnboardingPage( context.getString(R.string.onboarding_1_title), context.getString(R.string.onboarding_1_text), ContextCompat.getDrawable(context, R.drawable.onboarding_1), context.getString(R.string.onboarding_button) ), OnboardingPage( context.getString(R.string.onboarding_2_title), context.getString(R.string.onboarding_2_text), ContextCompat.getDrawable(context, R.drawable.onboarding_2), context.getString(R.string.onboarding_button) ), OnboardingPage( context.getString(R.string.onboarding_3_title), context.getString(R.string.onboarding_3_text), ContextCompat.getDrawable(context, R.drawable.onboarding_3), context.getString(R.string.onboarding_button_last) ) ) ) // Get the current page button text so we can put the "next button" in the fragment layout and avoid duplicate code. val buttonText: LiveData<CharSequence> = Lives.combineLatest(currentPage, pages) { currentPage, pages -> pages[currentPage].buttonText } // Binding for view pager val itemBinding: ItemBinding<OnboardingPage> = ItemBinding.of(BR.onboardingPage, R.layout.onboarding_page) //Simple method to go to the next page fun nextPage() { val current = currentPage.value!! if (current < pages.value!!.lastIndex) { currentPage.value = currentPage.value!! + 1 } else { _navigationEvent.value = OnboardingFinish } } private fun completeOnboarding() { // We pretend you have set up some preference service to store whether the onboarding is completed or not 🙂 prefs.completeOnboarding() _navigationEvent.value = OnboardingFinish } }
Disposición del fragmento
Usaremos ViewPager2
para mostrar nuestras páginas, necesitamos un "botón siguiente"
y opcionalmente, un logotipo y un indicador de página.
<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <data> <variable name="viewModel" type="no.example.onboarding.OnboardingViewModel" /> </data> <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent" android:layout_margin="24dp"> <ImageView android:id="@+id/onboardingLogo" android:layout_width="wrap_content" android:layout_height="wrap_content" android:paddingTop="24dp" android:src="@drawable/pr" app:layout_constraintHeight_max="150dp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <androidx.viewpager2.widget.ViewPager2 android:id="@+id/view_pager" currentItem="@={viewModel.currentPage}" itemBinding="@{viewModel.itemBinding}" items="@{viewModel.pages}" android:layout_width="match_parent" android:layout_height="0dp" app:layout_constraintBottom_toTopOf="@id/onboarding_button" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/onboardingLogo" /> <Button android:id="@+id/onboarding_button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="24dp" android:layout_marginBottom="24dp" android:onClick="@{() -> viewModel.nextPage()}" android:text="@{viewModel.buttonText}" app:layout_constraintBottom_toTopOf="@id/tab_layout" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" /> <com.google.android.material.tabs.TabLayout android:id="@+id/tab_layout" android:layout_width="wrap_content" android:layout_height="wrap_content" viewPager="@{viewPager}" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:tabBackground="@drawable/tab_selector" app:tabIndicatorHeight="0dp" /> </androidx.constraintlayout.widget.ConstraintLayout> </layout>
Probablemente note que en realidad no necesita un adaptador, por eso agregamos las dependencias anteriormente.
Todo lo que tenemos que hacer es transmitir el itemBinding
y pages
desde el modelo de vista al paginador de vista.
<androidx.viewpager2.widget.ViewPager2 itemBinding="@{viewModel.itemBinding}" items="@{viewModel.pages}" />
Estamos usando un TabLayout
como indicador de página. Para evitar conectar el ViewPager
al TabLayout
en el fragmento, agregamos un adaptador de encuadernación para hacerlo de manera más limpia:
object TabLayoutBindingAdapter { @JvmStatic @BindingAdapter(value = ["viewPager"], requireAll = false) fun TabLayout.setViewPager(viewPager: ViewPager2) { viewPager.viewTreeObserver?.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener { override fun onGlobalLayout() { TabLayoutMediator(this@setViewPager, viewPager) { _, _ -> }.attach() viewPager.viewTreeObserver.removeOnGlobalLayoutListener(this) } }) } }
También podemos agregar un estilo simple para hacer que TabLayout
se vea más como un indicador de página con:
app:tabBackground="@drawable/tab_selector" app:tabIndicatorHeight="0dp"
tab_selector.xml
:
<?xml version="1.0" encoding="utf-8"?> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:drawable="@drawable/tab_indicator_selected" android:state_selected="true"/> <item android:drawable="@drawable/tab_indicator_default"/> </selector>
tab_indicator_selected.xml
:
<?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android" android:innerRadius="0dp" android:shape="ring" android:thickness="5dp" android:useLevel="false"> <solid android:color="@color/colorPrimary" /> </shape>
y tab_indicator_default.xml
:
<?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android" android:innerRadius="0dp" android:shape="ring" android:thickness="5dp" android:useLevel="false"> <solid android:width="2dp" android:color="@color/colorPrimaryWithOpacity" /> </shape>
Eso es mucho código y no demasiada explicación, pero creo que el código habla por sí solo.
Con esta configuración, debería ser bastante rápido cambiar la incorporación para que se ajuste a su diseño. Deberá agregar algunas cadenas, imágenes y estilos, pero si copia y pega este código en su proyecto, se verá muy similar a la imagen de arriba.
Algunas alternativas
Por ejemplo, podrías cambiar OnboardingPage
tomar imageUrl: String
en lugar de Drawable
. Luego, cree un enlace de datos para configurar la imagen con eso en lugar de pasar el dibujante local:
@JvmStatic @BindingAdapter ( " imageUrl " ) divertido ImageView. setImageUri ( imageUrl : String? ) { if (imageUrl == null || imageUrl.isBlank ()) { setImageDrawable ( nulo ) } más { prueba { Deslizar . con ( este .context) .load (imageUrl) .into ( este ) } catch ( e : Exception ) { Timber .e (e, " No se pudo cargar la imagen " ) } } }
Puede obtener la incorporación de un servidor y publicarla en viewModel.pages
, ya lo admite ya que utiliza datos en vivo.
Espero que te haya sido de utilidad. Gracias por leer este post.
Añadir comentario