airbnb / epoxy

Epoxy is an Android library for building complex screens in a RecyclerView
https://goo.gl/eIK82p
Apache License 2.0
8.51k stars 726 forks source link

Dynamically Add Views #955

Open zsweigart opened 4 years ago

zsweigart commented 4 years ago

I have an Epoxy Model that I want to add and remove views from dynamically when I click the view. I don't know the number of views beforehand to include them in the xml so I can't use visibility on a known set of views. I am also adding drag and drop which is why I can't use multiple view types as described in https://github.com/airbnb/epoxy/issues/473#issuecomment-500120564

What happens is when I click the view the size of the view expands slightly so it seems like the views are being added, but the new views aren't visible.

I am using a vertical recyclerview with a custom EpoxyController

Layout xml file

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools"
  android:layout_height="wrap_content"
  android:layout_width="match_parent"
  android:orientation="vertical">

  <LinearLayout
    android:id="@+id/header"
    android:background="?android:attr/selectableItemBackground"
    android:clickable="true"
    android:focusable="true"
    android:layout_height="wrap_content"
    android:layout_width="match_parent">

    <ImageView
      android:id="@+id/carrot"
      android:adjustViewBounds="true"
      android:layout_height="@dimen/space_4x"
      android:layout_width="@dimen/space_4x"
      android:scaleType="fitXY"
      android:src="@drawable/carrot_right"
      android:layout_gravity="center_vertical"/>

    <TextView
      android:id="@+id/title"
      android:layout_height="wrap_content"
      android:layout_width="0dp"
      android:layout_weight="1"
      android:layout_marginStart="@dimen/space_2x"/>

  </LinearLayout>

</LinearLayout>

Epoxy View

@ModelView(autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT)
class EpoxyTestView @JvmOverloads constructor(
  context: Context,
  attrs: AttributeSet? = null,
  defStyleAttr: Int = 0
) : LinearLayout(context, attrs, defStyleAttr) {

  private val headerView: LinearLayout
  private val titleView: TextView
  private val carrotView: ImageView
  private val childViews: MutableList<TextView> = mutableListOf()
  private var children: List<String>? = null
  private var isOpen: Boolean = false

  init {
    LayoutInflater.from(context).inflate(R.layout.test_layout, this, true)
    headerView = findViewById(R.id.header)
    titleView = findViewById(R.id.title)
    carrotView = findViewById(R.id.carrot)
  }

  @ModelProp
  fun setOpen(isOpen: Boolean) {
    this.isOpen = isOpen
    val anim: Animation =
      AnimationUtils.loadAnimation(
        this.context,
        if (isOpen) R.anim.rotate_carrot_left_ninety else R.anim.rotate_carrot_from_left_ninety
      )

    if (isOpen) {
      for (child in childViews) {
        addView(child)
      }
    } else {
      for (child in childViews) {
        removeView(child)
      }
    }
    carrotView.startAnimation(anim)
  }

  @TextProp
  fun setTitle(title: CharSequence) {
    titleView.text = title
  }

  @ModelProp
  fun children(children: List<String>) {
    this.children = children
    for (child in children) {
      val textView = TextView(context)
      textView.text = child
      textView.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
      childViews.add(textView)
    }
  }

  @CallbackProp
  fun setHeaderClickListener(clickListener: OnClickListener?) {
    headerView.setOnClickListener {
      setOpen(!this@EpoxyTestView.isOpen)
      clickListener?.onClick(this@EpoxyTestView)
    }
  }
}

I have also tried adding and removing the views in an animation listener but that crashes

    anim.setAnimationListener(object : Animation.AnimationListener {
      override fun onAnimationRepeat(animation: Animation?) {
      }

      override fun onAnimationEnd(animation: Animation?) {
        if (isOpen) {
          for (child in childViews) {
            addView(child)
          }
        } else {
          for (child in childViews) {
            removeView(child)
          }
        }
      }

      override fun onAnimationStart(animation: Animation?) {
      }
    })

In my Epoxy controller I am setting up the model

  override fun buildModels() {
      for (thing in thingsList) {
        epoxyTestView {
          id(thing.id)
          title(thing.title)
          headerClickListener { _, _, _, _ ->
            round.isExpanded = !round.isExpanded
            requestModelBuild()
          }
          children(thing.children)
        }
      }
  }

I have tried it with and without requestModelBuild() after I add the views with the same result

Here is the result epoxy_error

premacck commented 4 years ago

Could it be orientation = VERTICAL which is mission from the init { } block in EpoxyTestView? Because the default orientation of a LinearLayout is HORIZONTAL

zsweigart commented 4 years ago

Adding orientation = VERTICAL in the init { } block in EpoxyTestView does not solve the issue. I have switched to using multiple epoxy views and adding and removing them instead of adding views inside of an epoxy view