Repo created
This commit is contained in:
parent
61d62cabdf
commit
ad3df69bdb
872 changed files with 65976 additions and 2 deletions
15
tab-layout-helper/build.gradle
Normal file
15
tab-layout-helper/build.gradle
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
apply plugin: 'com.android.library'
|
||||
|
||||
android {
|
||||
compileSdk 35
|
||||
namespace 'it.niedermann.android.tablayouthelper'
|
||||
|
||||
defaultConfig {
|
||||
minSdk 22
|
||||
targetSdk 35
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "com.google.android.material:material:$rootProject.materialVersion"
|
||||
}
|
||||
1
tab-layout-helper/src/main/AndroidManifest.xml
Normal file
1
tab-layout-helper/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1 @@
|
|||
<manifest />
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
package it.niedermann.android.tablayouthelper;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.viewpager2.widget.ViewPager2;
|
||||
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
class FixedTabLayoutOnPageChangeListener extends ViewPager2.OnPageChangeCallback {
|
||||
private final WeakReference<TabLayout> mTabLayoutRef;
|
||||
private int mPreviousScrollState;
|
||||
private int mScrollState;
|
||||
|
||||
FixedTabLayoutOnPageChangeListener(TabLayout tabLayout) {
|
||||
mTabLayoutRef = new WeakReference<>(tabLayout);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageScrollStateChanged(int state) {
|
||||
mPreviousScrollState = mScrollState;
|
||||
mScrollState = state;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageScrolled(int position, float positionOffset,
|
||||
int positionOffsetPixels) {
|
||||
final TabLayout tabLayout = mTabLayoutRef.get();
|
||||
if (tabLayout != null && shouldUpdateScrollPosition()) {
|
||||
// Update the scroll position, only update the text selection if we're being
|
||||
// dragged (or we're settling after a drag)
|
||||
tabLayout.setScrollPosition(position, positionOffset, true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageSelected(int position) {
|
||||
final TabLayout tabLayout = mTabLayoutRef.get();
|
||||
if (tabLayout != null && tabLayout.getSelectedTabPosition() != position) {
|
||||
// Select the tab, only updating the indicator if we're not being dragged/settled
|
||||
// (since onPageScrolled will handle that).
|
||||
Internal.selectTab(tabLayout, tabLayout.getTabAt(position),
|
||||
mScrollState == ViewPager2.SCROLL_STATE_IDLE);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean shouldUpdateScrollPosition() {
|
||||
return (mScrollState == ViewPager2.SCROLL_STATE_DRAGGING) ||
|
||||
((mScrollState == ViewPager2.SCROLL_STATE_SETTLING) && (mPreviousScrollState == ViewPager2.SCROLL_STATE_DRAGGING));
|
||||
}
|
||||
|
||||
private static class Internal {
|
||||
private static final Method mMethodSelectTab;
|
||||
|
||||
static {
|
||||
mMethodSelectTab = getAccessiblePrivateMethod(TabLayout.class, "selectTab", TabLayout.Tab.class, boolean.class);
|
||||
}
|
||||
|
||||
@SuppressWarnings("SameParameterValue")
|
||||
private static Method getAccessiblePrivateMethod(Class<?> targetClass, String methodName, Class<?>... params) throws RuntimeException {
|
||||
try {
|
||||
Method m = targetClass.getDeclaredMethod(methodName, params);
|
||||
m.setAccessible(true);
|
||||
return m;
|
||||
} catch (NoSuchMethodException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void selectTab(TabLayout tabLayout, TabLayout.Tab tab, boolean updateIndicator) {
|
||||
try {
|
||||
mMethodSelectTab.invoke(tabLayout, tab, updateIndicator);
|
||||
} catch (IllegalAccessException e) {
|
||||
Log.e(TabLayoutHelper.class.getCanonicalName(), e.getMessage(), new IllegalStateException(e));
|
||||
} catch (InvocationTargetException e) {
|
||||
throw handleInvocationTargetException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static RuntimeException handleInvocationTargetException(InvocationTargetException e) {
|
||||
Throwable targetException = e.getTargetException();
|
||||
if (targetException instanceof RuntimeException) {
|
||||
throw (RuntimeException) targetException;
|
||||
} else {
|
||||
throw new IllegalStateException(targetException);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,363 @@
|
|||
/*
|
||||
* Copyright (C) 2015 Haruki Hasegawa
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package it.niedermann.android.tablayouthelper;
|
||||
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.viewpager2.widget.ViewPager2;
|
||||
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
|
||||
/**
|
||||
* This is a fork of the android-tablayouthelper project to make it compatible with Viewpager2.
|
||||
* See also https://github.com/h6ah4i/android-tablayouthelper/issues/13
|
||||
*/
|
||||
public class TabLayoutHelper {
|
||||
private TabLayout mTabLayout;
|
||||
private TabTitleGenerator mTabTitleGenerator;
|
||||
private ViewPager2 mViewPager;
|
||||
|
||||
private TabLayout.OnTabSelectedListener mInternalOnTabSelectedListener;
|
||||
private FixedTabLayoutOnPageChangeListener mInternalTabLayoutOnPageChangeListener;
|
||||
private RecyclerView.AdapterDataObserver mInternalDataSetObserver;
|
||||
private Runnable mAdjustTabModeRunnable;
|
||||
private Runnable mSetTabsFromPagerAdapterRunnable;
|
||||
private Runnable mUpdateScrollPositionRunnable;
|
||||
private boolean mAutoAdjustTabMode = false;
|
||||
private boolean mDuringSetTabsFromPagerAdapter;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param tabLayout TabLayout instance
|
||||
* @param viewPager ViewPager2 instance
|
||||
* @param tabTitleGenerator TabTitleGenerator instance
|
||||
*/
|
||||
public TabLayoutHelper(@NonNull TabLayout tabLayout, @NonNull ViewPager2 viewPager, @NonNull TabTitleGenerator tabTitleGenerator) {
|
||||
RecyclerView.Adapter<?> adapter = viewPager.getAdapter();
|
||||
|
||||
if (adapter == null) {
|
||||
throw new IllegalArgumentException("ViewPager does not have a PagerAdapter set");
|
||||
}
|
||||
|
||||
mTabLayout = tabLayout;
|
||||
mViewPager = viewPager;
|
||||
mTabTitleGenerator = tabTitleGenerator;
|
||||
|
||||
|
||||
mInternalDataSetObserver = new RecyclerView.AdapterDataObserver() {
|
||||
@Override
|
||||
public void onChanged() {
|
||||
handleOnDataSetChanged();
|
||||
}
|
||||
};
|
||||
|
||||
mInternalOnTabSelectedListener = new TabLayout.OnTabSelectedListener() {
|
||||
@Override
|
||||
public void onTabSelected(TabLayout.Tab tab) {
|
||||
handleOnTabSelected(tab);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTabUnselected(TabLayout.Tab tab) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTabReselected(TabLayout.Tab tab) {
|
||||
// Do nothing
|
||||
}
|
||||
};
|
||||
|
||||
mInternalTabLayoutOnPageChangeListener = new FixedTabLayoutOnPageChangeListener(mTabLayout);
|
||||
|
||||
|
||||
viewPager.getAdapter().registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
|
||||
@Override
|
||||
public void onChanged() {
|
||||
setTabsFromPagerAdapter(mTabLayout, viewPager.getAdapter(), viewPager.getCurrentItem());
|
||||
}
|
||||
});
|
||||
|
||||
setupWithViewPager(mTabLayout, mViewPager);
|
||||
|
||||
setAutoAdjustTabModeEnabled(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets auto tab mode adjustment enabled
|
||||
*
|
||||
* @param enabled True for enabled, otherwise false.
|
||||
*/
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public void setAutoAdjustTabModeEnabled(boolean enabled) {
|
||||
if (mAutoAdjustTabMode == enabled) {
|
||||
return;
|
||||
}
|
||||
mAutoAdjustTabMode = enabled;
|
||||
|
||||
if (mAutoAdjustTabMode) {
|
||||
adjustTabMode(-1);
|
||||
} else {
|
||||
cancelPendingAdjustTabMode();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister internal listener objects, release object references, etc.
|
||||
* This method should be called in order to avoid memory leaks.
|
||||
*/
|
||||
public void release() {
|
||||
cancelPendingAdjustTabMode();
|
||||
cancelPendingSetTabsFromPagerAdapter();
|
||||
cancelPendingUpdateScrollPosition();
|
||||
|
||||
if (mInternalDataSetObserver != null) {
|
||||
mInternalDataSetObserver = null;
|
||||
}
|
||||
if (mInternalOnTabSelectedListener != null) {
|
||||
mTabLayout.removeOnTabSelectedListener(mInternalOnTabSelectedListener);
|
||||
mInternalOnTabSelectedListener = null;
|
||||
}
|
||||
if (mInternalTabLayoutOnPageChangeListener != null) {
|
||||
mInternalTabLayoutOnPageChangeListener = null;
|
||||
}
|
||||
mViewPager = null;
|
||||
mTabLayout = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override this method if you want to use custom tab layout.
|
||||
*
|
||||
* @param tabLayout TabLayout
|
||||
* @param position Position of the item
|
||||
* @return TabLayout.Tab
|
||||
*/
|
||||
private TabLayout.Tab onCreateTab(TabLayout tabLayout, int position) {
|
||||
TabLayout.Tab tab = tabLayout.newTab();
|
||||
tab.setText(mTabTitleGenerator.getTitle(position));
|
||||
return tab;
|
||||
}
|
||||
|
||||
public void setTabTitleGenerator(TabTitleGenerator mTabTitleGenerator) {
|
||||
this.mTabTitleGenerator = mTabTitleGenerator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override this method if you want to use custom tab layout
|
||||
*
|
||||
* @param tab Tab
|
||||
*/
|
||||
private void onUpdateTab(TabLayout.Tab tab) {
|
||||
if (tab.getCustomView() == null) {
|
||||
tab.setCustomView(null); // invokes update() method internally.
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// internal methods
|
||||
//
|
||||
private void handleOnDataSetChanged() {
|
||||
cancelPendingUpdateScrollPosition();
|
||||
cancelPendingSetTabsFromPagerAdapter();
|
||||
|
||||
if (mSetTabsFromPagerAdapterRunnable == null) {
|
||||
mSetTabsFromPagerAdapterRunnable = () -> setTabsFromPagerAdapter(mTabLayout, mViewPager.getAdapter(), mViewPager.getCurrentItem());
|
||||
}
|
||||
|
||||
mTabLayout.post(mSetTabsFromPagerAdapterRunnable);
|
||||
}
|
||||
|
||||
private void handleOnTabSelected(TabLayout.Tab tab) {
|
||||
if (mDuringSetTabsFromPagerAdapter) {
|
||||
return;
|
||||
}
|
||||
mViewPager.setCurrentItem(tab.getPosition());
|
||||
cancelPendingUpdateScrollPosition();
|
||||
}
|
||||
|
||||
private void cancelPendingAdjustTabMode() {
|
||||
if (mAdjustTabModeRunnable != null) {
|
||||
mTabLayout.removeCallbacks(mAdjustTabModeRunnable);
|
||||
mAdjustTabModeRunnable = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void cancelPendingSetTabsFromPagerAdapter() {
|
||||
if (mSetTabsFromPagerAdapterRunnable != null) {
|
||||
mTabLayout.removeCallbacks(mSetTabsFromPagerAdapterRunnable);
|
||||
mSetTabsFromPagerAdapterRunnable = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void cancelPendingUpdateScrollPosition() {
|
||||
if (mUpdateScrollPositionRunnable != null) {
|
||||
mTabLayout.removeCallbacks(mUpdateScrollPositionRunnable);
|
||||
mUpdateScrollPositionRunnable = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void adjustTabMode(final int prevScrollX) {
|
||||
final int prevScrollXMinZero = prevScrollX < 0 ? mTabLayout.getScrollX() : prevScrollX;
|
||||
|
||||
if (mAdjustTabModeRunnable != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ViewCompat.isLaidOut(mTabLayout)) {
|
||||
adjustTabModeInternal(mTabLayout, prevScrollXMinZero);
|
||||
} else {
|
||||
mAdjustTabModeRunnable = () -> {
|
||||
mAdjustTabModeRunnable = null;
|
||||
adjustTabModeInternal(mTabLayout, prevScrollXMinZero);
|
||||
};
|
||||
mTabLayout.post(mAdjustTabModeRunnable);
|
||||
}
|
||||
}
|
||||
|
||||
private TabLayout.Tab createNewTab(TabLayout tabLayout, int position) {
|
||||
return onCreateTab(tabLayout, position);
|
||||
}
|
||||
|
||||
private void setupWithViewPager(@NonNull TabLayout tabLayout, @NonNull ViewPager2 viewPager) {
|
||||
final RecyclerView.Adapter<?> adapter = viewPager.getAdapter();
|
||||
if (adapter == null) {
|
||||
throw new IllegalArgumentException("ViewPager does not have a PagerAdapter set");
|
||||
}
|
||||
|
||||
setTabsFromPagerAdapter(tabLayout, adapter, viewPager.getCurrentItem());
|
||||
viewPager.getAdapter().registerAdapterDataObserver(mInternalDataSetObserver);
|
||||
viewPager.registerOnPageChangeCallback(mInternalTabLayoutOnPageChangeListener);
|
||||
tabLayout.addOnTabSelectedListener(mInternalOnTabSelectedListener);
|
||||
}
|
||||
|
||||
private void setTabsFromPagerAdapter(@NonNull TabLayout tabLayout, @Nullable RecyclerView.Adapter<?> adapter, final int currentItem) {
|
||||
try {
|
||||
mDuringSetTabsFromPagerAdapter = true;
|
||||
|
||||
int prevScrollX = tabLayout.getScrollX();
|
||||
|
||||
// remove all tabs
|
||||
tabLayout.removeAllTabs();
|
||||
|
||||
// add tabs
|
||||
if (adapter != null) {
|
||||
int count = adapter.getItemCount();
|
||||
for (int i = 0; i < count; i++) {
|
||||
TabLayout.Tab tab = createNewTab(tabLayout, i);
|
||||
tabLayout.addTab(tab, false);
|
||||
updateTab(tab);
|
||||
}
|
||||
|
||||
// select current tab
|
||||
final int currentItemPosition = Math.min(currentItem, count - 1);
|
||||
TabLayout.Tab tab = tabLayout.getTabAt(currentItemPosition);
|
||||
if (currentItemPosition >= 0 && tab != null) {
|
||||
tab.select();
|
||||
}
|
||||
}
|
||||
|
||||
// adjust tab mode & gravity
|
||||
if (mAutoAdjustTabMode) {
|
||||
adjustTabMode(prevScrollX);
|
||||
} else {
|
||||
// restore scroll position if needed
|
||||
int curTabMode = tabLayout.getTabMode();
|
||||
if (curTabMode == TabLayout.MODE_SCROLLABLE) {
|
||||
tabLayout.scrollTo(prevScrollX, 0);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
mDuringSetTabsFromPagerAdapter = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void updateTab(TabLayout.Tab tab) {
|
||||
onUpdateTab(tab);
|
||||
}
|
||||
|
||||
private int determineTabMode(@NonNull TabLayout tabLayout) {
|
||||
LinearLayout slidingTabStrip = (LinearLayout) tabLayout.getChildAt(0);
|
||||
|
||||
int childCount = slidingTabStrip.getChildCount();
|
||||
|
||||
// NOTE: slidingTabStrip.getMeasuredWidth() method does not return correct width!
|
||||
// Need to measure each tabs and calculate the sum of them.
|
||||
|
||||
final int tabLayoutWidth = tabLayout.getMeasuredWidth() - tabLayout.getPaddingStart() - tabLayout.getPaddingEnd();
|
||||
final int tabLayoutHeight = tabLayout.getMeasuredHeight() - tabLayout.getPaddingTop() - tabLayout.getPaddingBottom();
|
||||
|
||||
if (childCount == 0) {
|
||||
return TabLayout.MODE_FIXED;
|
||||
}
|
||||
|
||||
int stripWidth = 0;
|
||||
int maxWidthTab = 0;
|
||||
int tabHeightMeasureSpec = View.MeasureSpec.makeMeasureSpec(tabLayoutHeight, View.MeasureSpec.EXACTLY);
|
||||
|
||||
for (int i = 0; i < childCount; i++) {
|
||||
View tabView = slidingTabStrip.getChildAt(i);
|
||||
tabView.measure(View.MeasureSpec.UNSPECIFIED, tabHeightMeasureSpec);
|
||||
int tabWidth = tabView.getMeasuredWidth();
|
||||
stripWidth += tabWidth;
|
||||
maxWidthTab = Math.max(maxWidthTab, tabWidth);
|
||||
}
|
||||
|
||||
return ((stripWidth < tabLayoutWidth) && (maxWidthTab < (tabLayoutWidth / childCount)))
|
||||
? TabLayout.MODE_FIXED : TabLayout.MODE_SCROLLABLE;
|
||||
}
|
||||
|
||||
private void adjustTabModeInternal(@NonNull TabLayout tabLayout, int prevScrollX) {
|
||||
int prevTabMode = tabLayout.getTabMode();
|
||||
|
||||
tabLayout.setTabMode(TabLayout.MODE_SCROLLABLE);
|
||||
tabLayout.setTabGravity(TabLayout.GRAVITY_CENTER);
|
||||
|
||||
int newTabMode = determineTabMode(tabLayout);
|
||||
|
||||
cancelPendingUpdateScrollPosition();
|
||||
|
||||
if (newTabMode == TabLayout.MODE_FIXED) {
|
||||
tabLayout.setTabGravity(TabLayout.GRAVITY_FILL);
|
||||
tabLayout.setTabMode(TabLayout.MODE_FIXED);
|
||||
} else {
|
||||
LinearLayout slidingTabStrip = (LinearLayout) tabLayout.getChildAt(0);
|
||||
slidingTabStrip.setGravity(Gravity.CENTER_HORIZONTAL);
|
||||
if (prevTabMode == TabLayout.MODE_SCROLLABLE) {
|
||||
// restore scroll position
|
||||
tabLayout.scrollTo(prevScrollX, 0);
|
||||
} else {
|
||||
// scroll to current selected tab
|
||||
mUpdateScrollPositionRunnable = () -> {
|
||||
mUpdateScrollPositionRunnable = null;
|
||||
updateScrollPosition();
|
||||
};
|
||||
mTabLayout.post(mUpdateScrollPositionRunnable);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updateScrollPosition() {
|
||||
mTabLayout.setScrollPosition(mTabLayout.getSelectedTabPosition(), 0, false);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package it.niedermann.android.tablayouthelper;
|
||||
|
||||
public interface TabTitleGenerator {
|
||||
String getTitle(int position);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue