Skip to content

Commit 95a93df

Browse files
Material Design Teamldjcmu
authored andcommitted
Adding TabLayoutMediator to MDC tabs library
The TabLayoutMediator makes it possible for a ViewPager2 to be linked to a TabLayout similarly to how a ViewPager was linked to TabLayout. PiperOrigin-RevId: 248127710
1 parent da51a66 commit 95a93df

2 files changed

Lines changed: 301 additions & 4 deletions

File tree

lib/java/com/google/android/material/tabs/TabLayout.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -232,19 +232,19 @@ public class TabLayout extends HorizontalScrollView {
232232
public @interface Mode {}
233233

234234
/**
235-
* If a tab is instantiated with {@link TabLayout#setText(CharSequence)}, and this mode is set,
235+
* If a tab is instantiated with {@link Tab#setText(CharSequence)}, and this mode is set,
236236
* the text will be saved and utilized for the content description, but no visible labels will be
237237
* created.
238238
*
239-
* @see #setTabLabelVisibility(int)
239+
* @see Tab#setTabLabelVisibility(int)
240240
*/
241241
public static final int TAB_LABEL_VISIBILITY_UNLABELED = 0;
242242

243243
/**
244244
* This mode is set by default. If a tab is instantiated with {@link
245-
* TabLayout#setText(CharSequence)}, a visible label will be created.
245+
* Tab#setText(CharSequence)}, a visible label will be created.
246246
*
247-
* @see #setTabLabelVisibility(int)
247+
* @see Tab#setTabLabelVisibility(int)
248248
*/
249249
public static final int TAB_LABEL_VISIBILITY_LABELED = 1;
250250

Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
1+
/*
2+
* Copyright 2018 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.android.material.tabs;
18+
19+
import static androidx.viewpager2.widget.ViewPager2.SCROLL_STATE_DRAGGING;
20+
import static androidx.viewpager2.widget.ViewPager2.SCROLL_STATE_IDLE;
21+
import static androidx.viewpager2.widget.ViewPager2.SCROLL_STATE_SETTLING;
22+
23+
import androidx.annotation.NonNull;
24+
import androidx.annotation.Nullable;
25+
import androidx.recyclerview.widget.RecyclerView;
26+
import androidx.viewpager2.widget.ViewPager2;
27+
import java.lang.ref.WeakReference;
28+
29+
/**
30+
* A mediator to link a TabLayout with a ViewPager2. The mediator will synchronize the ViewPager2's
31+
* position with the selected tab when a tab is selected, and the TabLayout's scroll position when
32+
* the user drags the ViewPager2.
33+
*
34+
* <p>Establish the link by creating an instance of this class, make sure the ViewPager2 has an
35+
* adapter and then call {@link #attach()} on it. When creating an instance of this class, you must
36+
* supply an implementation of {@link OnConfigureTabCallback} in which you set the text of the tab,
37+
* and/or perform any styling of the tabs that you require.
38+
*/
39+
public final class TabLayoutMediator {
40+
@NonNull private final TabLayout tabLayout;
41+
@NonNull private final ViewPager2 viewPager;
42+
private final boolean autoRefresh;
43+
private final OnConfigureTabCallback onConfigureTabCallback;
44+
private RecyclerView.Adapter<?> adapter;
45+
private boolean attached;
46+
47+
private TabLayoutOnPageChangeCallback onPageChangeCallback;
48+
private TabLayout.OnTabSelectedListener onTabSelectedListener;
49+
private RecyclerView.AdapterDataObserver pagerAdapterObserver;
50+
51+
/**
52+
* A callback interface that must be implemented to set the text and styling of newly created
53+
* tabs.
54+
*/
55+
public interface OnConfigureTabCallback {
56+
/**
57+
* Called to configure the tab for the page at the specified position. Typically calls {@link
58+
* TabLayout.Tab#setText(CharSequence)}, but any form of styling can be applied.
59+
*
60+
* @param tab The Tab which should be configured to represent the title of the item at the given
61+
* position in the data set.
62+
* @param position The position of the item within the adapter's data set.
63+
*/
64+
void onConfigureTab(@NonNull TabLayout.Tab tab, int position);
65+
}
66+
67+
/**
68+
* Creates a TabLayoutMediator to synchronize a TabLayout and a ViewPager2 together. It will
69+
* update the tabs automatically when the data set of the view pager's adapter changes. The link
70+
* will be established after {@link #attach()} is called.
71+
*
72+
* @param tabLayout The tab bar to link
73+
* @param viewPager The view pager to link
74+
*/
75+
public TabLayoutMediator(
76+
@NonNull TabLayout tabLayout,
77+
@NonNull ViewPager2 viewPager,
78+
@NonNull OnConfigureTabCallback onConfigureTabCallback) {
79+
this(tabLayout, viewPager, true, onConfigureTabCallback);
80+
}
81+
82+
/**
83+
* Creates a TabLayoutMediator to synchronize a TabLayout and a ViewPager2 together. If {@code
84+
* autoRefresh} is true, it will update the tabs automatically when the data set of the view
85+
* pager's adapter changes. The link will be established after {@link #attach()} is called.
86+
*
87+
* @param tabLayout The tab bar to link
88+
* @param viewPager The view pager to link
89+
* @param autoRefresh If {@code true}, will recreate all tabs when the data set of the view
90+
* pager's adapter changes.
91+
*/
92+
public TabLayoutMediator(
93+
@NonNull TabLayout tabLayout,
94+
@NonNull ViewPager2 viewPager,
95+
boolean autoRefresh,
96+
@NonNull OnConfigureTabCallback onConfigureTabCallback) {
97+
this.tabLayout = tabLayout;
98+
this.viewPager = viewPager;
99+
this.autoRefresh = autoRefresh;
100+
this.onConfigureTabCallback = onConfigureTabCallback;
101+
}
102+
103+
/**
104+
* Link the TabLayout and the ViewPager2 together.
105+
*
106+
* @throws IllegalStateException If the mediator is already attached, or the ViewPager2 has no
107+
* adapter.
108+
*/
109+
public void attach() {
110+
if (attached) {
111+
throw new IllegalStateException("TabLayoutMediator is already attached");
112+
}
113+
adapter = viewPager.getAdapter();
114+
if (adapter == null) {
115+
throw new IllegalStateException(
116+
"TabLayoutMediator attached before ViewPager2 has an " + "adapter");
117+
}
118+
attached = true;
119+
120+
// Add our custom OnPageChangeCallback to the ViewPager
121+
onPageChangeCallback = new TabLayoutOnPageChangeCallback(tabLayout);
122+
viewPager.registerOnPageChangeCallback(onPageChangeCallback);
123+
124+
// Now we'll add a tab selected listener to set ViewPager's current item
125+
onTabSelectedListener = new ViewPagerOnTabSelectedListener(viewPager);
126+
tabLayout.addOnTabSelectedListener(onTabSelectedListener);
127+
128+
// Now we'll populate ourselves from the pager adapter, adding an observer if
129+
// autoRefresh is enabled
130+
if (autoRefresh) {
131+
// Register our observer on the new adapter
132+
pagerAdapterObserver = new PagerAdapterObserver();
133+
adapter.registerAdapterDataObserver(pagerAdapterObserver);
134+
}
135+
136+
populateTabsFromPagerAdapter();
137+
138+
// Now update the scroll position to match the ViewPager's current item
139+
tabLayout.setScrollPosition(viewPager.getCurrentItem(), 0f, true);
140+
}
141+
142+
/** Unlink the TabLayout and the ViewPager */
143+
public void detach() {
144+
adapter.unregisterAdapterDataObserver(pagerAdapterObserver);
145+
tabLayout.removeOnTabSelectedListener(onTabSelectedListener);
146+
viewPager.unregisterOnPageChangeCallback(onPageChangeCallback);
147+
pagerAdapterObserver = null;
148+
onTabSelectedListener = null;
149+
onPageChangeCallback = null;
150+
attached = false;
151+
}
152+
153+
@SuppressWarnings("WeakerAccess")
154+
void populateTabsFromPagerAdapter() {
155+
tabLayout.removeAllTabs();
156+
157+
if (adapter != null) {
158+
int adapterCount = adapter.getItemCount();
159+
for (int i = 0; i < adapterCount; i++) {
160+
TabLayout.Tab tab = tabLayout.newTab();
161+
onConfigureTabCallback.onConfigureTab(tab, i);
162+
tabLayout.addTab(tab, false);
163+
}
164+
165+
// Make sure we reflect the currently set ViewPager item
166+
if (adapterCount > 0) {
167+
int currItem = viewPager.getCurrentItem();
168+
if (currItem != tabLayout.getSelectedTabPosition()) {
169+
tabLayout.getTabAt(currItem).select();
170+
}
171+
}
172+
}
173+
}
174+
175+
/**
176+
* A {@link ViewPager2.OnPageChangeCallback} class which contains the necessary calls back to the
177+
* provided {@link TabLayout} so that the tab position is kept in sync.
178+
*
179+
* <p>This class stores the provided TabLayout weakly, meaning that you can use {@link
180+
* ViewPager2#registerOnPageChangeCallback(ViewPager2.OnPageChangeCallback)} without removing the
181+
* callback and not cause a leak.
182+
*/
183+
private static class TabLayoutOnPageChangeCallback extends ViewPager2.OnPageChangeCallback {
184+
private final WeakReference<TabLayout> tabLayoutRef;
185+
private int previousScrollState;
186+
private int scrollState;
187+
188+
TabLayoutOnPageChangeCallback(TabLayout tabLayout) {
189+
tabLayoutRef = new WeakReference<>(tabLayout);
190+
reset();
191+
}
192+
193+
@Override
194+
public void onPageScrollStateChanged(final int state) {
195+
previousScrollState = scrollState;
196+
scrollState = state;
197+
}
198+
199+
@Override
200+
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
201+
TabLayout tabLayout = tabLayoutRef.get();
202+
if (tabLayout != null) {
203+
// Only update the text selection if we're not settling, or we are settling after
204+
// being dragged
205+
boolean updateText =
206+
scrollState != SCROLL_STATE_SETTLING || previousScrollState == SCROLL_STATE_DRAGGING;
207+
// Update the indicator if we're not settling after being idle. This is caused
208+
// from a setCurrentItem() call and will be handled by an animation from
209+
// onPageSelected() instead.
210+
boolean updateIndicator =
211+
!(scrollState == SCROLL_STATE_SETTLING && previousScrollState == SCROLL_STATE_IDLE);
212+
tabLayout.setScrollPosition(position, positionOffset, updateText, updateIndicator);
213+
}
214+
}
215+
216+
@Override
217+
public void onPageSelected(final int position) {
218+
TabLayout tabLayout = tabLayoutRef.get();
219+
if (tabLayout != null
220+
&& tabLayout.getSelectedTabPosition() != position
221+
&& position < tabLayout.getTabCount()) {
222+
// Select the tab, only updating the indicator if we're not being dragged/settled
223+
// (since onPageScrolled will handle that).
224+
boolean updateIndicator =
225+
scrollState == SCROLL_STATE_IDLE
226+
|| (scrollState == SCROLL_STATE_SETTLING
227+
&& previousScrollState == SCROLL_STATE_IDLE);
228+
tabLayout.selectTab(tabLayout.getTabAt(position), updateIndicator);
229+
}
230+
}
231+
232+
void reset() {
233+
previousScrollState = scrollState = SCROLL_STATE_IDLE;
234+
}
235+
}
236+
237+
/**
238+
* A {@link TabLayout.OnTabSelectedListener} class which contains the necessary calls back to the
239+
* provided {@link ViewPager2} so that the tab position is kept in sync.
240+
*/
241+
private static class ViewPagerOnTabSelectedListener implements TabLayout.OnTabSelectedListener {
242+
private final ViewPager2 viewPager;
243+
244+
ViewPagerOnTabSelectedListener(ViewPager2 viewPager) {
245+
this.viewPager = viewPager;
246+
}
247+
248+
@Override
249+
public void onTabSelected(TabLayout.Tab tab) {
250+
viewPager.setCurrentItem(tab.getPosition(), true);
251+
}
252+
253+
@Override
254+
public void onTabUnselected(TabLayout.Tab tab) {
255+
// No-op
256+
}
257+
258+
@Override
259+
public void onTabReselected(TabLayout.Tab tab) {
260+
// No-op
261+
}
262+
}
263+
264+
private class PagerAdapterObserver extends RecyclerView.AdapterDataObserver {
265+
PagerAdapterObserver() {}
266+
267+
@Override
268+
public void onChanged() {
269+
populateTabsFromPagerAdapter();
270+
}
271+
272+
@Override
273+
public void onItemRangeChanged(int positionStart, int itemCount) {
274+
populateTabsFromPagerAdapter();
275+
}
276+
277+
@Override
278+
public void onItemRangeChanged(int positionStart, int itemCount, @Nullable Object payload) {
279+
populateTabsFromPagerAdapter();
280+
}
281+
282+
@Override
283+
public void onItemRangeInserted(int positionStart, int itemCount) {
284+
populateTabsFromPagerAdapter();
285+
}
286+
287+
@Override
288+
public void onItemRangeRemoved(int positionStart, int itemCount) {
289+
populateTabsFromPagerAdapter();
290+
}
291+
292+
@Override
293+
public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
294+
populateTabsFromPagerAdapter();
295+
}
296+
}
297+
}

0 commit comments

Comments
 (0)