diff options
author | Stefan Niedermann <info@niedermann.it> | 2020-04-08 12:42:37 +0300 |
---|---|---|
committer | Stefan Niedermann <info@niedermann.it> | 2020-04-08 12:42:37 +0300 |
commit | 35f20c292bb4f1f3c7b9aadc8427fd86358a64e9 (patch) | |
tree | a72f3cd5791fb144ab295149e8e12e7edf377618 /tab-layout-helper | |
parent | aae206befa986e11f29b61d8761b1cf7b6599fa3 (diff) |
Move TabLayoutHelper to own library
Signed-off-by: Stefan Niedermann <info@niedermann.it>
Diffstat (limited to 'tab-layout-helper')
8 files changed, 519 insertions, 0 deletions
diff --git a/tab-layout-helper/.gitignore b/tab-layout-helper/.gitignore new file mode 100644 index 000000000..796b96d1c --- /dev/null +++ b/tab-layout-helper/.gitignore @@ -0,0 +1 @@ +/build diff --git a/tab-layout-helper/build.gradle b/tab-layout-helper/build.gradle new file mode 100644 index 000000000..c9b3119fb --- /dev/null +++ b/tab-layout-helper/build.gradle @@ -0,0 +1,36 @@ +apply plugin: 'com.android.library' + +android { + compileSdkVersion 29 + buildToolsVersion "29.0.3" + + defaultConfig { + minSdkVersion 14 + targetSdkVersion 29 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles 'consumer-rules.pro' + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + + implementation 'androidx.appcompat:appcompat:1.1.0' + implementation "androidx.viewpager2:viewpager2:1.0.0" + implementation 'com.google.android.material:material:1.1.0' +} diff --git a/tab-layout-helper/consumer-rules.pro b/tab-layout-helper/consumer-rules.pro new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/tab-layout-helper/consumer-rules.pro diff --git a/tab-layout-helper/proguard-rules.pro b/tab-layout-helper/proguard-rules.pro new file mode 100644 index 000000000..f1b424510 --- /dev/null +++ b/tab-layout-helper/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/tab-layout-helper/src/main/AndroidManifest.xml b/tab-layout-helper/src/main/AndroidManifest.xml new file mode 100644 index 000000000..0e49f42ac --- /dev/null +++ b/tab-layout-helper/src/main/AndroidManifest.xml @@ -0,0 +1 @@ +<manifest package="it.niedermann.android.tablayouthelper" /> diff --git a/tab-layout-helper/src/main/java/it/niedermann/android/tablayouthelper/FixedTabLayoutOnPageChangeListener.java b/tab-layout-helper/src/main/java/it/niedermann/android/tablayouthelper/FixedTabLayoutOnPageChangeListener.java new file mode 100644 index 000000000..c3d33d083 --- /dev/null +++ b/tab-layout-helper/src/main/java/it/niedermann/android/tablayouthelper/FixedTabLayoutOnPageChangeListener.java @@ -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); + } + } + } +}
\ No newline at end of file diff --git a/tab-layout-helper/src/main/java/it/niedermann/android/tablayouthelper/TabLayoutHelper.java b/tab-layout-helper/src/main/java/it/niedermann/android/tablayouthelper/TabLayoutHelper.java new file mode 100644 index 000000000..2402fb1de --- /dev/null +++ b/tab-layout-helper/src/main/java/it/niedermann/android/tablayouthelper/TabLayoutHelper.java @@ -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. + + int tabLayoutWidth = tabLayout.getMeasuredWidth() - tabLayout.getPaddingLeft() - tabLayout.getPaddingRight(); + 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); + } +} diff --git a/tab-layout-helper/src/main/java/it/niedermann/android/tablayouthelper/TabTitleGenerator.java b/tab-layout-helper/src/main/java/it/niedermann/android/tablayouthelper/TabTitleGenerator.java new file mode 100644 index 000000000..1640fe21d --- /dev/null +++ b/tab-layout-helper/src/main/java/it/niedermann/android/tablayouthelper/TabTitleGenerator.java @@ -0,0 +1,5 @@ +package it.niedermann.android.tablayouthelper; + +public interface TabTitleGenerator { + String getTitle(int position); +}
\ No newline at end of file |