Skip to content

Instantly share code, notes, and snippets.

@webserveis
Created October 12, 2020 18:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save webserveis/3c9ce2287069e01d5b51001bd68274a1 to your computer and use it in GitHub Desktop.
Save webserveis/3c9ce2287069e01d5b51001bd68274a1 to your computer and use it in GitHub Desktop.
StateLayout mostrar diferentes vistas en Android Kotlin

Si se requiere mostrar diferentes vistas dependiendo del contexto o información que se debe proyectar en pantalla, con StateLayout podremos controlar la carga de vistas en un proyecto. Para mostrar la pantalla de sin datos, la vista personalizada en caso de error, la vista de contenido etc...

Su uso

Definición del componente dentro del layout.xml

<?xml version="1.0" encoding="utf-8"?>
<com.webserveis.app.testapp.views.StateLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/layout_state"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:view_empty="@layout/view_empty"
    app:view_error="@layout/view_error"
    app:animate="true"
    app:animDuration="750"
    app:view_loading="@layout/view_loading">
 
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:text="This is the content." />
 
</com.webserveis.app.testapp.views.StateLayout>

Propiedades del componente

  • app:view_loading asigna la vista de carga
  • app_view_empty asigna la vista sin datos
  • app_view_error asigna la vista de error
  • app:animate para establecer el cambio de vista con un fundido
  • app:animDuration establece la duración del fundido

Usando código Kotlin

Para asignar una vista se usa el método .setViewForState

layout_state.setViewForState(StateLayout.STATE_LOADING, R.layout.view_loading)

Para cambiar de una vista a otra con el método .setState(State_id, animado)

layout_state.setState(StateLayout.STATE_LOADING, true)

Identificados predefinidos de StateLayout.STATE_

  • STATE_CONTENT = 0
  • STATE_LOADING = 1
  • STATE_ERROR = 2
  • STATE_EMPTY = 3

La segundo parámetro es opcional, es donde se especifica si el cambio de vista lo tiene que hacer mediante una animación de fundido, por defecto es false si no se indica lo contrario mediante la directiva en el componente xml app:animate=”true”

Si se desea se puede usar el método compacto

layout_state.content()
layout_state.loading()
layout_state.error()
layout_state.empty()
 
//con animación de fundido
layout_state.content(true)
layout_state.loading(true)
layout_state.error(true)
layout_state.empty(true)

Para asignar la velocidad de animación de fundido entre vistas, por defecto se establece en 350 milisegundos

layout_state.setAnimDuration(750L)

Personalización de una Vista

En la necesidad de querer personalizar una vista por ejemplo la vista de mostrar un error

Para personalizar el texto en tiempo de ejecución deberemos referirnos a los componentes que conforman la vista des de su view obtenido con layout_state.getView y luego con view?.findViewById se podrá importar el elemento

private fun showErrorUI(title: String, summary: String, @DrawableRes icon: Int? = null) {
    val view = layout_state.getView(StateLayout.STATE_ERROR)
    view?.findViewById<TextView>(R.id.error_title)?.text = title
    view?.findViewById<TextView>(R.id.error_summary)?.text = summary
    if (icon != null) view?.findViewById<ImageView>(R.id.error_icon)?.setImageResource(icon)
 
    layout_state.setState(StateLayout.STATE_ERROR)
}
 
