Android 支持设计 TabLayout: 重心和模式可滚动

我试图在我的项目中使用新的 Design TabLayout。我希望布局适应每个屏幕大小和方向,但它可以在一个方向正确地看到。

我正在处理重力和模式设置我的 tabLayout 为:

    tabLayout.setTabGravity(TabLayout.GRAVITY_CENTER);
tabLayout.setTabMode(TabLayout.MODE_SCROLLABLE);

所以我希望如果没有空间,tabLayout 是可滚动的,但是如果有空间,它是居中的。

导游说:

Public static final int GRAVITY _ CENTER GRAVITY 用于布局 在 TabLayout 中心的选项卡。

用于填充 尽可能使用 TabLayout。此选项仅在使用时生效 具有 MODE _ FixED。

固定选项卡显示所有选项卡 并且最好与受益于快速 选项卡之间的枢轴。选项卡的最大数量受 视图的宽度。根据最宽的选项卡,固定选项卡的宽度相等 标签。

Public static final int MODE _ SCROLLABLE 可滚动选项卡显示 标签的子集,并且可以包含更长的标签 以及更多的选项卡。它们最适合用于浏览上下文 在触摸界面时,用户不需要直接比较标签 标签。

所以 GRAVITY _ FILL 只与 MODE _ Fixed 兼容,但是 at 没有为 GRAVITY _ CENTER 指定任何东西,我希望它与 MODE _ SCROLLABLE 兼容,但是这是我使用 GRAVITY _ CENTER 和 MODE _ SCROLLABLE 得到的

enter image description here

因此它在两个方向上都使用 SCROLLABLE,但是它没有使用 GRAVITY _ CENTER。

这就是我所期望的风景; 但是为了得到这个,我需要设置 MODE _ FIXED,所以我在肖像中得到的是:

enter image description here

如果 tabLayout 适合屏幕,为什么 GRAVITY _ CENTER 不适合 SCROLLABLE? 有没有办法动态设置重力和模式(看看我期望的是什么) ?

非常感谢!

编辑: 这是我的桌面布局:

<android.support.design.widget.TabLayout
android:id="@+id/sliding_tabs"
android:layout_width="match_parent"
android:background="@color/orange_pager"
android:layout_height="wrap_content" />
124394 次浏览

As I didn't find why does this behaviour happen I have used the following code:

float myTabLayoutSize = 360;
if (DeviceInfo.getWidthDP(this) >= myTabLayoutSize ){
tabLayout.setTabMode(TabLayout.MODE_FIXED);
} else {
tabLayout.setTabMode(TabLayout.MODE_SCROLLABLE);
}

Basically, I have to calculate manually the width of my tabLayout and then I set the Tab Mode depending on if the tabLayout fits in the device or not.

The reason why I get the size of the layout manually is because not all the tabs have the same width in Scrollable mode, and this could provoke that some names use 2 lines as it happened to me in the example.

Tab gravity only effects MODE_FIXED.

One possible solution is to set your layout_width to wrap_content and layout_gravity to center_horizontal:

<android.support.design.widget.TabLayout
android:id="@+id/sliding_tabs"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
app:tabMode="scrollable" />

If the tabs are smaller than the screen width, the TabLayout itself will also be smaller and it will be centered because of the gravity. If the tabs are bigger than the screen width, the TabLayout will match the screen width and scrolling will activate.

Look at android-tablayouthelper

Automatically switch TabLayout.MODE_FIXED and TabLayout.MODE_SCROLLABLE depends on total tab width.

This is the solution I used to automatically change between SCROLLABLE and FIXED+FILL. It is the complete code for the @Fighter42 solution:

(The code below shows where to put the modification if you've used Google's tabbed activity template)

@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);


Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
// Create the adapter that will return a fragment for each of the three
// primary sections of the activity.
mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());


// Set up the ViewPager with the sections adapter.
mViewPager = (ViewPager) findViewById(R.id.container);
mViewPager.setAdapter(mSectionsPagerAdapter);


// Set up the tabs
final TabLayout tabLayout = (TabLayout) findViewById(R.id.tabs);
tabLayout.setupWithViewPager(mViewPager);


// Mario Velasco's code
tabLayout.post(new Runnable()
{
@Override
public void run()
{
int tabLayoutWidth = tabLayout.getWidth();


DisplayMetrics metrics = new DisplayMetrics();
ActivityMain.this.getWindowManager().getDefaultDisplay().getMetrics(metrics);
int deviceWidth = metrics.widthPixels;


if (tabLayoutWidth < deviceWidth)
{
tabLayout.setTabMode(TabLayout.MODE_FIXED);
tabLayout.setTabGravity(TabLayout.GRAVITY_FILL);
} else
{
tabLayout.setTabMode(TabLayout.MODE_SCROLLABLE);
}
}
});
}

Layout:

    <android.support.design.widget.TabLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal" />

If you don't need to fill width, better to use @karaokyo solution.

I made small changes of @Mario Velasco's solution on the runnable part:

TabLayout.xml

<android.support.design.widget.TabLayout
android:id="@+id/tab_layout"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:background="@android:color/transparent"
app:tabGravity="fill"
app:tabMode="scrollable"
app:tabTextAppearance="@style/TextAppearance.Design.Tab"
app:tabSelectedTextColor="@color/myPrimaryColor"
app:tabIndicatorColor="@color/myPrimaryColor"
android:overScrollMode="never"
/>

Oncreate

@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mToolbar = (Toolbar) findViewById(R.id.toolbar_actionbar);
mTabLayout = (TabLayout)findViewById(R.id.tab_layout);
mTabLayout.setOnTabSelectedListener(this);
setSupportActionBar(mToolbar);


mTabLayout.addTab(mTabLayout.newTab().setText("Dashboard"));
mTabLayout.addTab(mTabLayout.newTab().setText("Signature"));
mTabLayout.addTab(mTabLayout.newTab().setText("Booking/Sampling"));
mTabLayout.addTab(mTabLayout.newTab().setText("Calendar"));
mTabLayout.addTab(mTabLayout.newTab().setText("Customer Detail"));


mTabLayout.post(mTabLayout_config);
}


Runnable mTabLayout_config = new Runnable()
{
@Override
public void run()
{
              

if(mTabLayout.getWidth() < MainActivity.this.getResources().getDisplayMetrics().widthPixels)
{
mTabLayout.setTabMode(TabLayout.MODE_FIXED);
ViewGroup.LayoutParams mParams = mTabLayout.getLayoutParams();
mParams.width = ViewGroup.LayoutParams.MATCH_PARENT;
mTabLayout.setLayoutParams(mParams);


}
else
{
mTabLayout.setTabMode(TabLayout.MODE_SCROLLABLE);
}
}
};

This is the only code that worked for me:

public static void adjustTabLayoutBounds(final TabLayout tabLayout,
final DisplayMetrics displayMetrics){


final ViewTreeObserver vto = tabLayout.getViewTreeObserver();
vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {


@Override
public void onGlobalLayout() {


tabLayout.getViewTreeObserver().removeOnGlobalLayoutListener(this);


int totalTabPaddingPlusWidth = 0;
for(int i=0; i < tabLayout.getTabCount(); i++){


final LinearLayout tabView = ((LinearLayout)((LinearLayout) tabLayout.getChildAt(0)).getChildAt(i));
totalTabPaddingPlusWidth += (tabView.getMeasuredWidth() + tabView.getPaddingLeft() + tabView.getPaddingRight());
}


if (totalTabPaddingPlusWidth <= displayMetrics.widthPixels){


tabLayout.setTabMode(TabLayout.MODE_FIXED);
tabLayout.setTabGravity(TabLayout.GRAVITY_FILL);


}else{
tabLayout.setTabMode(TabLayout.MODE_SCROLLABLE);
}


tabLayout.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT));
}
});
}

The DisplayMetrics can be retrieved using this:

public DisplayMetrics getDisplayMetrics() {


final WindowManager wm = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
final Display display = wm.getDefaultDisplay();
final DisplayMetrics displayMetrics = new DisplayMetrics();


if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) {
display.getMetrics(displayMetrics);


}else{
display.getRealMetrics(displayMetrics);
}


return displayMetrics;
}

And your TabLayout XML should look like this (don't forget to set tabMaxWidth to 0):

<android.support.design.widget.TabLayout
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/tab_layout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:tabMaxWidth="0dp"/>

keeps things simple just add app:tabMode="scrollable" and android:layout_gravity= "bottom"

just like this

<android.support.design.widget.TabLayout
android:id="@+id/tabs"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:layout_gravity="bottom"
app:tabMode="scrollable"
app:tabIndicatorColor="@color/colorAccent" />

enter image description here

All you need is to add the following to your TabLayout

custom:tabGravity="fill"

So then you'll have:

xmlns:custom="http://schemas.android.com/apk/res-auto"
<android.support.design.widget.TabLayout
android:id="@+id/tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
custom:tabGravity="fill"
/>

I created an AdaptiveTabLayout class to achieve this. This was the only way I found to actually solve the problem, answer the question and avoid/workaround problems that other answers here don't.

Notes:

  • Handles phone/tablet layouts.
  • Handles cases where there's enough room for MODE_SCROLLABLE but not enough room for MODE_FIXED. If you don't handle this case it's gonna happen on some devices you'll see different text sizes or oven two lines of text in some tabs, which looks bad.
  • It gets real measures and doesn't make any assumptions (like screen is 360dp wide or whatever...). This works with real screen sizes and real tab sizes. This means works well with translations because doesn't assume any tab size, the tabs get measure.
  • Deals with the different passes on the onLayout phase in order to avoid extra work.
  • Layout width needs to be wrap_content on the xml. Don't set any mode or gravity on the xml.

AdaptiveTabLayout.java

import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.design.widget.TabLayout;
import android.util.AttributeSet;
import android.widget.LinearLayout;


public class AdaptiveTabLayout extends TabLayout
{
private boolean mGravityAndModeSeUpNeeded = true;


public AdaptiveTabLayout(@NonNull final Context context)
{
this(context, null);
}


public AdaptiveTabLayout(@NonNull final Context context, @Nullable final AttributeSet attrs)
{
this(context, attrs, 0);
}


public AdaptiveTabLayout
(
@NonNull final Context context,
@Nullable final AttributeSet attrs,
final int defStyleAttr
)
{
super(context, attrs, defStyleAttr);
setTabMode(MODE_SCROLLABLE);
}


@Override
protected void onLayout(final boolean changed, final int l, final int t, final int r, final int b)
{
super.onLayout(changed, l, t, r, b);


if (mGravityAndModeSeUpNeeded)
{
setModeAndGravity();
}
}


private void setModeAndGravity()
{
final int tabCount = getTabCount();
final int screenWidth = UtilsDevice.getScreenWidth();
final int minWidthNeedForMixedMode = getMinSpaceNeededForFixedMode(tabCount);


if (minWidthNeedForMixedMode == 0)
{
return;
}
else if (minWidthNeedForMixedMode < screenWidth)
{
setTabMode(MODE_FIXED);
setTabGravity(UtilsDevice.isBigTablet() ? GRAVITY_CENTER : GRAVITY_FILL) ;
}
else
{
setTabMode(TabLayout.MODE_SCROLLABLE);
}


setLayoutParams(new LinearLayout.LayoutParams
(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT));


mGravityAndModeSeUpNeeded = false;
}


private int getMinSpaceNeededForFixedMode(final int tabCount)
{
final LinearLayout linearLayout = (LinearLayout) getChildAt(0);
int widestTab = 0;
int currentWidth;


for (int i = 0; i < tabCount; i++)
{
currentWidth = linearLayout.getChildAt(i).getWidth();


if (currentWidth == 0) return 0;


if (currentWidth > widestTab)
{
widestTab = currentWidth;
}
}


return widestTab * tabCount;
}


}

And this is the DeviceUtils class:

import android.content.res.Resources;


public class UtilsDevice extends Utils
{
private static final int sWIDTH_FOR_BIG_TABLET_IN_DP = 720;


private UtilsDevice() {}


public static int pixelToDp(final int pixels)
{
return (int) (pixels / Resources.getSystem().getDisplayMetrics().density);
}


public static int getScreenWidth()
{
return Resources
.getSystem()
.getDisplayMetrics()
.widthPixels;
}


public static boolean isBigTablet()
{
return pixelToDp(getScreenWidth()) >= sWIDTH_FOR_BIG_TABLET_IN_DP;
}


}

Use example:

<?xml version="1.0" encoding="utf-8"?>


<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">


<com.com.stackoverflow.example.AdaptiveTabLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?colorPrimary"
app:tabIndicatorColor="@color/white"
app:tabSelectedTextColor="@color/text_white_primary"
app:tabTextColor="@color/text_white_secondary"
tools:layout_width="match_parent"/>


</FrameLayout>

Gist

Problems/Ask for help:

  • You'll see this:

Logcat:

W/View: requestLayout() improperly called by android.support.design.widget.TabLayout$SlidingTabStrip{3e1ebcd6 V.ED.... ......ID 0,0-466,96} during layout: running second layout pass
W/View: requestLayout() improperly called by android.support.design.widget.TabLayout$TabView{3423cb57 VFE...C. ..S...ID 0,0-144,96} during layout: running second layout pass
W/View: requestLayout() improperly called by android.support.design.widget.TabLayout$TabView{377c4644 VFE...C. ......ID 144,0-322,96} during layout: running second layout pass
W/View: requestLayout() improperly called by android.support.design.widget.TabLayout$TabView{19ead32d VFE...C. ......ID 322,0-466,96} during layout: running second layout pass

I'm not sure how to solve it. Any suggestions?

  • To make the TabLayout child measures, I'm making some castings and assumptions (Like the child is a LinearLayout containing other views....) This might cause problems with in further Design Support Library updates. A better approach/suggestions?
if(tabLayout_chemistCategory.getTabCount()<4)
{
tabLayout_chemistCategory.setTabGravity(TabLayout.GRAVITY_FILL);
}else
{
tabLayout_chemistCategory.setTabMode(TabLayout.MODE_SCROLLABLE);


}
 <android.support.design.widget.TabLayout
android:id="@+id/tabList"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
app:tabMode="scrollable"/>


Very simple example and it always works.

/**
* Setup stretch and scrollable TabLayout.
* The TabLayout initial parameters in layout must be:
* android:layout_width="wrap_content"
* app:tabMaxWidth="0dp"
* app:tabGravity="fill"
* app:tabMode="fixed"
*
* @param context   your Context
* @param tabLayout your TabLayout
*/
public static void setupStretchTabLayout(Context context, TabLayout tabLayout) {
tabLayout.post(() -> {


ViewGroup.LayoutParams params = tabLayout.getLayoutParams();
if (params.width == ViewGroup.LayoutParams.MATCH_PARENT) { // is already set up for stretch
return;
}


int deviceWidth = context.getResources()
.getDisplayMetrics().widthPixels;


if (tabLayout.getWidth() < deviceWidth) {
tabLayout.setTabMode(TabLayout.MODE_FIXED);
params.width = ViewGroup.LayoutParams.MATCH_PARENT;
} else {
tabLayout.setTabMode(TabLayout.MODE_SCROLLABLE);
params.width = ViewGroup.LayoutParams.WRAP_CONTENT;
}
tabLayout.setLayoutParams(params);


});


}
class DynamicModeTabLayout : TabLayout {
 

constructor(context: Context?) : super(context)
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)


override fun setupWithViewPager(viewPager: ViewPager?) {
super.setupWithViewPager(viewPager)


val view = getChildAt(0) ?: return
view.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED)
val size = view.measuredWidth


if (size > measuredWidth) {
tabMode = MODE_SCROLLABLE
tabGravity = GRAVITY_CENTER
} else {
tabMode = MODE_FIXED
tabGravity = GRAVITY_FILL
}
}
}

