Java tutorial
/* * 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 com.h6ah4i.android.tablayouthelper; import android.database.DataSetObserver; import android.support.annotation.NonNull; import android.support.design.widget.TabLayout; import android.support.v4.view.PagerAdapter; import android.support.v4.view.ViewCompat; import android.support.v4.view.ViewPager; import android.view.Gravity; import android.view.View; import android.widget.LinearLayout; import java.lang.ref.WeakReference; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; public class TabLayoutHelper { protected TabLayout mTabLayout; protected ViewPager mViewPager; protected TabLayout.OnTabSelectedListener mUserOnTabSelectedListener; protected TabLayout.OnTabSelectedListener mInternalOnTabSelectedListener; protected FixedTabLayoutOnPageChangeListener mInternalTabLayoutOnPageChangeListener; protected DataSetObserver mInternalDataSetObserver; protected Runnable mAdjustTabModeRunnable; protected Runnable mSetTabsFromPagerAdapterRunnable; protected Runnable mUpdateScrollPositionRunnable; protected boolean mAutoAdjustTabMode = false; /** * Constructor. * * @param tabLayout TabLayout instance * @param viewPager ViewPager instance */ public TabLayoutHelper(@NonNull TabLayout tabLayout, @NonNull ViewPager viewPager) { PagerAdapter adapter = viewPager.getAdapter(); if (adapter == null) { throw new IllegalArgumentException("ViewPager does not have a PagerAdapter set"); } mTabLayout = tabLayout; mViewPager = viewPager; mInternalDataSetObserver = new DataSetObserver() { @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) { handleOnTabUnselected(tab); } @Override public void onTabReselected(TabLayout.Tab tab) { handleOnTabReselected(tab); } }; mInternalTabLayoutOnPageChangeListener = new FixedTabLayoutOnPageChangeListener(mTabLayout); setupWithViewPager(mTabLayout, mViewPager); } // // public methods // /** * Retrieve underlying TabLayout instance. * * @return TabLayout instance */ public TabLayout getTabLayout() { return mTabLayout; } /** * Retrieve ViewPager instance. * * @return ViewPager instance */ public ViewPager getViewPager() { return mViewPager; } /** * Sets auto tab mode adjustment enabled * * @param enabled True for enabled, otherwise false. */ public void setAutoAdjustTabModeEnabled(boolean enabled) { if (mAutoAdjustTabMode == enabled) { return; } mAutoAdjustTabMode = enabled; if (mAutoAdjustTabMode) { adjustTabMode(-1); } else { cancelPendingAdjustTabMode(); } } /** * Gets whether auto tab mode adjustment is enabled. * * @return True for enabled, otherwise false. */ public boolean isAutoAdjustTabModeEnabled() { return mAutoAdjustTabMode; } /** * Sets {@link TabLayout.OnTabSelectedListener} * * @param listener Listener */ public void setOnTabSelectedListener(TabLayout.OnTabSelectedListener listener) { mUserOnTabSelectedListener = listener; } /** * 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) { mViewPager.getAdapter().unregisterDataSetObserver(mInternalDataSetObserver); mInternalDataSetObserver = null; } if (mInternalOnTabSelectedListener != null) { mTabLayout.setOnTabSelectedListener(null); mInternalOnTabSelectedListener = null; } if (mInternalTabLayoutOnPageChangeListener != null) { mViewPager.removeOnPageChangeListener(mInternalTabLayoutOnPageChangeListener); mInternalTabLayoutOnPageChangeListener = null; } mUserOnTabSelectedListener = null; mViewPager = null; mTabLayout = null; } public void updateAllTabs() { int count = mTabLayout.getTabCount(); for (int i = 0; i < count; i++) { updateTab(mTabLayout.getTabAt(i)); } } /** * Override this method if you want to use custom tab layout. * * @param tabLayout TabLayout * @param adapter PagerAdapter * @param position Position of the item * @return TabLayout.Tab */ protected TabLayout.Tab onCreateTab(TabLayout tabLayout, PagerAdapter adapter, int position) { TabLayout.Tab tab = tabLayout.newTab(); tab.setText(adapter.getPageTitle(position)); return tab; } /** * Override this method if you want to use custom tab layout * * @param tab Tab */ protected void onUpdateTab(TabLayout.Tab tab) { tab.setCustomView(null); // invokes update() method internally. } // // internal methods // protected void handleOnDataSetChanged() { cancelPendingUpdateScrollPosition(); cancelPendingSetTabsFromPagerAdapter(); if (mSetTabsFromPagerAdapterRunnable == null) { mSetTabsFromPagerAdapterRunnable = new Runnable() { @Override public void run() { setTabsFromPagerAdapter(mTabLayout, mViewPager.getAdapter(), mViewPager.getCurrentItem()); } }; } mTabLayout.post(mSetTabsFromPagerAdapterRunnable); } protected void handleOnTabSelected(TabLayout.Tab tab) { mViewPager.setCurrentItem(tab.getPosition()); cancelPendingUpdateScrollPosition(); if (mUserOnTabSelectedListener != null) { mUserOnTabSelectedListener.onTabSelected(tab); } } protected void handleOnTabUnselected(TabLayout.Tab tab) { if (mUserOnTabSelectedListener != null) { mUserOnTabSelectedListener.onTabUnselected(tab); } } protected void handleOnTabReselected(TabLayout.Tab tab) { if (mUserOnTabSelectedListener != null) { mUserOnTabSelectedListener.onTabReselected(tab); } } protected void cancelPendingAdjustTabMode() { if (mAdjustTabModeRunnable != null) { mTabLayout.removeCallbacks(mAdjustTabModeRunnable); mAdjustTabModeRunnable = null; } } protected void cancelPendingSetTabsFromPagerAdapter() { if (mSetTabsFromPagerAdapterRunnable != null) { mTabLayout.removeCallbacks(mSetTabsFromPagerAdapterRunnable); mSetTabsFromPagerAdapterRunnable = null; } } protected void cancelPendingUpdateScrollPosition() { if (mUpdateScrollPositionRunnable != null) { mTabLayout.removeCallbacks(mUpdateScrollPositionRunnable); mUpdateScrollPositionRunnable = null; } } protected void adjustTabMode(int prevScrollX) { if (mAdjustTabModeRunnable != null) { return; } if (prevScrollX < 0) { prevScrollX = mTabLayout.getScrollX(); } if (ViewCompat.isLaidOut(mTabLayout)) { adjustTabModeInternal(mTabLayout, prevScrollX); } else { final int prevScrollX1 = prevScrollX; mAdjustTabModeRunnable = new Runnable() { @Override public void run() { mAdjustTabModeRunnable = null; adjustTabModeInternal(mTabLayout, prevScrollX1); } }; mTabLayout.post(mAdjustTabModeRunnable); } } protected TabLayout.Tab createNewTab(TabLayout tabLayout, PagerAdapter adapter, int position) { return onCreateTab(tabLayout, adapter, position); } protected void setupWithViewPager(@NonNull TabLayout tabLayout, @NonNull ViewPager viewPager) { final PagerAdapter adapter = viewPager.getAdapter(); if (adapter == null) { throw new IllegalArgumentException("ViewPager does not have a PagerAdapter set"); } setTabsFromPagerAdapter(tabLayout, adapter, viewPager.getCurrentItem()); viewPager.getAdapter().registerDataSetObserver(mInternalDataSetObserver); viewPager.addOnPageChangeListener(mInternalTabLayoutOnPageChangeListener); tabLayout.setOnTabSelectedListener(mInternalOnTabSelectedListener); } protected void setTabsFromPagerAdapter(@NonNull TabLayout tabLayout, PagerAdapter adapter, int currentItem) { int prevSelectedTab = tabLayout.getSelectedTabPosition(); int prevScrollX = tabLayout.getScrollX(); // remove all tabs tabLayout.removeAllTabs(); // add tabs int count = adapter.getCount(); for (int i = 0; i < count; i++) { TabLayout.Tab tab = createNewTab(tabLayout, adapter, i); tabLayout.addTab(tab, false); updateTab(tab); } // select current tab currentItem = Math.min(currentItem, count - 1); if (currentItem >= 0) { if (prevSelectedTab == currentItem) { tabLayout.setOnTabSelectedListener(null); } tabLayout.getTabAt(currentItem).select(); if (prevSelectedTab == currentItem) { tabLayout.setOnTabSelectedListener(mInternalOnTabSelectedListener); } } // 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); } } } protected void updateTab(TabLayout.Tab tab) { onUpdateTab(tab); } protected 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; } protected 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 = new Runnable() { @Override public void run() { mUpdateScrollPositionRunnable = null; updateScrollPosition(); } }; mTabLayout.post(mUpdateScrollPositionRunnable); } } } private void updateScrollPosition() { mTabLayout.setScrollPosition(mTabLayout.getSelectedTabPosition(), 0, false); } protected static class FixedTabLayoutOnPageChangeListener implements ViewPager.OnPageChangeListener { private final WeakReference<TabLayout> mTabLayoutRef; private int mPreviousScrollState; private int mScrollState; public 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) { if (shouldUpdateScrollPosition()) { // Update the scroll position, only update the text selection if we're being // dragged (or we're settling after a drag) final boolean updateText = (mScrollState == ViewPager.SCROLL_STATE_DRAGGING) || (mScrollState == ViewPager.SCROLL_STATE_SETTLING && mPreviousScrollState == ViewPager.SCROLL_STATE_DRAGGING); tabLayout.setScrollPosition(position, positionOffset, updateText); } } } @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 == ViewPager.SCROLL_STATE_IDLE); } } private boolean shouldUpdateScrollPosition() { return (mScrollState == ViewPager.SCROLL_STATE_DRAGGING) || ((mScrollState == ViewPager.SCROLL_STATE_SETTLING) && (mPreviousScrollState == ViewPager.SCROLL_STATE_DRAGGING)); } } static class Internal { private static final Method mMethodSelectTab; static { mMethodSelectTab = getAccessiblePrivateMethod(TabLayout.class, "selectTab", TabLayout.Tab.class, boolean.class); } 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); } } public static void selectTab(TabLayout tabLayout, TabLayout.Tab tab, boolean updateIndicator) { try { mMethodSelectTab.invoke(tabLayout, tab, updateIndicator); } catch (IllegalAccessException e) { 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); } } } }