Repo created

This commit is contained in:
Fr4nz D13trich 2025-11-23 08:51:57 +01:00
parent 61d62cabdf
commit ad3df69bdb
872 changed files with 65976 additions and 2 deletions

View 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"
}

View file

@ -0,0 +1 @@
<manifest />

View file

@ -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);
}
}
}
}

View file

@ -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);
}
}

View file

@ -0,0 +1,5 @@
package it.niedermann.android.tablayouthelper;
public interface TabTitleGenerator {
String getTitle(int position);
}