TabLayout scrolls to unkown position after calling notifyDataSetChanged on PagerAdapter
Asked Answered
K

5

11

I have sample project with TabLayout and PagerAdapter. Strange things happens with TabLayout when I call pagerAdapter.notifyDataSetChanged(); after tabLayout.setupWithViewPager(viewPager);

TabLayout is scrolling to unknown x position so the current tab is not visible. However if I scroll to left to expecting tab, this tab has indicator.

What is going on? Could anyone help me? I have spent on it too many time.

enter image description here

Below the code.

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Get the ViewPager and set it's PagerAdapter so that it can display items
        ViewPager viewPager = (ViewPager) findViewById(R.id.viewpager);
        final SampleFragmentPagerAdapter pagerAdapter = new SampleFragmentPagerAdapter(getSupportFragmentManager(), MainActivity.this);
        viewPager.setAdapter(pagerAdapter);

        // Give the TabLayout the ViewPager
        TabLayout tabLayout = (TabLayout) findViewById(R.id.sliding_tabs);
        tabLayout.setupWithViewPager(viewPager);

        findViewById(R.id.fab).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                pagerAdapter.notifyDataSetChanged();
            }
        });
    }

}

I tested on nexus emulators and nexus real devices (api 21+)

Gradle settings:

android {
    compileSdkVersion 25
    buildToolsVersion "25.0.2"
    defaultConfig {
        applicationId "xx.xxx.myapplication4"
        minSdkVersion 21
        targetSdkVersion 25
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

Link to reported issue and ready to test project as attachment here

Kurtiskurtosis answered 27/3, 2017 at 20:38 Comment(11)
Can you post that project?Forzando
click on blue link here - the last paragraph of my postKurtiskurtosis
The error comes from this line: had there not been - (getWidth() / 2) it would work as expected. You can test that placing a breakpoint before returning from method and changing scrollBase value to a correct one. See this.Forzando
Nice one, can I somehow hack it?Kurtiskurtosis
I tried to do via reflection, but there are no enough seams to do that. Nevertheless, that -width had some reasoning, which means you'll break something else by changing it. I guess there should be some if-else statement.Forzando
Yes, I see. Imho reflections are not safe for production apps. I will look around this part of code. Maybe there is any other valid solution for that problem. Thanks.Kurtiskurtosis
Place your xml code here pleaseHowardhowarth
How does it relate to xml? All information is in my question.Kurtiskurtosis
@Kurtiskurtosis Please check my answerBoaten
upvoted but I will be able check this tommorowKurtiskurtosis
@Kurtiskurtosis No issue, test it and let me know if I skipped anything.Boaten
B
9

Every time setup view pager with the use of setupWithViewPager might be costly. Because notifyDataSetChanged() on your ViewPager Adapter causing TabLayout to redraw its all views. So for redrawing, TabLayout remove all its associated Views and re add them.

Please check below thread execution steps which happen after notifyDataSetChanged on pager adapter.

  at android.support.design.widget.TabLayout.removeAllTabs(TabLayout.java:654)
  at android.support.design.widget.TabLayout.populateFromPagerAdapter(TabLayout.java:904)
  at android.support.design.widget.TabLayout$PagerAdapterObserver.onChanged(TabLayout.java:2211)
  at android.database.DataSetObservable.notifyChanged(DataSetObservable.java:37)
  - locked <0x129f> (a java.util.ArrayList)
  at android.support.v4.view.PagerAdapter.notifyDataSetChanged(PagerAdapter.java:287)
  at com.sample.testtablayout.MainActivity$1.onClick(MainActivity.java:45)

According to thread execution -

Clicked on Action Button > Pager Adapter Notified for data set changed > TabLayout got notification of data change through observers > It try to populate new data from adapter > Removed all tabs.

Below is a code of TabLayout class, from which you can check whenever all tabs are going to remove then last selected tab is lost(Intentionally marked null).

/**
 * Remove all tabs from the action bar and deselect the current tab.
 */
public void removeAllTabs() {
    // Remove all the views
    for (int i = mTabStrip.getChildCount() - 1; i >= 0; i--) {
        removeTabViewAt(i);
    }

    for (final Iterator<Tab> i = mTabs.iterator(); i.hasNext();) {
        final Tab tab = i.next();
        i.remove();
        tab.reset();
        sTabPool.release(tab);
    }

    mSelectedTab = null; // Thats a cause for your issue.
}

To retain last selected tab I have created my own CustomTabLayout class and retained last selected position.

public class RetainableTabLayout extends TabLayout {

   /*
   * Variable to store invalid position.
   */
   private static final int INVALID_TAB_POS = -1;

   /*
   * Variable to store last selected position, init it with invalid tab position.
   */
   private int mLastSelectedTabPosition = INVALID_TAB_POS;

   public RetainableTabLayout(Context context) {
       super(context);
   }

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

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

   @Override
    public void removeAllTabs() {
       // Retain last selected position before removing all tabs
       mLastSelectedTabPosition = getSelectedTabPosition();
       super.removeAllTabs();
   }

   @Override
   public int getSelectedTabPosition() {
       // Override selected tab position to return your last selected tab position
       final int selectedTabPositionAtParent = super.getSelectedTabPosition();
       return selectedTabPositionAtParent == INVALID_TAB_POS ? 
              mLastSelectedTabPosition : selectedTabPositionAtParent;
   }
}

At the end make sure to reselect your tab after recreation of your TabLayout.

findViewById(R.id.fab).setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            pagerAdapter.notifyDataSetChanged();
            // At the end make sure to reselect your last item.
            new Handler().postDelayed(
                    new Runnable() {
                        @Override
                        public void run() {
                            final TabLayout.Tab selectedTab = tabLayout.getTabAt(
                                 tabLayout.getSelectedTabPosition());
                            if (selectedTab != null) {
                                selectedTab.select();
                            }
                        }
                    }, 100);
        }
    });

