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

Viewpager as a item in RecyclerView #210

Closed saurabhdtu closed 7 years ago

saurabhdtu commented 7 years ago

I am using a epoxy for the following RecyclerView structure.

     ------ LinearLayout Type 1--------
     ------ LinearLayout Type 2--------
     ----- LinearLayout Type 3--------
     ------   PagerTab Strip     --------
                 View Pager 

My ViewPager will have its own lengthy sublist of about 20-25 items for a minimum of 4 views inside the ViewPager as below

      ----View 1----    ----- View 2 -----     -----   View M
          textView1
          textView2
          textView3
          textView4
             ......
             ......
           textViewN        -----------------      TextViewMN

So I have created an EpoxyController1 for the first 3 LinearLayouts.. I am stuck at implementing the ViewPager. What I have thought till now is to create a nested EpoxyController2 inside EpoxyController1 for implementing the ViewPager. Am I on the right track ? Or there is any other way to utilise the EpoxyController1 to implement the ViewPager and its views? Or some other better way?

elihart commented 7 years ago

To be clear this is a vertical recyclerview with a horizontally swiping nested pager?

Is the viewpager for fragments or for views? Can you use a RecyclerView instead of a view pager? If you are just trying to implement a Carousel pattern a recyclerview works great. You can see how we do that in the sample app.

In general the nested scrolling should be an Epoxy Model whose view is the viewpager/recyclerview. That model passes on the data to the pager and the pager can initialize its subviews. If you use a horizontal recyclerview as a carousel it can have another EpoxyController to manage its children.

Again there is a carousel example in the sample app. It isn't set up as a viewpager type interaction, but that is easy to do with layoutmanager configuration

saurabhdtu commented 7 years ago

To be clear this is a vertical recyclerview with a horizontally swiping nested pager?

Yes I am trying to use the ViewPager inside a vertical RecyclerView. The ViewPager will be along with the TabLayout.

Is the viewpager for fragments or for views? Can you use a RecyclerView instead of a view pager? If you are just trying to implement a Carousel pattern a recyclerview works great. You can see how we do that in the sample app.

The ViewPager is going to have fragments having a Recyclerview . Because these fragments will be reusable and would be used independently elsewhere. I haven't given a thought about using a RecyclerView because my use case has a TabLayout and a ViewPager like functionality to be implemented which I don't suppose can be effectively implemented by using a RecyclerView. The RecyclerView inside the Fragments will have 3 or 4 types of ViewModels.

In general the nested scrolling should be an Epoxy Model whose view is the viewpager/recyclerview. That model passes on the data to the pager and the pager can initialize its subviews. If you use a horizontal recyclerview as a carousel it can have another EpoxyController to manage its children.

I have currently implemented the TabLayout & ViewPager as an EpoxyModel in the EpoxyController. The EpoxyModel is binding the PagerAdapter for the ViewPager in the onBind() method. Its not a carousel kind of this. Its supposed to be exactly like tabbed views

Can you tell me if this is the right way to solve this use case ? I am afraid of memory leaks and memory usage? Will it hamper my app performance? How is the best way to accomplish this scenario ?

saurabhdtu commented 7 years ago

These are the samples for a better understanding

screen shot 2017-05-07 at 00 59 16 screen shot 2017-05-07 at 00 58 04
elihart commented 7 years ago

Thanks for the examples, that makes it a lot easier to understand. There are several ways you could build this. I've actually built something similar to this (with a tab bar) with Epoxy.

I didn't use a viewpager. I just used the TabLayout view in the android support library and made an EpoxyModel for it and inserted it directly into the epoxy controller. Depending on what tab was selected I inserted the necessary models below the tab bar. This made it very efficient since I didn't need the nested scrolling. It looks good, but the downside is you can only change tabs by tapping on the tab, not by swiping. If you can live without swiping between tabs I'd recommend that approach. I'll attach the code I used below.

The other simplest approach I can think of is to not use a vertical recyclerview and Epoxy for the top level. it looks like the top horizontal linear layouts are static? Can you use a scroll view with those linear layouts and a viewpager? that would simplify things a lot.

If you do need a vertical recyclerview I think you could use a nested view pager without much trouble. I think you could also use a nested horizontal recyclerview and you could set up the swiping to snap like a view pager, but if you already have the fragments built then it's probably easier to reuse those.

I wouldn't worry about performance too much with the nested viewpager. Memory leaks shouldn't be a problem as long as you manage your resources properly. The viewpager looks pretty simple with just text views.

Here is my tablayout code using a custom view for the layout and an epoxy model

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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="56dp">

    <View
        android:id="@+id/section_divider"
        style="@style/n2_SubsectionDivider"
        android:layout_gravity="bottom"
        android:layout_marginLeft="0dp"
        android:layout_marginRight="0dp" />

    <android.support.design.widget.TabLayout
        android:id="@+id/tab_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_marginLeft="@dimen/n2_horizontal_padding_medium"
        android:layout_marginRight="@dimen/n2_horizontal_padding_medium"
        app:tabIndicatorColor="@color/n2_babu"
        app:tabIndicatorHeight="2dp"
        app:tabMode="scrollable" />

</FrameLayout>
public class WLDetailsTabBar extends FrameLayout implements DividerView {

    @BindView(R2.id.tab_layout) TabLayout tabLayout;
    @BindView(R2.id.section_divider) View divider;

    private List<WLTab> lastTabs = Collections.emptyList();

    public WLDetailsTabBar(Context context) {
        super(context);
        init();
    }

    public WLDetailsTabBar(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public WLDetailsTabBar(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        inflate(getContext(), R.layout.wl_details_tab_bar, this);
        ButterKnife.bind(this);
    }

    public void bind(List<WLTab> tabs, int selectedPosition, OnTabSelectedListener onTabSelectedListener) {
        tabLayout.clearOnTabSelectedListeners();

        if (!lastTabs.equals(tabs)) {
            setNewTabs(tabs);
        }

        // We don't set the position if it is the same, since the layout will reanimate back to that position
        if (selectedPosition != tabLayout.getSelectedTabPosition()) {
            tabLayout.getTabAt(selectedPosition).select();
        }

        // Add the listener after selecting the tab so the listener isn't called
        tabLayout.addOnTabSelectedListener(onTabSelectedListener);

        lastTabs = ImmutableList.copyOf(tabs);
    }

    private void setNewTabs(List<WLTab> tabs) {
        tabLayout.removeAllTabs();

        for (WLTab tab : tabs) {
            tabLayout.addTab(tabLayout.newTab()
                    .setCustomView(R.layout.wl_details_tab_bar_item)
                    .setText(tab.titleRes));
        }
    }

    @Override
    public void showDivider(boolean showDivider) {
        setVisibleIf(divider, showDivider);
    }
}
@EpoxyModelClass
abstract class WLDetailsTabBarModel extends AirEpoxyModel<WLDetailsTabBar> {

    /**
     * The selected position changes due to user input to the tab view, so that tab view already knows about position changes. We just need to tell it
     * the correct starting position, but have it trigger change updates.
     */
    @EpoxyAttribute(DoNotHash) int selectedPosition;
    @EpoxyAttribute List<WLTab> tabs;
    @EpoxyAttribute(DoNotHash) OnTabSelectedListener onTabSelectedListener;

    @Override
    protected int getDefaultLayout() {
        return R.layout.model_wl_details_tab_bar;
    }

    @Override
    public void bind(WLDetailsTabBar tabBar) {
        super.bind(tabBar);
        tabBar.bind(tabs, selectedPosition, onTabSelectedListener);
    }
}

The WLTab class is just an enum with the title of the tab. You could change it to whatever you need to represent your tab data. it just needs to implement equals/hashcode.

My tab select listener is

@Override
        public void onTabSelected(Tab tab) {
            WLTab previousTab = currentTab;
            lastSelectedTabPosition = tab.getPosition();
            requestModelBuild();

            WLTab newTab = currentTab;

Then when I build my models I use the current tab to figure out which models to insert below the tab bar.

saurabhdtu commented 7 years ago

Thanks a lot... The simple implementation with Scrollview and would facilitate my requirements. I'll try to implement it that way. If i come up with any doubt I'll surely get back to you..

qbait commented 5 years ago

I like your solution with TabLayout @elihart. And I've implemented my tabs like that. However, my client is asking now about swiping. Has anything changed in regards to swiping with this solution?

Pooja-Abbott commented 3 years ago

i want to create a imageslider with epoxy as circleindicators on top of imageview as i cannot find any approach to add indicators on the top of it and add swiping behavior. is there any approach to do the same.