add this line in your actiity when you adding tabs in tablayout

 tabLayout.setTabGravity(TabLayout.GRAVITY_FILL);

I think a better approach will be to set app:tabMode="auto" and app:tabGravity="fill" because setting tabMode to fixed can make headings congested and cause headings to occupy multiple lines on the other side setting it to scrollable could make them leave spaces at the end in some screen sizes. manually setting tabMode would give a problem when dealing with multiple screen sizes

    <com.google.android.material.tabs.TabLayout
android:id="@+id/tabLayout"
app:tabGravity="fill"
android:textAlignment="center"
app:tabMode="auto"
/>

The Sotti's solution is great! It works exactly as the basis component should work.

In my case the tabs can evolve dynamically according a filter change, so I have done small adaptation to allow the tabmode be updated with the redraw() method. It's also in Kotlin

class AdaptiveTabLayout @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : TabLayout(context, attrs, defStyleAttr) {
private var gravityAndModeSeUpNeeded = true


override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
super.onLayout(changed, l, t, r, b)
if (gravityAndModeSeUpNeeded) {
setModeAndGravity()
}
}


fun redraw() {
post {
tabMode = MODE_SCROLLABLE
gravityAndModeSeUpNeeded = true
invalidate()
}
}


private fun setModeAndGravity() {
val tabCount = tabCount
val screenWidth = Utils.getScreenWidth()
val minWidthNeedForMixedMode = getMinSpaceNeededForFixedMode(tabCount)
if (minWidthNeedForMixedMode == 0) {
return
} else if (minWidthNeedForMixedMode < screenWidth) {
tabMode = MODE_FIXED
tabGravity = if (Utils.isBigTablet()) GRAVITY_CENTER else GRAVITY_FILL
} else {
tabMode = MODE_SCROLLABLE
}
gravityAndModeSeUpNeeded = false
}


private fun getMinSpaceNeededForFixedMode(tabCount: Int): Int {
val linearLayout = getChildAt(0) as LinearLayout
var widestTab = 0
var currentWidth: Int
for (i in 0 until tabCount) {
currentWidth = linearLayout.getChildAt(i).width
if (currentWidth == 0) return 0
if (currentWidth > widestTab) {
widestTab = currentWidth
}
}
return widestTab * tabCount
}


init {
tabMode = MODE_SCROLLABLE
}
}