This solution will resolve your problem but what I believe is TabLayout must have to retain last selected position in case of data set change. This is what I understand, any comments or more understanding is welcome.

Boaten answered 16/4, 2017 at 10:56 Comment(2)
After .notifyDataSetChanged no need to create new Handler() and set delay. TabLayout contains Handler. Use tabLayout.post()Ergonomics
Thank you so much for this answer!Negligent
H
6

You can call:

setupWithViewPager(@Nullable final ViewPager viewPager, boolean autoRefresh)

and set param autoRefresh to false.

See also: TabLayout(ViewPager,boolean)

Harness answered 21/6, 2017 at 3:10 Comment(3)
developer.android.com/reference/android/support/design/widget/…Trinitrobenzene
This solution works perfectly if you have a TabLayout with a custom viewPager with fragments. I needed to update the fragment UI but it crashed in onTabSelected because it redrawed tabs content. I spent literally about 3 hours to achieve this.Immotile
Isn't resetting viewPager is costly in terms of memory management? That's a reason of notifyDataSetChanged() to came in picture.Boaten
N
1

I just added notifyDataSetChanged method inside of TabLayout implementation of Rasi.

It works for me.

public class CustomTabLayout extends TabLayout {

/*
   * Variable to store invalid position.
   */
private static final int INVALID_TAB_POS = -1;

/*
* Variable to store last selected position, init it with invalid tab position.
*/
private int mLastSelectedTabPosition = INVALID_TAB_POS;

public CustomTabLayout(Context context) {
    super(context);
}

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

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

@Override
public void removeAllTabs() {
    // Retain last selected position before removing all tabs
    mLastSelectedTabPosition = getSelectedTabPosition();
    super.removeAllTabs();
}

@Override
public int getSelectedTabPosition() {
    // Override selected tab position to return your last selected tab position
    final int selectedTabPositionAtParent = super.getSelectedTabPosition();
    return selectedTabPositionAtParent == INVALID_TAB_POS ?
            mLastSelectedTabPosition : selectedTabPositionAtParent;
}

public void notifyDataSetChanged() {
    post(new Runnable() {
        @Override
        public void run() {
            TabLayout.Tab selectedTab = getTabAt(getSelectedTabPosition());
            if (selectedTab != null) {
                selectedTab.select();
            }
        }
    });
}
}

and to notify call

mPagerAdapter.notifyDataSetChanged();
mCustomTabLayout.notifyDataSetChanged();
Negligent answered 21/4, 2017 at 15:45 Comment(1)
This appears to be a cleaner version of Rahul's existing answer, above: https://mcmap.net/q/986794/-tablayout-scrolls-to-unkown-position-after-calling-notifydatasetchanged-on-pageradapterPericline
W
0

you can directly set the current postion.

you can get the selected position before notifying dataset change and save it in static varaible and update it everytime:

public static tabPostion;

    @Override
    public void onTabSelected(TabLayout.Tab tab) {
      tabPostion = tab.getPosition();
    }

    @Override
    public void onTabUnselected(TabLayout.Tab tab) {

    }

    @Override
    public void onTabReselected(TabLayout.Tab tab) {

    }

//set the postion of the pervious tab
    yourviewpager.setCurrentItem(tabPostion);

below is the code from my project :

   @Override
    public void onTabSelected(TabLayout.Tab tab) {

        // imageHashMap = new HashMap<>();
        onClearOrSelectAllImage(FlickQuickEnum.PIC_SELECTION_ACTION.CLEAR);
        selectedImageUrls = new HashMap<>();
        String tag = tab.getText().toString();
        String tagName = tag.substring(1, tag.length());
        currentTab = tagName;
        if (tagName != null) {
            dbAlbumPhotoList = new ArrayList<>();
            if (tagName.equals("All")) {
                dbAlbumPhotoList = dbAlbumPhotosHashMap.get(album.getAlbumName());
            } else {
                dbAlbumPhotoList = dbAlbumPhotosHashMap.get(tagName);
            }
        }
        updatePhotoCount();
        setPhotosSelectedActions(false);
    }

    @Override
    public void onTabUnselected(TabLayout.Tab tab) {

    }

    @Override
    public void onTabReselected(TabLayout.Tab tab) {

    }
Weidner answered 14/4, 2017 at 12:42 Comment(3)
yes this code is from my project and it works properlyWeidner
I am aslo refreshing the UI of the tabs on some actions but i am not using notifydatasetchange, instead i am using Eventbus to refresh the whole data and UI as per my needWeidner
@Kurtiskurtosis I have used this code in my app and it works fineWeidner
E
0

I fix the issue, you need modify the TabLayout source code

void populateFromPagerAdapter() {
    removeAllTabs();

    if (mPagerAdapter != null) {
        final int adapterCount = mPagerAdapter.getCount();
        for (int i = 0; i < adapterCount; i++) {
            addTab(newTab().setText(mPagerAdapter.getPageTitle(i)), false);
        }

        // need call post to run the code, to fix children views not layout
        post(new Runnable() {
            @Override
            public void run() {
                // Make sure we reflect the currently set ViewPager item
                if (mViewPager != null && adapterCount > 0) {
                    final int curItem = mViewPager.getCurrentItem();
                    if (curItem != getSelectedTabPosition() && curItem < getTabCount()) {
                        selectTab(getTabAt(curItem));
                    }
                }
            }
        });
    }
}
Eugenides answered 28/7, 2017 at 2:27 Comment(1)
You also can use the library linkEugenides

© 2022 - 2024 — McMap. All rights reserved.