airbnb / epoxy

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

Dynamically resizing a carousel group #636

Closed davidcrotty closed 5 years ago

davidcrotty commented 5 years ago

Hello, excellent work on this library, really love the composable approach, as what i am writing needs to be a very flexible feed of items. I am still learning the ropes with this library so hopefully there is an obvious answer to this question :)

Context: I am creating a media carousel using a ModelGroup like in the sample, but with a horizontal scroll and full bleed images.

One of the requirements is to have this carousel fit a 16:9 aspect ratio, which unless I am missing something here I will need to calculate dynamically and inject into the modelgroup as its height measurement.

The question is how do I achieve this?

With a recyclerview in the onCreateViewHolder i'd hook in a predraw\layout listener listener to resize in one pass.

I've currently hooked into the preBind of the EpoxyModelGroup which resizes the parent but not the child items in the carousel, but this still feels wrong as if I traced back correctly this is more of a bindViewHolder then create which is pretty explicit. The only other thing I can think of is overriding onMeasure of the carousel subclass and overriding the size there?

Hopefully im missing something obvious here!

Code:

MediaCarouselGroup.kt

class MediaCarouselGroup(mediaList: List<Media>, mediaPaddingPx: Int) : EpoxyModelGroup(R.layout.view_group_carousel_container, buildModels(mediaList, mediaPaddingPx)) {
    companion object {
        fun buildModels(contentList: List<Media>, mediaPaddingPx: Int): List<EpoxyModel<*>> {
            val contentModelList = ArrayList<EpoxyModel<*>>()
            val mediaList = ArrayList<MediaModel_>()
            contentList.forEach { media ->
                mediaList.add(
                        MediaModel_()
                        .id(UUID.randomUUID().toString())
                )
            }

            contentModelList.add(MediaCarouselModel_()
                .id(UUID.randomUUID().toString())
                .padding(Carousel.Padding(0, mediaPaddingPx)) 
                .models(mediaList))

            return contentModelList
        }
    }

    override fun handlePreBind(holder: EpoxyViewHolder?, groupHolder: Holder?, position: Int) {
        // pass params before a layout, should prevent redraw
        groupHolder?.rootView?.layoutParams?.let { params ->
            params.height = 1000 // resizes the parent, this will be injectable once determined if its the right place!
            groupHolder.rootView.layoutParams = params
        }
        super.handlePreBind(holder, groupHolder, position)
    }
}

MediaCarousel.kt

@ModelView(defaultLayout = R.layout.view_carousel) // also tried autolayed sizes
class MediaCarousel(context: Context, attributeSet: AttributeSet) : Carousel(context, attributeSet) {
    override fun createLayoutManager(): LayoutManager {
        return LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
    }
}

Screenshot: (black is the ModelGroup that i managed to resize, orange is the carousel which is not matching parent) screenshot_1544698136

davidcrotty commented 5 years ago

Update: Think I have a solution, I removed the pre bind call from the group, and instead left its constraints to the layout xml (using constraint layout so 0dp in its viewstub).

Then in the individual media items themselves I overrode the buildView callback which I traced back in the stack to being the closest to onCreateView and added a predraw listener:

    override fun buildView(parent: ViewGroup): View {
        val listener = object : ViewTreeObserver.OnPreDrawListener {
            override fun onPreDraw(): Boolean {
                parent.viewTreeObserver.removeOnPreDrawListener(this)
                val params = parent.layoutParams
                params.height = 1000
                parent.layoutParams = params
                return false
            }
        }
        parent.viewTreeObserver.addOnPreDrawListener(listener)
        return super.buildView(parent)
    }

I think from here I just use an epoxy attribute that will allow me to dynamically change the height parameter?

Edit: Just using the constructor and the generated model works at the moment, since I'm assuming the attributes are only designed to be used with the bind call.

elihart commented 5 years ago

Sorry for the delay, still catching up on things since being on vacation.

Unless I'm missing something, it seems that your solution is more complex than it needs to be, if you have the height of the Carousel wrap_content, then it will match the size of the children.

Then have all child models in the Carousel set their size to 16:9 - the easiest way to do this is with ConstraintLayout.

davidcrotty commented 5 years ago

Of course, thank you - this worked for me. I thought I'd tried it before in constraint layout but I'd used wrap_content rather than 0dp on my imageview.