//Su uso
showErrorUI("SIN INTERNET","No hay conexión a internet")
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="StateLayout">
<attr name="view_loading" format="reference" />
<attr name="view_error" format="reference" />
<attr name="view_empty" format="reference" />
<attr name="animate" format="boolean" />
<attr name="animDuration" format="integer" />
</declare-styleable>
</resources>
package com.webserveis.app.testapp
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.widget.Button
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.DrawableRes
import androidx.appcompat.app.AppCompatActivity
import com.webserveis.app.testapp.views.StateLayout
import kotlinx.android.synthetic.main.content_main.*
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(findViewById(R.id.toolbar))
layout_state.setViewForState(STATE_CUSTOM, R.layout.view_custom)
layout_state.getView(STATE_CUSTOM)
?.findViewById<Button>(R.id.btn_retry)
?.setOnClickListener {
layout_state.loading()
layout_state.postDelayed({ layout_state.content() }, 2000)
}
//layout_state.setAnimDuration(5000L)
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_main, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_content -> {
layout_state.setState(StateLayout.STATE_CONTENT)
return true
}
R.id.action_loading -> {
layout_state.setState(StateLayout.STATE_LOADING)
return true
}
R.id.action_error -> {
showErrorUI(
getString(R.string.state_error_internet_title),
getString(R.string.state_error_internet_summary)
)
return true
}
R.id.action_empty -> {
showEmptyUI(
getString(R.string.state_empty_title),
getString(R.string.state_empty_summary)
)
return true
}
R.id.action_custom -> {
layout_state.setState(STATE_CUSTOM)
return true
}
}
return super.onOptionsItemSelected(item)
}
private fun showErrorUI(title: String, summary: String, @DrawableRes icon: Int? = null) {
val view = layout_state.getView(StateLayout.STATE_ERROR)
view?.findViewById<TextView>(R.id.error_title)?.text = title
view?.findViewById<TextView>(R.id.error_summary)?.text = summary
if (icon != null) view?.findViewById<ImageView>(R.id.error_icon)?.setImageResource(icon)
layout_state.setState(StateLayout.STATE_ERROR)
}
private fun showEmptyUI(title: String, summary: String, @DrawableRes icon: Int? = null) {
val view = layout_state.getView(StateLayout.STATE_EMPTY)
view?.findViewById<TextView>(R.id.empty_title)?.text = title
view?.findViewById<TextView>(R.id.empty_summary)?.text = summary
if (icon != null) view?.findViewById<ImageView>(R.id.empty_icon)?.setImageResource(icon)
layout_state.setState(StateLayout.STATE_EMPTY)
}
companion object {
val STATE_CUSTOM = 4
}
}
package com.webserveis.app.testapp.views
import android.animation.ObjectAnimator
import android.content.Context
import android.os.Parcel
import android.os.Parcelable
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.view.animation.AccelerateInterpolator
import android.widget.FrameLayout
import androidx.annotation.LayoutRes
import com.webserveis.app.testapp.R
class StateLayout
@JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
FrameLayout(context, attrs, defStyleAttr) {
companion object {
// Build-in states.
const val STATE_CONTENT = 0
const val STATE_LOADING = 1
const val STATE_ERROR = 2
const val STATE_EMPTY = 3
}
private var mAnimate: Boolean
private var mAlphaAnimator: ObjectAnimator? = null
private var mAnimDuration: Long
private var state = STATE_CONTENT
private var stateMap = HashMap<Int, View>()
private var loadingRes = -1
private var errorRes = -1
private var emptyRes = -1
init {
val ta = context.obtainStyledAttributes(attrs, R.styleable.StateLayout)
loadingRes = ta.getResourceId(R.styleable.StateLayout_view_loading, -1)
errorRes = ta.getResourceId(R.styleable.StateLayout_view_error, -1)
emptyRes = ta.getResourceId(R.styleable.StateLayout_view_empty, -1)
mAnimate = ta.getBoolean(R.styleable.StateLayout_animate, false)
mAnimDuration = ta.getInt(R.styleable.StateLayout_animDuration, 350).toLong()
ta.recycle()
}
override fun onFinishInflate() {
super.onFinishInflate()
if (childCount > 1) throw IllegalArgumentException("You must have only one content view.")
if (childCount == 1) {
val contentView = getChildAt(0)
stateMap[STATE_CONTENT] = contentView
}
if (loadingRes != -1) setViewForState(STATE_LOADING, loadingRes)
if (errorRes != -1) setViewForState(STATE_ERROR, errorRes)
if (emptyRes != -1) setViewForState(STATE_EMPTY, emptyRes)
}
fun setViewForState(state: Int, @LayoutRes res: Int) {
val view = LayoutInflater.from(context).inflate(res, this, false)
setViewForState(state, view)
}
private fun setViewForState(state: Int, view: View) {
if (stateMap.containsKey(state)) {
removeView(stateMap[state])
}
addView(view)
view.visibility = View.GONE
stateMap[state] = view
}
fun setState(state: Int, animate: Boolean = false) {
if (this.state == state) return
if (!stateMap.containsKey(state)) throw IllegalStateException("Invalid state: $state")
for (key in stateMap.keys) {
if (key == state) {
stateMap[key]?.visibility = View.VISIBLE
if (animate || mAnimate) execAlphaAnimation(stateMap[key])
} else {
stateMap[key]?.visibility = View.GONE
}
//stateMap[key]!!.visibility = if (key == state) View.VISIBLE else View.GONE
}
this.state = state
}
fun getView(state: Int): View? {
return stateMap[state]
}
fun content(animate: Boolean = false) = setState(STATE_CONTENT, animate)
fun loading(animate: Boolean = false) = setState(STATE_LOADING, animate)
fun error(animate: Boolean = false) = setState(STATE_ERROR, animate)
fun empty(animate: Boolean = false) = setState(STATE_EMPTY, animate)
fun setAnimDuration(value: Long) {
mAnimDuration = value
}
private fun clearTargetViewAnimation() {
mAlphaAnimator?.let {
mAlphaAnimator!!.cancel()
}
}
private fun execAlphaAnimation(targetView: View?) {
if (targetView == null) return
mAlphaAnimator = ObjectAnimator.ofFloat(targetView, View.ALPHA, 0.0f, 1.0f).apply {
this.interpolator = AccelerateInterpolator()
this.duration = mAnimDuration
}
mAlphaAnimator?.start()
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
clearTargetViewAnimation()
}
override fun onSaveInstanceState(): Parcelable? {
val superState = super.onSaveInstanceState()
return if (superState == null) {
superState
} else {
SavedState(superState, state)
}
}
override fun onRestoreInstanceState(state: Parcelable?) {
if (state is SavedState) {
super.onRestoreInstanceState(state.superState)
setState(state.state)
} else {
super.onRestoreInstanceState(state)
}
}
internal class SavedState : BaseSavedState {
var state: Int
constructor(superState: Parcelable, state: Int) : super(superState) {
this.state = state
}
constructor(source: Parcel) : super(source) {
state = source.readInt()
}
override fun writeToParcel(out: Parcel, flags: Int) {
super.writeToParcel(out, flags)
out.writeInt(state)
}
companion object {
@JvmField
val CREATOR: Parcelable.Creator<SavedState> = object : Parcelable.Creator<SavedState> {
override fun createFromParcel(`in`: Parcel): SavedState {
return SavedState(`in`)
}
override fun newArray(size: Int): Array<SavedState?> {
return arrayOfNulls(size)
}
}
}
}
}
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<ImageView
android:id="@+id/error_icon"
android:layout_width="96dp"
android:layout_height="96dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:contentDescription="Error"
app:layout_constraintBottom_toTopOf="@+id/error_title"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
app:srcCompat="@drawable/ic_error_24"
app:tint="?attr/colorButtonNormal" />
<TextView
android:id="@+id/error_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="8dp"
android:text="Error title"
android:textAlignment="center"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Title"
app:layout_constraintBottom_toTopOf="@+id/error_summary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/error_icon" />
<TextView
android:id="@+id/error_summary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:text="Error summary"
android:textAlignment="center"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Body1"
android:textColor="?android:textColorSecondary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/error_title" />
<Button
android:id="@+id/btn_retry"
style="@style/Widget.AppCompat.Button.Colored"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Retry"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/error_summary" />
</androidx.constraintlayout.widget.ConstraintLayout>
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ImageView
android:id="@+id/empty_icon"
android:layout_width="96dp"
android:layout_height="96dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
app:tint="?attr/colorButtonNormal"
app:layout_constraintBottom_toTopOf="@+id/empty_title"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
app:srcCompat="@drawable/ic_inbox_24" />
<TextView
android:id="@+id/empty_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="8dp"
android:text="@string/state_empty_title"
android:textAlignment="center"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Title"
android:textColor="?android:textColorPrimary"
app:layout_constraintBottom_toTopOf="@+id/empty_summary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/empty_icon" />
<TextView
android:id="@+id/empty_summary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:text="@string/state_empty_summary"
android:textAlignment="center"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Body1"
android:textColor="?android:textColorSecondary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/empty_title" />
</androidx.constraintlayout.widget.ConstraintLayout>
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<ImageView
android:id="@+id/error_icon"
android:layout_width="96dp"
android:layout_height="96dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:contentDescription="Error"
app:tint="?attr/colorButtonNormal"
app:layout_constraintBottom_toTopOf="@+id/error_title"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
app:srcCompat="@drawable/ic_error_24" />
<TextView
android:id="@+id/error_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="8dp"
android:textAlignment="center"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Title"
app:layout_constraintBottom_toTopOf="@+id/error_summary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/error_icon"
tools:text="Error title" />
<TextView
android:id="@+id/error_summary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:textAlignment="center"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Body1"
android:textColor="?android:textColorSecondary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/error_title"
tools:text="Error summary" />
</androidx.constraintlayout.widget.ConstraintLayout>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center">
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment