Publicado en Android

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…

Componente StateLayout

Código fuente del componente StateLayout creado con Kotlin, modificado de StateLayout de wangshouquan

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)
                }
            }
        }
    }

}

Atributos que usa el componente attrs.xml

<?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>

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

Su vista base es la siguiente view_error.xml

<?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>

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")

Demostración

Para ver en funcionamiento el componente de control de carga de vista

Código fuente

Código fuente del ejemplo de demostración en gist

Autor:

Desarrollador freelance programador apasionado por el arte de programar, amante del auto aprendizaje y interesado por la tecnología en general.

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión /  Cambiar )

Google photo

Estás comentando usando tu cuenta de Google. Cerrar sesión /  Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión /  Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión /  Cambiar )

Conectando a %s

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios .