├── .gitignore ├── .idea ├── compiler.xml ├── copyright │ └── profiles_settings.xml ├── gradle.xml ├── inspectionProfiles │ └── Project_Default.xml ├── misc.xml ├── modules.xml ├── runConfigurations.xml └── vcs.xml ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── mrs │ │ └── materialtablayout │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── mrs │ │ │ └── materialtablayout │ │ │ ├── MainActivity.java │ │ │ └── MaterialTabLayout.java │ └── res │ │ ├── layout │ │ ├── activity_main.xml │ │ ├── viewpager_item1.xml │ │ ├── viewpager_item2.xml │ │ ├── viewpager_item3.xml │ │ ├── viewpager_item4.xml │ │ └── viewpager_item5.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── mrs │ └── materialtablayout │ └── ExampleUnitTest.java ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── imgs ├── 3.gif └── 4.gif └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | .externalNativeBuild 10 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 26 | 27 | 28 | 29 | 30 | 31 | 33 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MaterialTabLayout 2 | ### 两种方式自定义Tablayout指示器长度 3 | ### 添加依赖 4 | ```java 5 | dependencies{ 6 | compile 'com.qiaomu.library:tablayout:1.0.3' 7 | } 8 | ``` 9 | 10 | ### 修改源代码 11 | 12 | ![image](https://github.com/mrme2014/MaterialTabLayout/raw/master/imgs/3.gif) 13 | 14 | ### 反射修改 15 | 16 | ![image](https://github.com/mrme2014/MaterialTabLayout/raw/master/imgs/4.gif) 17 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 25 5 | buildToolsVersion '25.0.0' 6 | defaultConfig { 7 | applicationId "com.mrs.materialtablayout" 8 | minSdkVersion 15 9 | targetSdkVersion 25 10 | versionCode 1 11 | versionName "1.0" 12 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | compile fileTree(include: ['*.jar'], dir: 'libs') 24 | androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { 25 | exclude group: 'com.android.support', module: 'support-annotations' 26 | }) 27 | compile 'com.android.support:appcompat-v7:25.1.1' 28 | compile 'com.android.support.constraint:constraint-layout:1.0.2' 29 | testCompile 'junit:junit:4.12' 30 | compile 'com.android.support:design:25.1.1' 31 | } 32 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in C:\Users\mrs\AppData\Local\Android\Sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | 19 | # Uncomment this to preserve the line number information for 20 | # debugging stack traces. 21 | #-keepattributes SourceFile,LineNumberTable 22 | 23 | # If you keep the line number information, uncomment this to 24 | # hide the original source file name. 25 | #-renamesourcefileattribute SourceFile 26 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/mrs/materialtablayout/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.mrs.materialtablayout; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumentation test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() throws Exception { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("com.mrs.materialtablayout", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/mrs/materialtablayout/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.mrs.materialtablayout; 2 | 3 | import android.graphics.Color; 4 | import android.os.Bundle; 5 | import android.support.annotation.NonNull; 6 | import android.support.design.widget.TabLayout; 7 | import android.support.v4.view.ViewPager; 8 | import android.support.v7.app.AppCompatActivity; 9 | import android.view.LayoutInflater; 10 | import android.view.View; 11 | import android.view.ViewGroup; 12 | import android.widget.FrameLayout; 13 | import android.widget.LinearLayout; 14 | import android.widget.Toast; 15 | 16 | import java.lang.reflect.Field; 17 | import java.util.ArrayList; 18 | import java.util.TreeMap; 19 | 20 | public class MainActivity extends AppCompatActivity { 21 | 22 | MaterialTabLayout materialTabLayout; 23 | android.support.design.widget.TabLayout tableLayout; 24 | ViewPager materialVp; 25 | ViewPager vp; 26 | 27 | 28 | FrameLayout content; 29 | 30 | @Override 31 | protected void onCreate(Bundle savedInstanceState) { 32 | super.onCreate(savedInstanceState); 33 | setContentView(R.layout.activity_main); 34 | 35 | materialTabLayout = (MaterialTabLayout) findViewById(R.id.materialTab); 36 | tableLayout = (android.support.design.widget.TabLayout) findViewById(R.id.Tab); 37 | materialVp = (ViewPager) findViewById(R.id.materialVp); 38 | vp = (ViewPager) findViewById(R.id.vp); 39 | 40 | content = (FrameLayout) findViewById(R.id.content); 41 | setUpMaterialTab(); 42 | 43 | setTab(); 44 | } 45 | 46 | public void setUpMaterialTab() { 47 | 48 | final ArrayList mViewList = getViewList(); 49 | final ArrayList mTitleList = getTitleList(); 50 | 51 | 52 | materialVp.setAdapter(new MyPagerAdapter(mViewList, mTitleList)); 53 | materialTabLayout 54 | .setupWithViewPager(materialVp) 55 | .setBottomLineColor(Color.RED) 56 | .setTabMargin(25) 57 | .setTabsFromPagerAdapter(materialVp.getAdapter()) 58 | .setOnMaterialTabSelectedListener(new MaterialTabLayout.MaterialTabSelectedListener() { 59 | @Override 60 | public void onTabSelected(MaterialTabLayout.Tab tab, boolean reSelected) { 61 | materialVp.setCurrentItem(tab.getPosition(), false); 62 | Toast.makeText(MainActivity.this, tab.getPosition() + "--" + reSelected, Toast.LENGTH_SHORT).show(); 63 | } 64 | }); 65 | // .setSelectTab(0); 66 | 67 | } 68 | 69 | public void setTab() { 70 | 71 | final ArrayList mViewList = getViewList(); 72 | final ArrayList mTitleList = getTitleList(); 73 | 74 | // vp.setAdapter(new MyPagerAdapter(mViewList, mTitleList)); 75 | // tableLayout.setupWithViewPager(vp); 76 | // tableLayout.setTabsFromPagerAdapter(vp.getAdapter()); 77 | tableLayout.addTab(tableLayout.newTab().setText("tab0")); 78 | tableLayout.addTab(tableLayout.newTab().setText("tab1")); 79 | tableLayout.addTab(tableLayout.newTab().setText("tab2")); 80 | tableLayout.addTab(tableLayout.newTab().setText("tab3")); 81 | tableLayout.addTab(tableLayout.newTab().setText("tab4")); 82 | tableLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() { 83 | @Override 84 | public void onTabSelected(TabLayout.Tab tab) { 85 | View view = mViewList.get(tab.getPosition()); 86 | if (view.getParent()!=null){ 87 | ((ViewGroup)view.getParent()).removeView(view); 88 | } 89 | content.addView(view); 90 | Toast.makeText(MainActivity.this, tab.getPosition() + "", Toast.LENGTH_SHORT).show(); 91 | } 92 | 93 | @Override 94 | public void onTabUnselected(TabLayout.Tab tab) { 95 | 96 | } 97 | 98 | @Override 99 | public void onTabReselected(TabLayout.Tab tab) { 100 | } 101 | }); 102 | try { 103 | Field mTabStrip = tableLayout.getClass().getDeclaredField("mTabStrip"); 104 | mTabStrip.setAccessible(true); 105 | LinearLayout ltab = (LinearLayout) mTabStrip.get(tableLayout); 106 | int childCount = ltab.getChildCount(); 107 | for (int i = 0; i < childCount; i++) { 108 | View childAt = ltab.getChildAt(i); 109 | LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(0, -1); 110 | params.weight=1; 111 | params.leftMargin = 20; 112 | params.rightMargin = 20; 113 | childAt.setLayoutParams(params); 114 | childAt.invalidate(); 115 | } 116 | } catch (NoSuchFieldException e) { 117 | e.printStackTrace(); 118 | } catch (IllegalAccessException e) { 119 | e.printStackTrace(); 120 | } 121 | tableLayout.getTabAt(0).select(); 122 | } 123 | 124 | @NonNull 125 | private ArrayList getTitleList() { 126 | //添加页卡标题 127 | final ArrayList mTitleList = new ArrayList<>(); 128 | mTitleList.add("No:1"); 129 | mTitleList.add("No:2"); 130 | mTitleList.add("No:3"); 131 | mTitleList.add("No:4"); 132 | mTitleList.add("No:5"); 133 | return mTitleList; 134 | } 135 | 136 | @NonNull 137 | private ArrayList getViewList() { 138 | LayoutInflater mInflater = LayoutInflater.from(this); 139 | View view1 = mInflater.inflate(R.layout.viewpager_item1, null); 140 | View view2 = mInflater.inflate(R.layout.viewpager_item2, null); 141 | View view3 = mInflater.inflate(R.layout.viewpager_item3, null); 142 | View view4 = mInflater.inflate(R.layout.viewpager_item4, null); 143 | View view5 = mInflater.inflate(R.layout.viewpager_item5, null); 144 | 145 | //添加页卡视图 146 | final ArrayList mViewList = new ArrayList<>(); 147 | mViewList.add(view1); 148 | mViewList.add(view2); 149 | mViewList.add(view3); 150 | mViewList.add(view4); 151 | mViewList.add(view5); 152 | return mViewList; 153 | } 154 | 155 | 156 | class MyPagerAdapter extends android.support.v4.view.PagerAdapter { 157 | private ArrayList mViewList; 158 | private ArrayList mTitleList; 159 | 160 | public MyPagerAdapter(ArrayList mViewList, ArrayList mTitleList) { 161 | 162 | this.mViewList = mViewList; 163 | this.mTitleList = mTitleList; 164 | } 165 | 166 | @Override 167 | public Object instantiateItem(ViewGroup container, int position) { 168 | container.addView(mViewList.get(position));//添加页卡 169 | return mViewList.get(position); 170 | } 171 | 172 | @Override 173 | public void destroyItem(ViewGroup container, int position, Object object) { 174 | container.removeView(mViewList.get(position));//删除页卡 175 | } 176 | 177 | @Override 178 | public int getCount() { 179 | return mTitleList.size(); 180 | } 181 | 182 | @Override 183 | public boolean isViewFromObject(View view, Object object) { 184 | return view == object; 185 | } 186 | 187 | @Override 188 | public CharSequence getPageTitle(int position) { 189 | return mTitleList.get(position);//页卡标题 190 | } 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /app/src/main/java/com/mrs/materialtablayout/MaterialTabLayout.java: -------------------------------------------------------------------------------- 1 | package com.mrs.materialtablayout; 2 | 3 | /** 4 | * Created by mrs on 2017/4/21. 5 | */ 6 | 7 | /* 8 | * Copyright (C) 2015 The Android Open Source Project 9 | * 10 | * Licensed under the Apache License, Version 2.0 (the "License"); 11 | * you may not use this file except in compliance with the License. 12 | * You may obtain a copy of the License at 13 | * 14 | * http://www.apache.org/licenses/LICENSE-2.0 15 | * 16 | * Unless required by applicable law or agreed to in writing, software 17 | * distributed under the License is distributed on an "AS IS" BASIS, 18 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 19 | * See the License for the specific language governing permissions and 20 | * limitations under the License. 21 | */ 22 | 23 | 24 | import android.animation.Animator; 25 | import android.animation.AnimatorListenerAdapter; 26 | import android.animation.ValueAnimator; 27 | import android.annotation.TargetApi; 28 | import android.content.Context; 29 | import android.content.res.ColorStateList; 30 | import android.content.res.Resources; 31 | import android.content.res.TypedArray; 32 | import android.graphics.Canvas; 33 | import android.graphics.Color; 34 | import android.graphics.Paint; 35 | import android.graphics.drawable.Drawable; 36 | import android.os.Build; 37 | import android.support.annotation.ColorInt; 38 | import android.support.annotation.DrawableRes; 39 | import android.support.annotation.IntDef; 40 | import android.support.annotation.LayoutRes; 41 | import android.support.annotation.NonNull; 42 | import android.support.annotation.Nullable; 43 | import android.support.annotation.StringRes; 44 | import android.support.v4.content.ContextCompat; 45 | import android.support.v4.view.GravityCompat; 46 | import android.support.v4.view.PagerAdapter; 47 | import android.support.v4.view.ViewCompat; 48 | import android.support.v4.view.ViewPager; 49 | import android.support.v4.view.animation.FastOutSlowInInterpolator; 50 | import android.support.v4.widget.TextViewCompat; 51 | import android.support.v7.app.ActionBar; 52 | import android.text.Layout; 53 | import android.text.TextUtils; 54 | import android.util.AttributeSet; 55 | import android.util.TypedValue; 56 | import android.view.Gravity; 57 | import android.view.LayoutInflater; 58 | import android.view.View; 59 | import android.view.ViewGroup; 60 | import android.view.ViewParent; 61 | import android.view.accessibility.AccessibilityEvent; 62 | import android.view.accessibility.AccessibilityNodeInfo; 63 | import android.widget.HorizontalScrollView; 64 | import android.widget.ImageView; 65 | import android.widget.LinearLayout; 66 | import android.widget.TextView; 67 | import android.widget.Toast; 68 | 69 | import java.lang.annotation.Retention; 70 | import java.lang.annotation.RetentionPolicy; 71 | import java.lang.ref.WeakReference; 72 | import java.util.ArrayList; 73 | import java.util.Iterator; 74 | 75 | /** 76 | * TabLayout provides a horizontal layout to display tabs. 77 | *

78 | *

Population of the tabs to display is 79 | * done through {@link Tab} instances. You create tabs via {@link #newTab()}. From there you can 80 | * change the tab's label or icon via {@link Tab#setText(int)} and {@link Tab#setIcon(int)} 81 | * respectively. To display the tab, you need to add it to the layout via one of the 82 | * {@link #addTab(Tab)} methods. For example: 83 | *

  84 |  * TabLayout tabLayout = ...;
  85 |  * tabLayout.addTab(tabLayout.newTab().setText("Tab 1"));
  86 |  * tabLayout.addTab(tabLayout.newTab().setText("Tab 2"));
  87 |  * tabLayout.addTab(tabLayout.newTab().setText("Tab 3"));
  88 |  * 
89 | * You should set a listener via {@link #setOnTabSelectedListener(OnTabSelectedListener)} to be 90 | * notified when any tab's selection state has been changed. 91 | *

92 | * If you're using a {@link android.support.v4.view.ViewPager} together 93 | * with this layout, you can use {@link #setTabsFromPagerAdapter(PagerAdapter)} which will populate 94 | * the tabs using the given {@link PagerAdapter}'s page titles. You should also use a 95 | * {@link TabLayoutOnPageChangeListener} to forward the scroll and selection changes to this 96 | * layout like so: 97 | *

  98 |  * ViewPager viewPager = ...;
  99 |  * TabLayout tabLayout = ...;
 100 |  * viewPager.addOnPageChangeListener(new TabLayoutOnPageChangeListener(tabLayout));
 101 |  * 
102 | * 103 | * @see Tabs 104 | */ 105 | public class MaterialTabLayout extends HorizontalScrollView { 106 | 107 | private static final int DEFAULT_HEIGHT_WITH_TEXT_ICON = 72; // dps 108 | private static final int DEFAULT_GAP_TEXT_ICON = 8; // dps 109 | private static final int INVALID_WIDTH = -1; 110 | private static final int DEFAULT_HEIGHT = 48; // dps 111 | private static final int TAB_MIN_WIDTH_MARGIN = 56; //dps 112 | private static final int FIXED_WRAP_GUTTER_MIN = 16; //dps 113 | private static final int MOTION_NON_ADJACENT_OFFSET = 24; 114 | 115 | private static final int ANIMATION_DURATION = 300; 116 | 117 | /** 118 | * Scrollable tabs display a subset of tabs at any given moment, and can contain longer tab 119 | * labels and a larger number of tabs. They are best used for browsing contexts in touch 120 | * interfaces when users don’t need to directly compare the tab labels. 121 | * 122 | * @see #setTabMode(int) 123 | * @see #getTabMode() 124 | */ 125 | public static final int MODE_SCROLLABLE = 0; 126 | 127 | /** 128 | * Fixed tabs display all tabs concurrently and are best used with content that benefits from 129 | * quick pivots between tabs. The maximum number of tabs is limited by the view’s width. 130 | * Fixed tabs have equal width, based on the widest tab label. 131 | * 132 | * @see #setTabMode(int) 133 | * @see #getTabMode() 134 | */ 135 | public static final int MODE_FIXED = 1; 136 | 137 | /** 138 | * @hide 139 | */ 140 | @IntDef(value = {MODE_SCROLLABLE, MODE_FIXED}) 141 | @Retention(RetentionPolicy.SOURCE) 142 | public @interface Mode { 143 | } 144 | 145 | /** 146 | * Gravity used to fill the {@link MaterialTabLayout} as much as possible. This option only takes effect 147 | * when used with {@link #MODE_FIXED}. 148 | * 149 | * @see #setTabGravity(int) 150 | * @see #getTabGravity() 151 | */ 152 | public static final int GRAVITY_FILL = 0; 153 | 154 | /** 155 | * Gravity used to lay out the tabs in the center of the {@link MaterialTabLayout}. 156 | * 157 | * @see #setTabGravity(int) 158 | * @see #getTabGravity() 159 | */ 160 | public static final int GRAVITY_CENTER = 1; 161 | 162 | /** 163 | * @hide 164 | */ 165 | @IntDef(flag = true, value = {GRAVITY_FILL, GRAVITY_CENTER}) 166 | @Retention(RetentionPolicy.SOURCE) 167 | public @interface TabGravity { 168 | } 169 | 170 | /** 171 | * Callback interface invoked when a tab's selection state changes. 172 | */ 173 | public interface OnTabSelectedListener { 174 | 175 | /** 176 | * Called when a tab enters the selected state. 177 | * 178 | * @param tab The tab that was selected 179 | */ 180 | public void onTabSelected(Tab tab); 181 | 182 | /** 183 | * Called when a tab exits the selected state. 184 | * 185 | * @param tab The tab that was unselected 186 | */ 187 | public void onTabUnselected(Tab tab); 188 | 189 | /** 190 | * Called when a tab that is already selected is chosen again by the user. Some applications 191 | * may use this action to return to the top level of a category. 192 | * 193 | * @param tab The tab that was reselected. 194 | */ 195 | public void onTabReselected(Tab tab); 196 | } 197 | 198 | private final ArrayList mTabs = new ArrayList<>(); 199 | private Tab mSelectedTab; 200 | 201 | private final SlidingTabStrip mTabStrip; 202 | 203 | private int mTabPaddingStart; 204 | private int mTabPaddingTop; 205 | private int mTabPaddingEnd; 206 | private int mTabPaddingBottom; 207 | 208 | private int mTabTextAppearance; 209 | private ColorStateList mTabTextColors; 210 | private float mTabTextSize; 211 | private float mTabTextMultiLineSize; 212 | 213 | private final int mTabBackgroundResId; 214 | 215 | private int mTabMaxWidth = Integer.MAX_VALUE; 216 | private final int mRequestedTabMinWidth; 217 | private final int mRequestedTabMaxWidth; 218 | private final int mScrollableTabMinWidth; 219 | 220 | private int mContentInsetStart; 221 | 222 | private int mTabGravity; 223 | private int mMode; 224 | 225 | private OnTabSelectedListener mOnTabSelectedListener; 226 | private View.OnClickListener mTabClickListener; 227 | 228 | private ValueAnimator mScrollAnimator; 229 | private ValueAnimator mIndicatorAnimator; 230 | 231 | public MaterialTabLayout(Context context) { 232 | this(context, null); 233 | } 234 | 235 | public MaterialTabLayout(Context context, AttributeSet attrs) { 236 | this(context, attrs, 0); 237 | } 238 | 239 | public MaterialTabLayout(Context context, AttributeSet attrs, int defStyleAttr) { 240 | super(context, attrs, defStyleAttr); 241 | 242 | // Disable the Scroll Bar 243 | setHorizontalScrollBarEnabled(false); 244 | 245 | // Add the TabStrip 246 | mTabStrip = new SlidingTabStrip(context); 247 | addView(mTabStrip, LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT); 248 | 249 | TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TabLayout, 250 | defStyleAttr, R.style.Widget_Design_TabLayout); 251 | 252 | mTabStrip.setSelectedIndicatorHeight( 253 | a.getDimensionPixelSize(R.styleable.TabLayout_tabIndicatorHeight, 0)); 254 | mTabStrip.setSelectedIndicatorColor(a.getColor(R.styleable.TabLayout_tabIndicatorColor, 0)); 255 | 256 | mTabPaddingStart = mTabPaddingTop = mTabPaddingEnd = mTabPaddingBottom = a 257 | .getDimensionPixelSize(R.styleable.TabLayout_tabPadding, 0); 258 | mTabPaddingStart = a.getDimensionPixelSize(R.styleable.TabLayout_tabPaddingStart, 259 | mTabPaddingStart); 260 | mTabPaddingTop = a.getDimensionPixelSize(R.styleable.TabLayout_tabPaddingTop, 261 | mTabPaddingTop); 262 | mTabPaddingEnd = a.getDimensionPixelSize(R.styleable.TabLayout_tabPaddingEnd, 263 | mTabPaddingEnd); 264 | mTabPaddingBottom = a.getDimensionPixelSize(R.styleable.TabLayout_tabPaddingBottom, 265 | mTabPaddingBottom); 266 | 267 | mTabTextAppearance = a.getResourceId(R.styleable.TabLayout_tabTextAppearance, 268 | R.style.TextAppearance_Design_Tab); 269 | 270 | // Text colors/sizes come from the text appearance first 271 | final TypedArray ta = context.obtainStyledAttributes(mTabTextAppearance, 272 | R.styleable.TextAppearance); 273 | try { 274 | mTabTextSize = ta.getDimensionPixelSize(R.styleable.TextAppearance_android_textSize, 0); 275 | mTabTextColors = ta.getColorStateList(R.styleable.TextAppearance_android_textColor); 276 | } finally { 277 | ta.recycle(); 278 | } 279 | 280 | if (a.hasValue(R.styleable.TabLayout_tabTextColor)) { 281 | // If we have an explicit text color set, use it instead 282 | mTabTextColors = a.getColorStateList(R.styleable.TabLayout_tabTextColor); 283 | } 284 | 285 | if (a.hasValue(R.styleable.TabLayout_tabSelectedTextColor)) { 286 | // We have an explicit selected text color set, so we need to make merge it with the 287 | // current colors. This is exposed so that developers can use theme attributes to set 288 | // this (theme attrs in ColorStateLists are Lollipop+) 289 | final int selected = a.getColor(R.styleable.TabLayout_tabSelectedTextColor, 0); 290 | mTabTextColors = createColorStateList(mTabTextColors.getDefaultColor(), selected); 291 | } 292 | 293 | mRequestedTabMinWidth = a.getDimensionPixelSize(R.styleable.TabLayout_tabMinWidth, 294 | INVALID_WIDTH); 295 | mRequestedTabMaxWidth = a.getDimensionPixelSize(R.styleable.TabLayout_tabMaxWidth, 296 | INVALID_WIDTH); 297 | mTabBackgroundResId = a.getResourceId(R.styleable.TabLayout_tabBackground, 0); 298 | mContentInsetStart = a.getDimensionPixelSize(R.styleable.TabLayout_tabContentStart, 0); 299 | mMode = a.getInt(R.styleable.TabLayout_tabMode, MODE_FIXED); 300 | mTabGravity = a.getInt(R.styleable.TabLayout_tabGravity, GRAVITY_FILL); 301 | a.recycle(); 302 | 303 | // TODO add attr for these 304 | final Resources res = getResources(); 305 | mTabTextMultiLineSize = res.getDimensionPixelSize(R.dimen.design_tab_text_size_2line); 306 | mScrollableTabMinWidth = res.getDimensionPixelSize(R.dimen.design_tab_scrollable_min_width); 307 | 308 | // Now apply the tab mode and gravity 309 | applyModeAndGravity(); 310 | } 311 | 312 | /** 313 | * Sets the tab indicator's color for the currently selected tab. 314 | * 315 | * @param color color to use for the indicator 316 | */ 317 | public void setSelectedTabIndicatorColor(@ColorInt int color) { 318 | mTabStrip.setSelectedIndicatorColor(color); 319 | } 320 | 321 | /** 322 | * Sets the tab indicator's height for the currently selected tab. 323 | * 324 | * @param height height to use for the indicator in pixels 325 | */ 326 | public void setSelectedTabIndicatorHeight(int height) { 327 | mTabStrip.setSelectedIndicatorHeight(height); 328 | } 329 | 330 | /** 331 | * Set the scroll position of the tabs. This is useful for when the tabs are being displayed as 332 | * part of a scrolling container such as {@link android.support.v4.view.ViewPager}. 333 | *

334 | * Calling this method does not update the selected tab, it is only used for drawing purposes. 335 | * 336 | * @param position current scroll position 337 | * @param positionOffset Value from [0, 1) indicating the offset from {@code position}. 338 | * @param updateSelectedText Whether to update the text's selected state. 339 | */ 340 | public void setScrollPosition(int position, float positionOffset, boolean updateSelectedText) { 341 | if (mIndicatorAnimator != null && mIndicatorAnimator.isRunning()) { 342 | return; 343 | } 344 | if (position < 0 || position >= mTabStrip.getChildCount()) { 345 | return; 346 | } 347 | 348 | // Set the indicator position and update the scroll to match 349 | mTabStrip.setIndicatorPositionFromTabPosition(position, positionOffset); 350 | scrollTo(calculateScrollXForTab(position, positionOffset), 0); 351 | 352 | // Update the 'selected state' view as we scroll 353 | if (updateSelectedText) { 354 | setSelectedTabView(Math.round(position + positionOffset)); 355 | } 356 | } 357 | 358 | private float getScrollPosition() { 359 | return mTabStrip.getIndicatorPosition(); 360 | } 361 | 362 | /** 363 | * Add a tab to this layout. The tab will be added at the end of the list. 364 | * If this is the first tab to be added it will become the selected tab. 365 | * 366 | * @param tab Tab to add 367 | */ 368 | public void addTab(@NonNull Tab tab) { 369 | addTab(tab, mTabs.isEmpty()); 370 | } 371 | 372 | /** 373 | * Add a tab to this layout. The tab will be inserted at position. 374 | * If this is the first tab to be added it will become the selected tab. 375 | * 376 | * @param tab The tab to add 377 | * @param position The new position of the tab 378 | */ 379 | public void addTab(@NonNull Tab tab, int position) { 380 | addTab(tab, position, mTabs.isEmpty()); 381 | } 382 | 383 | /** 384 | * Add a tab to this layout. The tab will be added at the end of the list. 385 | * 386 | * @param tab Tab to add 387 | * @param setSelected True if the added tab should become the selected tab. 388 | */ 389 | public void addTab(@NonNull Tab tab, boolean setSelected) { 390 | if (tab.mParent != this) { 391 | throw new IllegalArgumentException("Tab belongs to a different TabLayout."); 392 | } 393 | 394 | addTabView(tab, setSelected); 395 | configureTab(tab, mTabs.size()); 396 | if (setSelected) { 397 | tab.select(); 398 | } 399 | } 400 | 401 | /** 402 | * Add a tab to this layout. The tab will be inserted at position. 403 | * 404 | * @param tab The tab to add 405 | * @param position The new position of the tab 406 | * @param setSelected True if the added tab should become the selected tab. 407 | */ 408 | public void addTab(@NonNull Tab tab, int position, boolean setSelected) { 409 | if (tab.mParent != this) { 410 | throw new IllegalArgumentException("Tab belongs to a different TabLayout."); 411 | } 412 | 413 | addTabView(tab, position, setSelected); 414 | configureTab(tab, position); 415 | if (setSelected) { 416 | tab.select(); 417 | } 418 | } 419 | 420 | /** 421 | * Set the {@link android.support.design.widget.TabLayout.OnTabSelectedListener} that will 422 | * handle switching to and from tabs. 423 | * 424 | * @param onTabSelectedListener Listener to handle tab selection events 425 | */ 426 | public void setOnTabSelectedListener(OnTabSelectedListener onTabSelectedListener) { 427 | mOnTabSelectedListener = onTabSelectedListener; 428 | } 429 | 430 | /** 431 | * Create and return a new {@link Tab}. You need to manually add this using 432 | * {@link #addTab(Tab)} or a related method. 433 | * 434 | * @return A new Tab 435 | * @see #addTab(Tab) 436 | */ 437 | @NonNull 438 | public Tab newTab() { 439 | return new Tab(this); 440 | } 441 | 442 | /** 443 | * Returns the number of tabs currently registered with the action bar. 444 | * 445 | * @return Tab count 446 | */ 447 | public int getTabCount() { 448 | return mTabs.size(); 449 | } 450 | 451 | /** 452 | * Returns the tab at the specified index. 453 | */ 454 | @Nullable 455 | public Tab getTabAt(int index) { 456 | return mTabs.get(index); 457 | } 458 | 459 | /** 460 | * Returns the position of the current selected tab. 461 | * 462 | * @return selected tab position, or {@code -1} if there isn't a selected tab. 463 | */ 464 | public int getSelectedTabPosition() { 465 | return mSelectedTab != null ? mSelectedTab.getPosition() : -1; 466 | } 467 | 468 | /** 469 | * Remove a tab from the layout. If the removed tab was selected it will be deselected 470 | * and another tab will be selected if present. 471 | * 472 | * @param tab The tab to remove 473 | */ 474 | public void removeTab(Tab tab) { 475 | if (tab.mParent != this) { 476 | throw new IllegalArgumentException("Tab does not belong to this TabLayout."); 477 | } 478 | 479 | removeTabAt(tab.getPosition()); 480 | } 481 | 482 | /** 483 | * Remove a tab from the layout. If the removed tab was selected it will be deselected 484 | * and another tab will be selected if present. 485 | * 486 | * @param position Position of the tab to remove 487 | */ 488 | public void removeTabAt(int position) { 489 | final int selectedTabPosition = mSelectedTab != null ? mSelectedTab.getPosition() : 0; 490 | removeTabViewAt(position); 491 | 492 | Tab removedTab = mTabs.remove(position); 493 | if (removedTab != null) { 494 | removedTab.setPosition(Tab.INVALID_POSITION); 495 | } 496 | 497 | final int newTabCount = mTabs.size(); 498 | for (int i = position; i < newTabCount; i++) { 499 | mTabs.get(i).setPosition(i); 500 | } 501 | 502 | if (selectedTabPosition == position) { 503 | selectTab(mTabs.isEmpty() ? null : mTabs.get(Math.max(0, position - 1))); 504 | } 505 | } 506 | 507 | /** 508 | * Remove all tabs from the action bar and deselect the current tab. 509 | */ 510 | public void removeAllTabs() { 511 | // Remove all the views 512 | mTabStrip.removeAllViews(); 513 | 514 | for (Iterator i = mTabs.iterator(); i.hasNext(); ) { 515 | Tab tab = i.next(); 516 | tab.setPosition(Tab.INVALID_POSITION); 517 | i.remove(); 518 | } 519 | 520 | mSelectedTab = null; 521 | } 522 | 523 | /** 524 | * Set the behavior mode for the Tabs in this layout. The valid input options are: 525 | *

    526 | *
  • {@link #MODE_FIXED}: Fixed tabs display all tabs concurrently and are best used 527 | * with content that benefits from quick pivots between tabs.
  • 528 | *
  • {@link #MODE_SCROLLABLE}: Scrollable tabs display a subset of tabs at any given moment, 529 | * and can contain longer tab labels and a larger number of tabs. They are best used for 530 | * browsing contexts in touch interfaces when users don’t need to directly compare the tab 531 | * labels. This mode is commonly used with a {@link android.support.v4.view.ViewPager}.
  • 532 | *
533 | * 534 | * @param mode one of {@link #MODE_FIXED} or {@link #MODE_SCROLLABLE}. 535 | */ 536 | public void setTabMode(@Mode int mode) { 537 | if (mode != mMode) { 538 | mMode = mode; 539 | applyModeAndGravity(); 540 | } 541 | } 542 | 543 | /** 544 | * Returns the current mode used by this {@link MaterialTabLayout}. 545 | * 546 | * @see #setTabMode(int) 547 | */ 548 | @Mode 549 | public int getTabMode() { 550 | return mMode; 551 | } 552 | 553 | /** 554 | * Set the gravity to use when laying out the tabs. 555 | * 556 | * @param gravity one of {@link #GRAVITY_CENTER} or {@link #GRAVITY_FILL}. 557 | */ 558 | public void setTabGravity(@TabGravity int gravity) { 559 | if (mTabGravity != gravity) { 560 | mTabGravity = gravity; 561 | applyModeAndGravity(); 562 | } 563 | } 564 | 565 | /** 566 | * The current gravity used for laying out tabs. 567 | * 568 | * @return one of {@link #GRAVITY_CENTER} or {@link #GRAVITY_FILL}. 569 | */ 570 | @TabGravity 571 | public int getTabGravity() { 572 | return mTabGravity; 573 | } 574 | 575 | /** 576 | * Sets the text colors for the different states (normal, selected) used for the tabs. 577 | */ 578 | public void setTabTextColors(@Nullable ColorStateList textColor) { 579 | if (mTabTextColors != textColor) { 580 | mTabTextColors = textColor; 581 | updateAllTabs(); 582 | } 583 | } 584 | 585 | /** 586 | * Gets the text colors for the different states (normal, selected) used for the tabs. 587 | */ 588 | @Nullable 589 | public ColorStateList getTabTextColors() { 590 | return mTabTextColors; 591 | } 592 | 593 | /** 594 | * Sets the text colors for the different states (normal, selected) used for the tabs. 595 | */ 596 | public void setTabTextColors(int normalColor, int selectedColor) { 597 | setTabTextColors(createColorStateList(normalColor, selectedColor)); 598 | } 599 | 600 | /** 601 | * The one-stop shop for setting up this {@link MaterialTabLayout} with a {@link ViewPager}. 602 | *

603 | *

This method will: 604 | *

    605 | *
  • Add a {@link ViewPager.OnPageChangeListener} that will forward events to 606 | * this TabLayout.
  • 607 | *
  • Populate the TabLayout's tabs from the ViewPager's {@link PagerAdapter}.
  • 608 | *
  • Set our {@link MaterialTabLayout.OnTabSelectedListener} which will forward 609 | * selected events to the ViewPager
  • 610 | *
611 | *

612 | * 613 | * @see #setTabsFromPagerAdapter(PagerAdapter) 614 | * @see TabLayoutOnPageChangeListener 615 | * @see ViewPagerOnTabSelectedListener 616 | */ 617 | public MaterialTabLayout setupWithViewPager(@NonNull ViewPager viewPager) { 618 | final PagerAdapter adapter = viewPager.getAdapter(); 619 | if (adapter == null) { 620 | throw new IllegalArgumentException("ViewPager does not have a PagerAdapter set"); 621 | } 622 | 623 | // First we'll add Tabs, using the adapter's page titles 624 | setTabsFromPagerAdapter(adapter); 625 | 626 | // Now we'll add our page change listener to the ViewPager 627 | viewPager.addOnPageChangeListener(new TabLayoutOnPageChangeListener(this)); 628 | 629 | // Now we'll add a tab selected listener to set ViewPager's current item 630 | setOnTabSelectedListener(new ViewPagerOnTabSelectedListener(viewPager)); 631 | 632 | // Make sure we reflect the currently set ViewPager item 633 | if (adapter.getCount() > 0) { 634 | final int curItem = viewPager.getCurrentItem(); 635 | if (getSelectedTabPosition() != curItem) { 636 | selectTab(getTabAt(curItem)); 637 | } 638 | } 639 | return this; 640 | } 641 | 642 | /** 643 | * Populate our tab content from the given {@link PagerAdapter}. 644 | *

645 | * Any existing tabs will be removed first. Each tab will have it's text set to the value 646 | * returned from {@link PagerAdapter#getPageTitle(int)} 647 | *

648 | * 649 | * @param adapter the adapter to populate from 650 | */ 651 | public MaterialTabLayout setTabsFromPagerAdapter(@NonNull PagerAdapter adapter) { 652 | removeAllTabs(); 653 | for (int i = 0, count = adapter.getCount(); i < count; i++) { 654 | addTab(newTab().setText(adapter.getPageTitle(i))); 655 | } 656 | return this; 657 | } 658 | 659 | private void updateAllTabs() { 660 | for (int i = 0, z = mTabStrip.getChildCount(); i < z; i++) { 661 | updateTab(i); 662 | } 663 | } 664 | 665 | private TabView createTabView(Tab tab) { 666 | final TabView tabView = new TabView(getContext(), tab); 667 | tabView.setFocusable(true); 668 | tabView.setMinimumWidth(getTabMinWidth()); 669 | 670 | if (mTabClickListener == null) { 671 | mTabClickListener = new View.OnClickListener() { 672 | @Override 673 | public void onClick(View view) { 674 | TabView tabView = (TabView) view; 675 | tabView.getTab().select(); 676 | } 677 | }; 678 | } 679 | tabView.setOnClickListener(mTabClickListener); 680 | return tabView; 681 | } 682 | 683 | private void configureTab(Tab tab, int position) { 684 | tab.setPosition(position); 685 | mTabs.add(position, tab); 686 | 687 | final int count = mTabs.size(); 688 | for (int i = position + 1; i < count; i++) { 689 | mTabs.get(i).setPosition(i); 690 | } 691 | } 692 | 693 | private void updateTab(int position) { 694 | final TabView view = getTabView(position); 695 | if (view != null) { 696 | view.update(); 697 | } 698 | } 699 | 700 | private TabView getTabView(int position) { 701 | return (TabView) mTabStrip.getChildAt(position); 702 | } 703 | 704 | private void addTabView(Tab tab, boolean setSelected) { 705 | final TabView tabView = createTabView(tab); 706 | mTabStrip.addView(tabView, createLayoutParamsForTabs()); 707 | if (setSelected) { 708 | tabView.setSelected(true); 709 | } 710 | } 711 | 712 | private void addTabView(Tab tab, int position, boolean setSelected) { 713 | final TabView tabView = createTabView(tab); 714 | mTabStrip.addView(tabView, position, createLayoutParamsForTabs()); 715 | if (setSelected) { 716 | tabView.setSelected(true); 717 | } 718 | } 719 | 720 | private LinearLayout.LayoutParams createLayoutParamsForTabs() { 721 | final LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams( 722 | LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT); 723 | updateTabViewLayoutParams(lp); 724 | return lp; 725 | } 726 | 727 | private void updateTabViewLayoutParams(LinearLayout.LayoutParams lp) { 728 | if (mMode == MODE_FIXED && mTabGravity == GRAVITY_FILL) { 729 | lp.width = 0; 730 | lp.weight = 1; 731 | } else { 732 | lp.width = LinearLayout.LayoutParams.WRAP_CONTENT; 733 | lp.weight = 0; 734 | } 735 | } 736 | 737 | private int dpToPx(int dps) { 738 | return Math.round(getResources().getDisplayMetrics().density * dps); 739 | } 740 | 741 | @Override 742 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 743 | // If we have a MeasureSpec which allows us to decide our height, try and use the default 744 | // height 745 | final int idealHeight = dpToPx(getDefaultHeight()) + getPaddingTop() + getPaddingBottom(); 746 | switch (MeasureSpec.getMode(heightMeasureSpec)) { 747 | case MeasureSpec.AT_MOST: 748 | heightMeasureSpec = MeasureSpec.makeMeasureSpec( 749 | Math.min(idealHeight, MeasureSpec.getSize(heightMeasureSpec)), 750 | MeasureSpec.EXACTLY); 751 | break; 752 | case MeasureSpec.UNSPECIFIED: 753 | heightMeasureSpec = MeasureSpec.makeMeasureSpec(idealHeight, MeasureSpec.EXACTLY); 754 | break; 755 | } 756 | 757 | final int specWidth = MeasureSpec.getSize(widthMeasureSpec); 758 | if (MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.UNSPECIFIED) { 759 | // If we don't have an unspecified width spec, use the given size to calculate 760 | // the max tab width 761 | mTabMaxWidth = mRequestedTabMaxWidth > 0 762 | ? mRequestedTabMaxWidth 763 | : specWidth - dpToPx(TAB_MIN_WIDTH_MARGIN); 764 | } 765 | 766 | // Now super measure itself using the (possibly) modified height spec 767 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 768 | 769 | if (getChildCount() == 1) { 770 | // If we're in fixed mode then we need to make the tab strip is the same width as us 771 | // so we don't scroll 772 | final View child = getChildAt(0); 773 | boolean remeasure = false; 774 | 775 | switch (mMode) { 776 | case MODE_SCROLLABLE: 777 | // We only need to resize the child if it's smaller than us. This is similar 778 | // to fillViewport 779 | remeasure = child.getMeasuredWidth() < getMeasuredWidth(); 780 | break; 781 | case MODE_FIXED: 782 | // Resize the child so that it doesn't scroll 783 | remeasure = child.getMeasuredWidth() != getMeasuredWidth(); 784 | break; 785 | } 786 | 787 | if (remeasure) { 788 | // Re-measure the child with a widthSpec set to be exactly our measure width 789 | int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, getPaddingTop() 790 | + getPaddingBottom(), child.getLayoutParams().height); 791 | int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec( 792 | getMeasuredWidth(), MeasureSpec.EXACTLY); 793 | child.measure(childWidthMeasureSpec, childHeightMeasureSpec); 794 | } 795 | } 796 | } 797 | 798 | private void removeTabViewAt(int position) { 799 | mTabStrip.removeViewAt(position); 800 | requestLayout(); 801 | } 802 | 803 | private void animateToTab(int newPosition) { 804 | if (newPosition == Tab.INVALID_POSITION) { 805 | return; 806 | } 807 | 808 | if (getWindowToken() == null || !ViewCompat.isLaidOut(this) 809 | || mTabStrip.childrenNeedLayout()) { 810 | // If we don't have a window token, or we haven't been laid out yet just draw the new 811 | // position now 812 | setScrollPosition(newPosition, 0f, true); 813 | return; 814 | } 815 | 816 | final int startScrollX = getScrollX(); 817 | final int targetScrollX = calculateScrollXForTab(newPosition, 0); 818 | 819 | if (startScrollX != targetScrollX) { 820 | if (mScrollAnimator == null) { 821 | mScrollAnimator = new ValueAnimator(); 822 | mScrollAnimator.setInterpolator(new FastOutSlowInInterpolator()); 823 | mScrollAnimator.setDuration(ANIMATION_DURATION); 824 | mScrollAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 825 | @Override 826 | public void onAnimationUpdate(ValueAnimator animator) { 827 | scrollTo((int) animator.getAnimatedValue(), 0); 828 | } 829 | }); 830 | } 831 | 832 | mScrollAnimator.setIntValues(startScrollX, targetScrollX); 833 | mScrollAnimator.start(); 834 | } 835 | 836 | // Now animate the indicator 837 | mTabStrip.animateIndicatorToPosition(newPosition, ANIMATION_DURATION); 838 | } 839 | 840 | private void setSelectedTabView(int position) { 841 | final int tabCount = mTabStrip.getChildCount(); 842 | if (position < tabCount && !mTabStrip.getChildAt(position).isSelected()) { 843 | for (int i = 0; i < tabCount; i++) { 844 | final View child = mTabStrip.getChildAt(i); 845 | child.setSelected(i == position); 846 | } 847 | } 848 | } 849 | 850 | void selectTab(Tab tab) { 851 | selectTab(tab, true); 852 | } 853 | 854 | void selectTab(Tab tab, boolean updateIndicator) { 855 | if (mSelectedTab == tab) { 856 | if (mSelectedTab != null) { 857 | if (mOnTabSelectedListener != null) { 858 | mOnTabSelectedListener.onTabReselected(mSelectedTab); 859 | } 860 | animateToTab(tab.getPosition()); 861 | } 862 | } else { 863 | if (updateIndicator) { 864 | final int newPosition = tab != null ? tab.getPosition() : Tab.INVALID_POSITION; 865 | if (newPosition != Tab.INVALID_POSITION) { 866 | setSelectedTabView(newPosition); 867 | } 868 | if ((mSelectedTab == null || mSelectedTab.getPosition() == Tab.INVALID_POSITION) 869 | && newPosition != Tab.INVALID_POSITION) { 870 | // If we don't currently have a tab, just draw the indicator 871 | setScrollPosition(newPosition, 0f, true); 872 | } else { 873 | animateToTab(newPosition); 874 | } 875 | } 876 | if (mSelectedTab != null && mOnTabSelectedListener != null) { 877 | mOnTabSelectedListener.onTabUnselected(mSelectedTab); 878 | } 879 | mSelectedTab = tab; 880 | if (mSelectedTab != null && mOnTabSelectedListener != null) { 881 | mOnTabSelectedListener.onTabSelected(mSelectedTab); 882 | } 883 | } 884 | } 885 | 886 | private int calculateScrollXForTab(int position, float positionOffset) { 887 | if (mMode == MODE_SCROLLABLE) { 888 | final View selectedChild = mTabStrip.getChildAt(position); 889 | final View nextChild = position + 1 < mTabStrip.getChildCount() 890 | ? mTabStrip.getChildAt(position + 1) 891 | : null; 892 | final int selectedWidth = selectedChild != null ? selectedChild.getWidth() : 0; 893 | final int nextWidth = nextChild != null ? nextChild.getWidth() : 0; 894 | 895 | return selectedChild.getLeft() 896 | + ((int) ((selectedWidth + nextWidth) * positionOffset * 0.5f)) 897 | + (selectedChild.getWidth() / 2) 898 | - (getWidth() / 2); 899 | } 900 | return 0; 901 | } 902 | 903 | private void applyModeAndGravity() { 904 | int paddingStart = 0; 905 | if (mMode == MODE_SCROLLABLE) { 906 | // If we're scrollable, or fixed at start, inset using padding 907 | paddingStart = Math.max(0, mContentInsetStart - mTabPaddingStart); 908 | } 909 | ViewCompat.setPaddingRelative(mTabStrip, paddingStart, 0, 0, 0); 910 | 911 | switch (mMode) { 912 | case MODE_FIXED: 913 | mTabStrip.setGravity(Gravity.CENTER_HORIZONTAL); 914 | break; 915 | case MODE_SCROLLABLE: 916 | mTabStrip.setGravity(GravityCompat.START); 917 | break; 918 | } 919 | 920 | updateTabViews(true); 921 | } 922 | 923 | private void updateTabViews(final boolean requestLayout) { 924 | for (int i = 0; i < mTabStrip.getChildCount(); i++) { 925 | View child = mTabStrip.getChildAt(i); 926 | child.setMinimumWidth(getTabMinWidth()); 927 | updateTabViewLayoutParams((LinearLayout.LayoutParams) child.getLayoutParams()); 928 | if (requestLayout) { 929 | child.requestLayout(); 930 | } 931 | } 932 | } 933 | 934 | /** 935 | * A tab in this layout. Instances can be created via {@link #newTab()}. 936 | */ 937 | public static final class Tab { 938 | 939 | /** 940 | * An invalid position for a tab. 941 | * 942 | * @see #getPosition() 943 | */ 944 | public static final int INVALID_POSITION = -1; 945 | 946 | private Object mTag; 947 | private Drawable mIcon; 948 | private CharSequence mText; 949 | private CharSequence mContentDesc; 950 | private int mPosition = INVALID_POSITION; 951 | private View mCustomView; 952 | 953 | private final MaterialTabLayout mParent; 954 | 955 | Tab(MaterialTabLayout parent) { 956 | mParent = parent; 957 | } 958 | 959 | /** 960 | * @return This Tab's tag object. 961 | */ 962 | @Nullable 963 | public Object getTag() { 964 | return mTag; 965 | } 966 | 967 | /** 968 | * Give this Tab an arbitrary object to hold for later use. 969 | * 970 | * @param tag Object to store 971 | * @return The current instance for call chaining 972 | */ 973 | @NonNull 974 | public Tab setTag(@Nullable Object tag) { 975 | mTag = tag; 976 | return this; 977 | } 978 | 979 | 980 | /** 981 | * Returns the custom view used for this tab. 982 | * 983 | * @see #setCustomView(View) 984 | * @see #setCustomView(int) 985 | */ 986 | @Nullable 987 | public View getCustomView() { 988 | return mCustomView; 989 | } 990 | 991 | /** 992 | * Set a custom view to be used for this tab. 993 | *

994 | * If the provided view contains a {@link TextView} with an ID of 995 | * {@link android.R.id#text1} then that will be updated with the value given 996 | * to {@link #setText(CharSequence)}. Similarly, if this layout contains an 997 | * {@link ImageView} with ID {@link android.R.id#icon} then it will be updated with 998 | * the value given to {@link #setIcon(Drawable)}. 999 | *

1000 | * 1001 | * @param view Custom view to be used as a tab. 1002 | * @return The current instance for call chaining 1003 | */ 1004 | @NonNull 1005 | public Tab setCustomView(@Nullable View view) { 1006 | mCustomView = view; 1007 | if (mPosition >= 0) { 1008 | mParent.updateTab(mPosition); 1009 | } 1010 | return this; 1011 | } 1012 | 1013 | /** 1014 | * Set a custom view to be used for this tab. 1015 | *

1016 | * If the inflated layout contains a {@link TextView} with an ID of 1017 | * {@link android.R.id#text1} then that will be updated with the value given 1018 | * to {@link #setText(CharSequence)}. Similarly, if this layout contains an 1019 | * {@link ImageView} with ID {@link android.R.id#icon} then it will be updated with 1020 | * the value given to {@link #setIcon(Drawable)}. 1021 | *

1022 | * 1023 | * @param resId A layout resource to inflate and use as a custom tab view 1024 | * @return The current instance for call chaining 1025 | */ 1026 | @NonNull 1027 | public Tab setCustomView(@LayoutRes int resId) { 1028 | final TabView tabView = mParent.getTabView(mPosition); 1029 | final LayoutInflater inflater = LayoutInflater.from(tabView.getContext()); 1030 | return setCustomView(inflater.inflate(resId, tabView, false)); 1031 | } 1032 | 1033 | /** 1034 | * Return the icon associated with this tab. 1035 | * 1036 | * @return The tab's icon 1037 | */ 1038 | @Nullable 1039 | public Drawable getIcon() { 1040 | return mIcon; 1041 | } 1042 | 1043 | /** 1044 | * Return the current position of this tab in the action bar. 1045 | * 1046 | * @return Current position, or {@link #INVALID_POSITION} if this tab is not currently in 1047 | * the action bar. 1048 | */ 1049 | public int getPosition() { 1050 | return mPosition; 1051 | } 1052 | 1053 | void setPosition(int position) { 1054 | mPosition = position; 1055 | } 1056 | 1057 | /** 1058 | * Return the text of this tab. 1059 | * 1060 | * @return The tab's text 1061 | */ 1062 | @Nullable 1063 | public CharSequence getText() { 1064 | return mText; 1065 | } 1066 | 1067 | /** 1068 | * Set the icon displayed on this tab. 1069 | * 1070 | * @param icon The drawable to use as an icon 1071 | * @return The current instance for call chaining 1072 | */ 1073 | @NonNull 1074 | public Tab setIcon(@Nullable Drawable icon) { 1075 | mIcon = icon; 1076 | if (mPosition >= 0) { 1077 | mParent.updateTab(mPosition); 1078 | } 1079 | return this; 1080 | } 1081 | 1082 | /** 1083 | * Set the icon displayed on this tab. 1084 | * 1085 | * @param resId A resource ID referring to the icon that should be displayed 1086 | * @return The current instance for call chaining 1087 | */ 1088 | @NonNull 1089 | public Tab setIcon(@DrawableRes int resId) { 1090 | return setIcon(ContextCompat.getDrawable(mParent.getContext(), resId)); 1091 | } 1092 | 1093 | /** 1094 | * Set the text displayed on this tab. Text may be truncated if there is not room to display 1095 | * the entire string. 1096 | * 1097 | * @param text The text to display 1098 | * @return The current instance for call chaining 1099 | */ 1100 | @NonNull 1101 | public Tab setText(@Nullable CharSequence text) { 1102 | mText = text; 1103 | if (mPosition >= 0) { 1104 | mParent.updateTab(mPosition); 1105 | } 1106 | return this; 1107 | } 1108 | 1109 | /** 1110 | * Set the text displayed on this tab. Text may be truncated if there is not room to display 1111 | * the entire string. 1112 | * 1113 | * @param resId A resource ID referring to the text that should be displayed 1114 | * @return The current instance for call chaining 1115 | */ 1116 | @NonNull 1117 | public Tab setText(@StringRes int resId) { 1118 | return setText(mParent.getResources().getText(resId)); 1119 | } 1120 | 1121 | /** 1122 | * Select this tab. Only valid if the tab has been added to the action bar. 1123 | */ 1124 | public void select() { 1125 | mParent.selectTab(this); 1126 | } 1127 | 1128 | /** 1129 | * Returns true if this tab is currently selected. 1130 | */ 1131 | public boolean isSelected() { 1132 | return mParent.getSelectedTabPosition() == mPosition; 1133 | } 1134 | 1135 | /** 1136 | * Set a description of this tab's content for use in accessibility support. If no content 1137 | * description is provided the title will be used. 1138 | * 1139 | * @param resId A resource ID referring to the description text 1140 | * @return The current instance for call chaining 1141 | * @see #setContentDescription(CharSequence) 1142 | * @see #getContentDescription() 1143 | */ 1144 | @NonNull 1145 | public Tab setContentDescription(@StringRes int resId) { 1146 | return setContentDescription(mParent.getResources().getText(resId)); 1147 | } 1148 | 1149 | /** 1150 | * Set a description of this tab's content for use in accessibility support. If no content 1151 | * description is provided the title will be used. 1152 | * 1153 | * @param contentDesc Description of this tab's content 1154 | * @return The current instance for call chaining 1155 | * @see #setContentDescription(int) 1156 | * @see #getContentDescription() 1157 | */ 1158 | @NonNull 1159 | public Tab setContentDescription(@Nullable CharSequence contentDesc) { 1160 | mContentDesc = contentDesc; 1161 | if (mPosition >= 0) { 1162 | mParent.updateTab(mPosition); 1163 | } 1164 | return this; 1165 | } 1166 | 1167 | /** 1168 | * Gets a brief description of this tab's content for use in accessibility support. 1169 | * 1170 | * @return Description of this tab's content 1171 | * @see #setContentDescription(CharSequence) 1172 | * @see #setContentDescription(int) 1173 | */ 1174 | @Nullable 1175 | public CharSequence getContentDescription() { 1176 | return mContentDesc; 1177 | } 1178 | } 1179 | 1180 | class TabView extends LinearLayout implements OnLongClickListener { 1181 | private final Tab mTab; 1182 | private TextView mTextView; 1183 | private ImageView mIconView; 1184 | 1185 | private View mCustomView; 1186 | private TextView mCustomTextView; 1187 | private ImageView mCustomIconView; 1188 | 1189 | private int mDefaultMaxLines = 2; 1190 | 1191 | public TabView(Context context, Tab tab) { 1192 | super(context); 1193 | mTab = tab; 1194 | if (mTabBackgroundResId != 0) { 1195 | setBackgroundDrawable(ContextCompat.getDrawable(context, mTabBackgroundResId)); 1196 | } 1197 | ViewCompat.setPaddingRelative(this, mTabPaddingStart, mTabPaddingTop, 1198 | mTabPaddingEnd, mTabPaddingBottom); 1199 | setGravity(Gravity.CENTER); 1200 | setOrientation(VERTICAL); 1201 | update(); 1202 | } 1203 | 1204 | @Override 1205 | public void setSelected(boolean selected) { 1206 | final boolean changed = (isSelected() != selected); 1207 | super.setSelected(selected); 1208 | if (changed && selected) { 1209 | sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); 1210 | 1211 | if (mTextView != null) { 1212 | mTextView.setSelected(selected); 1213 | } 1214 | if (mIconView != null) { 1215 | mIconView.setSelected(selected); 1216 | } 1217 | } 1218 | } 1219 | 1220 | @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) 1221 | @Override 1222 | public void onInitializeAccessibilityEvent(AccessibilityEvent event) { 1223 | super.onInitializeAccessibilityEvent(event); 1224 | // This view masquerades as an action bar tab. 1225 | event.setClassName(ActionBar.Tab.class.getName()); 1226 | } 1227 | 1228 | @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) 1229 | @Override 1230 | public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 1231 | super.onInitializeAccessibilityNodeInfo(info); 1232 | // This view masquerades as an action bar tab. 1233 | info.setClassName(ActionBar.Tab.class.getName()); 1234 | } 1235 | 1236 | @Override 1237 | public void onMeasure(final int origWidthMeasureSpec, final int origHeightMeasureSpec) { 1238 | final int specWidthSize = MeasureSpec.getSize(origWidthMeasureSpec); 1239 | final int specWidthMode = MeasureSpec.getMode(origWidthMeasureSpec); 1240 | final int maxWidth = getTabMaxWidth(); 1241 | 1242 | final int widthMeasureSpec; 1243 | final int heightMeasureSpec = origHeightMeasureSpec; 1244 | 1245 | if (maxWidth > 0 && (specWidthMode == MeasureSpec.UNSPECIFIED 1246 | || specWidthSize > maxWidth)) { 1247 | // If we have a max width and a given spec which is either unspecified or 1248 | // larger than the max width, update the width spec using the same mode 1249 | widthMeasureSpec = MeasureSpec.makeMeasureSpec(mTabMaxWidth, specWidthMode); 1250 | } else { 1251 | // Else, use the original width spec 1252 | widthMeasureSpec = origWidthMeasureSpec; 1253 | } 1254 | 1255 | // Now lets measure 1256 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 1257 | 1258 | // We need to switch the text size based on whether the text is spanning 2 lines or not 1259 | if (mTextView != null) { 1260 | final Resources res = getResources(); 1261 | float textSize = mTabTextSize; 1262 | int maxLines = mDefaultMaxLines; 1263 | 1264 | if (mIconView != null && mIconView.getVisibility() == VISIBLE) { 1265 | // If the icon view is being displayed, we limit the text to 1 line 1266 | maxLines = 1; 1267 | } else if (mTextView != null && mTextView.getLineCount() > 1) { 1268 | // Otherwise when we have text which wraps we reduce the text size 1269 | textSize = mTabTextMultiLineSize; 1270 | } 1271 | 1272 | final float curTextSize = mTextView.getTextSize(); 1273 | final int curLineCount = mTextView.getLineCount(); 1274 | final int curMaxLines = TextViewCompat.getMaxLines(mTextView); 1275 | 1276 | if (textSize != curTextSize || (curMaxLines >= 0 && maxLines != curMaxLines)) { 1277 | // We've got a new text size and/or max lines... 1278 | boolean updateTextView = true; 1279 | 1280 | if (mMode == MODE_FIXED && textSize > curTextSize && curLineCount == 1) { 1281 | // If we're in fixed mode, going up in text size and currently have 1 line 1282 | // then it's very easy to get into an infinite recursion. 1283 | // To combat that we check to see if the change in text size 1284 | // will cause a line count change. If so, abort the size change. 1285 | final Layout layout = mTextView.getLayout(); 1286 | if (layout == null 1287 | || approximateLineWidth(layout, 0, textSize) > layout.getWidth()) { 1288 | updateTextView = false; 1289 | } 1290 | } 1291 | 1292 | if (updateTextView) { 1293 | mTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize); 1294 | mTextView.setMaxLines(maxLines); 1295 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 1296 | } 1297 | } 1298 | } 1299 | } 1300 | 1301 | final void update() { 1302 | final Tab tab = mTab; 1303 | final View custom = tab.getCustomView(); 1304 | if (custom != null) { 1305 | final ViewParent customParent = custom.getParent(); 1306 | if (customParent != this) { 1307 | if (customParent != null) { 1308 | ((ViewGroup) customParent).removeView(custom); 1309 | } 1310 | addView(custom); 1311 | } 1312 | mCustomView = custom; 1313 | if (mTextView != null) { 1314 | mTextView.setVisibility(GONE); 1315 | } 1316 | if (mIconView != null) { 1317 | mIconView.setVisibility(GONE); 1318 | mIconView.setImageDrawable(null); 1319 | } 1320 | 1321 | mCustomTextView = (TextView) custom.findViewById(android.R.id.text1); 1322 | if (mCustomTextView != null) { 1323 | mDefaultMaxLines = TextViewCompat.getMaxLines(mCustomTextView); 1324 | } 1325 | mCustomIconView = (ImageView) custom.findViewById(android.R.id.icon); 1326 | } else { 1327 | // We do not have a custom view. Remove one if it already exists 1328 | if (mCustomView != null) { 1329 | removeView(mCustomView); 1330 | mCustomView = null; 1331 | } 1332 | mCustomTextView = null; 1333 | mCustomIconView = null; 1334 | } 1335 | 1336 | if (mCustomView == null) { 1337 | // If there isn't a custom view, we'll us our own in-built layouts 1338 | if (mIconView == null) { 1339 | ImageView iconView = (ImageView) LayoutInflater.from(getContext()) 1340 | .inflate(R.layout.design_layout_tab_icon, this, false); 1341 | addView(iconView, 0); 1342 | mIconView = iconView; 1343 | } 1344 | if (mTextView == null) { 1345 | TextView textView = (TextView) LayoutInflater.from(getContext()) 1346 | .inflate(R.layout.design_layout_tab_text, this, false); 1347 | addView(textView); 1348 | mTextView = textView; 1349 | mDefaultMaxLines = TextViewCompat.getMaxLines(mTextView); 1350 | } 1351 | mTextView.setTextAppearance(getContext(), mTabTextAppearance); 1352 | if (mTabTextColors != null) { 1353 | mTextView.setTextColor(mTabTextColors); 1354 | } 1355 | updateTextAndIcon(tab, mTextView, mIconView); 1356 | } else { 1357 | // Else, we'll see if there is a TextView or ImageView present and update them 1358 | if (mCustomTextView != null || mCustomIconView != null) { 1359 | updateTextAndIcon(tab, mCustomTextView, mCustomIconView); 1360 | } 1361 | } 1362 | } 1363 | 1364 | private void updateTextAndIcon(Tab tab, TextView textView, ImageView iconView) { 1365 | final Drawable icon = tab.getIcon(); 1366 | final CharSequence text = tab.getText(); 1367 | 1368 | if (iconView != null) { 1369 | if (icon != null) { 1370 | iconView.setImageDrawable(icon); 1371 | iconView.setVisibility(VISIBLE); 1372 | setVisibility(VISIBLE); 1373 | } else { 1374 | iconView.setVisibility(GONE); 1375 | iconView.setImageDrawable(null); 1376 | } 1377 | iconView.setContentDescription(tab.getContentDescription()); 1378 | } 1379 | 1380 | final boolean hasText = !TextUtils.isEmpty(text); 1381 | if (textView != null) { 1382 | if (hasText) { 1383 | textView.setText(text); 1384 | textView.setContentDescription(tab.getContentDescription()); 1385 | textView.setVisibility(VISIBLE); 1386 | setVisibility(VISIBLE); 1387 | } else { 1388 | textView.setVisibility(GONE); 1389 | textView.setText(null); 1390 | } 1391 | } 1392 | 1393 | if (iconView != null) { 1394 | MarginLayoutParams lp = ((MarginLayoutParams) iconView.getLayoutParams()); 1395 | int bottomMargin = 0; 1396 | if (hasText && iconView.getVisibility() == VISIBLE) { 1397 | // If we're showing both text and icon, add some margin bottom to the icon 1398 | bottomMargin = dpToPx(DEFAULT_GAP_TEXT_ICON); 1399 | } 1400 | if (bottomMargin != lp.bottomMargin) { 1401 | lp.bottomMargin = bottomMargin; 1402 | iconView.requestLayout(); 1403 | } 1404 | } 1405 | 1406 | if (!hasText && !TextUtils.isEmpty(tab.getContentDescription())) { 1407 | setOnLongClickListener(this); 1408 | } else { 1409 | setOnLongClickListener(null); 1410 | setLongClickable(false); 1411 | } 1412 | } 1413 | 1414 | @Override 1415 | public boolean onLongClick(View v) { 1416 | final int[] screenPos = new int[2]; 1417 | getLocationOnScreen(screenPos); 1418 | 1419 | final Context context = getContext(); 1420 | final int width = getWidth(); 1421 | final int height = getHeight(); 1422 | final int screenWidth = context.getResources().getDisplayMetrics().widthPixels; 1423 | 1424 | Toast cheatSheet = Toast.makeText(context, mTab.getContentDescription(), 1425 | Toast.LENGTH_SHORT); 1426 | // Show under the tab 1427 | cheatSheet.setGravity(Gravity.TOP | Gravity.CENTER_HORIZONTAL, 1428 | (screenPos[0] + width / 2) - screenWidth / 2, height); 1429 | 1430 | cheatSheet.show(); 1431 | return true; 1432 | } 1433 | 1434 | public Tab getTab() { 1435 | return mTab; 1436 | } 1437 | 1438 | /** 1439 | * Approximates a given lines width with the new provided text size. 1440 | */ 1441 | private float approximateLineWidth(Layout layout, int line, float textSize) { 1442 | return layout.getLineWidth(line) * (textSize / layout.getPaint().getTextSize()); 1443 | } 1444 | } 1445 | 1446 | private class SlidingTabStrip extends LinearLayout { 1447 | private int mSelectedIndicatorHeight; 1448 | private final Paint mSelectedIndicatorPaint; 1449 | 1450 | private int mSelectedPosition = -1; 1451 | private float mSelectionOffset; 1452 | 1453 | private int mIndicatorLeft = -1; 1454 | private int mIndicatorRight = -1; 1455 | 1456 | private ValueAnimator mCurrentAnimator; 1457 | 1458 | SlidingTabStrip(Context context) { 1459 | super(context); 1460 | setWillNotDraw(false); 1461 | mSelectedIndicatorPaint = new Paint(); 1462 | } 1463 | 1464 | void setSelectedIndicatorColor(int color) { 1465 | if (mSelectedIndicatorPaint.getColor() != color) { 1466 | mSelectedIndicatorPaint.setColor(color); 1467 | ViewCompat.postInvalidateOnAnimation(this); 1468 | } 1469 | } 1470 | 1471 | void setSelectedIndicatorHeight(int height) { 1472 | if (mSelectedIndicatorHeight != height) { 1473 | mSelectedIndicatorHeight = height; 1474 | ViewCompat.postInvalidateOnAnimation(this); 1475 | } 1476 | } 1477 | 1478 | boolean childrenNeedLayout() { 1479 | for (int i = 0, z = getChildCount(); i < z; i++) { 1480 | final View child = getChildAt(i); 1481 | if (child.getWidth() <= 0) { 1482 | return true; 1483 | } 1484 | } 1485 | return false; 1486 | } 1487 | 1488 | void setIndicatorPositionFromTabPosition(int position, float positionOffset) { 1489 | mSelectedPosition = position; 1490 | mSelectionOffset = positionOffset; 1491 | updateIndicatorPosition(); 1492 | } 1493 | 1494 | float getIndicatorPosition() { 1495 | return mSelectedPosition + mSelectionOffset; 1496 | } 1497 | 1498 | @Override 1499 | protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) { 1500 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 1501 | 1502 | if (MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY) { 1503 | // HorizontalScrollView will first measure use with UNSPECIFIED, and then with 1504 | // EXACTLY. Ignore the first call since anything we do will be overwritten anyway 1505 | return; 1506 | } 1507 | 1508 | if (mMode == MODE_FIXED && mTabGravity == GRAVITY_CENTER) { 1509 | final int count = getChildCount(); 1510 | 1511 | // First we'll find the widest tab 1512 | int largestTabWidth = 0; 1513 | for (int i = 0, z = count; i < z; i++) { 1514 | View child = getChildAt(i); 1515 | if (child.getVisibility() == VISIBLE) { 1516 | largestTabWidth = Math.max(largestTabWidth, child.getMeasuredWidth()); 1517 | } 1518 | } 1519 | 1520 | if (largestTabWidth <= 0) { 1521 | // If we don't have a largest child yet, skip until the next measure pass 1522 | return; 1523 | } 1524 | 1525 | final int gutter = dpToPx(FIXED_WRAP_GUTTER_MIN); 1526 | boolean remeasure = false; 1527 | 1528 | if (largestTabWidth * count <= getMeasuredWidth() - gutter * 2) { 1529 | // If the tabs fit within our width minus gutters, we will set all tabs to have 1530 | // the same width 1531 | for (int i = 0; i < count; i++) { 1532 | final LinearLayout.LayoutParams lp = 1533 | (LayoutParams) getChildAt(i).getLayoutParams(); 1534 | if (lp.width != largestTabWidth || lp.weight != 0) { 1535 | lp.width = largestTabWidth; 1536 | lp.weight = 0; 1537 | remeasure = true; 1538 | } 1539 | } 1540 | } else { 1541 | // If the tabs will wrap to be larger than the width minus gutters, we need 1542 | // to switch to GRAVITY_FILL 1543 | mTabGravity = GRAVITY_FILL; 1544 | updateTabViews(false); 1545 | remeasure = true; 1546 | } 1547 | 1548 | if (remeasure) { 1549 | // Now re-measure after our changes 1550 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 1551 | } 1552 | } 1553 | } 1554 | 1555 | @Override 1556 | protected void onLayout(boolean changed, int l, int t, int r, int b) { 1557 | super.onLayout(changed, l, t, r, b); 1558 | 1559 | if (mCurrentAnimator != null && mCurrentAnimator.isRunning()) { 1560 | // If we're currently running an animation, lets cancel it and start a 1561 | // new animation with the remaining duration 1562 | mCurrentAnimator.cancel(); 1563 | final long duration = mCurrentAnimator.getDuration(); 1564 | animateIndicatorToPosition(mSelectedPosition, 1565 | Math.round((1f - mCurrentAnimator.getAnimatedFraction()) * duration)); 1566 | } else { 1567 | // If we've been layed out, update the indicator position 1568 | updateIndicatorPosition(); 1569 | } 1570 | } 1571 | 1572 | private void updateIndicatorPosition() { 1573 | final View selectedTitle = getChildAt(mSelectedPosition); 1574 | int left, right; 1575 | 1576 | if (selectedTitle != null && selectedTitle.getWidth() > 0) { 1577 | left = selectedTitle.getLeft(); 1578 | right = selectedTitle.getRight(); 1579 | 1580 | if (mSelectionOffset > 0f && mSelectedPosition < getChildCount() - 1) { 1581 | // Draw the selection partway between the tabs 1582 | View nextTitle = getChildAt(mSelectedPosition + 1); 1583 | left = (int) (mSelectionOffset * nextTitle.getLeft() + 1584 | (1.0f - mSelectionOffset) * left); 1585 | right = (int) (mSelectionOffset * nextTitle.getRight() + 1586 | (1.0f - mSelectionOffset) * right); 1587 | } 1588 | } else { 1589 | left = right = -1; 1590 | } 1591 | 1592 | setIndicatorPosition(left, right); 1593 | } 1594 | 1595 | private void setIndicatorPosition(int left, int right) { 1596 | if (left != mIndicatorLeft || right != mIndicatorRight) { 1597 | // If the indicator's left/right has changed, invalidate 1598 | mIndicatorLeft = left; 1599 | mIndicatorRight = right; 1600 | ViewCompat.postInvalidateOnAnimation(this); 1601 | } 1602 | } 1603 | 1604 | void animateIndicatorToPosition(final int position, int duration) { 1605 | final boolean isRtl = ViewCompat.getLayoutDirection(this) 1606 | == ViewCompat.LAYOUT_DIRECTION_RTL; 1607 | 1608 | final View targetView = getChildAt(position); 1609 | final int targetLeft = targetView.getLeft(); 1610 | final int targetRight = targetView.getRight(); 1611 | final int startLeft; 1612 | final int startRight; 1613 | 1614 | if (Math.abs(position - mSelectedPosition) <= 1) { 1615 | // If the views are adjacent, we'll animate from edge-to-edge 1616 | startLeft = mIndicatorLeft; 1617 | startRight = mIndicatorRight; 1618 | } else { 1619 | // Else, we'll just grow from the nearest edge 1620 | final int offset = dpToPx(MOTION_NON_ADJACENT_OFFSET); 1621 | if (position < mSelectedPosition) { 1622 | // We're going end-to-start 1623 | if (isRtl) { 1624 | startLeft = startRight = targetLeft - offset; 1625 | } else { 1626 | startLeft = startRight = targetRight + offset; 1627 | } 1628 | } else { 1629 | // We're going start-to-end 1630 | if (isRtl) { 1631 | startLeft = startRight = targetRight + offset; 1632 | } else { 1633 | startLeft = startRight = targetLeft - offset; 1634 | } 1635 | } 1636 | } 1637 | 1638 | if (startLeft != targetLeft || startRight != targetRight) { 1639 | ValueAnimator animator = mIndicatorAnimator = new ValueAnimator(); 1640 | animator.setInterpolator(new FastOutSlowInInterpolator()); 1641 | animator.setDuration(duration); 1642 | animator.setFloatValues(0, 1); 1643 | animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 1644 | @Override 1645 | public void onAnimationUpdate(ValueAnimator animator) { 1646 | final float fraction = animator.getAnimatedFraction(); 1647 | setIndicatorPosition( 1648 | lerp(startLeft, targetLeft, fraction), 1649 | lerp(startRight, targetRight, fraction)); 1650 | } 1651 | }); 1652 | animator.addListener(new AnimatorListenerAdapter() { 1653 | 1654 | @Override 1655 | public void onAnimationEnd(Animator animator) { 1656 | mSelectedPosition = position; 1657 | mSelectionOffset = 0f; 1658 | } 1659 | 1660 | @Override 1661 | public void onAnimationCancel(Animator animator) { 1662 | mSelectedPosition = position; 1663 | mSelectionOffset = 0f; 1664 | } 1665 | }); 1666 | animator.start(); 1667 | mCurrentAnimator = animator; 1668 | } 1669 | } 1670 | 1671 | private int lerp(int startValue, int endValue, float fraction) { 1672 | return startValue + Math.round(fraction * (endValue - startValue)); 1673 | } 1674 | 1675 | @Override 1676 | public void draw(Canvas canvas) { 1677 | super.draw(canvas); 1678 | 1679 | // Thick colored underline below the current selection 1680 | if (mIndicatorLeft >= 0 && mIndicatorRight > mIndicatorLeft) { 1681 | canvas.drawRect(mIndicatorLeft + getTabMargin(), getHeight() - mSelectedIndicatorHeight, 1682 | mIndicatorRight - getTabMargin(), getHeight(), mSelectedIndicatorPaint); 1683 | } 1684 | } 1685 | } 1686 | 1687 | private static ColorStateList createColorStateList(int defaultColor, int selectedColor) { 1688 | final int[][] states = new int[2][]; 1689 | final int[] colors = new int[2]; 1690 | int i = 0; 1691 | 1692 | states[i] = SELECTED_STATE_SET; 1693 | colors[i] = selectedColor; 1694 | i++; 1695 | 1696 | // Default enabled state 1697 | states[i] = EMPTY_STATE_SET; 1698 | colors[i] = defaultColor; 1699 | i++; 1700 | 1701 | return new ColorStateList(states, colors); 1702 | } 1703 | 1704 | private int getDefaultHeight() { 1705 | boolean hasIconAndText = false; 1706 | for (int i = 0, count = mTabs.size(); i < count; i++) { 1707 | Tab tab = mTabs.get(i); 1708 | if (tab != null && tab.getIcon() != null && !TextUtils.isEmpty(tab.getText())) { 1709 | hasIconAndText = true; 1710 | break; 1711 | } 1712 | } 1713 | return hasIconAndText ? DEFAULT_HEIGHT_WITH_TEXT_ICON : DEFAULT_HEIGHT; 1714 | } 1715 | 1716 | private int getTabMinWidth() { 1717 | if (mRequestedTabMinWidth != INVALID_WIDTH) { 1718 | // If we have been given a min width, use it 1719 | return mRequestedTabMinWidth; 1720 | } 1721 | // Else, we'll use the default value 1722 | return mMode == MODE_SCROLLABLE ? mScrollableTabMinWidth : 0; 1723 | } 1724 | 1725 | private int getTabMaxWidth() { 1726 | return mTabMaxWidth; 1727 | } 1728 | 1729 | /** 1730 | * A {@link ViewPager.OnPageChangeListener} class which contains the 1731 | * necessary calls back to the provided {@link MaterialTabLayout} so that the tab position is 1732 | * kept in sync. 1733 | *

1734 | *

This class stores the provided TabLayout weakly, meaning that you can use 1735 | * {@link ViewPager#addOnPageChangeListener(ViewPager.OnPageChangeListener) 1736 | * addOnPageChangeListener(OnPageChangeListener)} without removing the listener and 1737 | * not cause a leak. 1738 | */ 1739 | public static class TabLayoutOnPageChangeListener implements ViewPager.OnPageChangeListener { 1740 | private final WeakReference mTabLayoutRef; 1741 | private int mPreviousScrollState; 1742 | private int mScrollState; 1743 | 1744 | public TabLayoutOnPageChangeListener(MaterialTabLayout tabLayout) { 1745 | mTabLayoutRef = new WeakReference<>(tabLayout); 1746 | } 1747 | 1748 | @Override 1749 | public void onPageScrollStateChanged(int state) { 1750 | mPreviousScrollState = mScrollState; 1751 | mScrollState = state; 1752 | } 1753 | 1754 | @Override 1755 | public void onPageScrolled(int position, float positionOffset, 1756 | int positionOffsetPixels) { 1757 | final MaterialTabLayout tabLayout = mTabLayoutRef.get(); 1758 | if (tabLayout != null) { 1759 | // Update the scroll position, only update the text selection if we're being 1760 | // dragged (or we're settling after a drag) 1761 | final boolean updateText = (mScrollState == ViewPager.SCROLL_STATE_DRAGGING) 1762 | || (mScrollState == ViewPager.SCROLL_STATE_SETTLING 1763 | && mPreviousScrollState == ViewPager.SCROLL_STATE_DRAGGING); 1764 | tabLayout.setScrollPosition(position, positionOffset, updateText); 1765 | } 1766 | } 1767 | 1768 | @Override 1769 | public void onPageSelected(int position) { 1770 | final MaterialTabLayout tabLayout = mTabLayoutRef.get(); 1771 | if (tabLayout != null && tabLayout.getSelectedTabPosition() != position) { 1772 | // Select the tab, only updating the indicator if we're not being dragged/settled 1773 | // (since onPageScrolled will handle that). 1774 | tabLayout.selectTab(tabLayout.getTabAt(position), 1775 | mScrollState == ViewPager.SCROLL_STATE_IDLE); 1776 | } 1777 | } 1778 | } 1779 | 1780 | /** 1781 | * A {@link MaterialTabLayout.OnTabSelectedListener} class which contains the necessary calls back 1782 | * to the provided {@link ViewPager} so that the tab position is kept in sync. 1783 | */ 1784 | public static class ViewPagerOnTabSelectedListener implements MaterialTabLayout.OnTabSelectedListener { 1785 | private final ViewPager mViewPager; 1786 | 1787 | public ViewPagerOnTabSelectedListener(ViewPager viewPager) { 1788 | mViewPager = viewPager; 1789 | } 1790 | 1791 | @Override 1792 | public void onTabSelected(MaterialTabLayout.Tab tab) { 1793 | mViewPager.setCurrentItem(tab.getPosition()); 1794 | } 1795 | 1796 | @Override 1797 | public void onTabUnselected(MaterialTabLayout.Tab tab) { 1798 | // No-op 1799 | } 1800 | 1801 | @Override 1802 | public void onTabReselected(MaterialTabLayout.Tab tab) { 1803 | // No-op 1804 | } 1805 | } 1806 | 1807 | //******************************以下都是我为了以后的方便添加的***********************************************************// 1808 | 1809 | private int mTabMargin; 1810 | private int mLastCheckedPos = -1; 1811 | private Paint mPaint; 1812 | private int mBottomLineColor = Color.parseColor("#dcdcdc"); 1813 | 1814 | public MaterialTabLayout addTabs(String... tabs) { 1815 | if (tabs == null || tabs.length == 0) 1816 | return this; 1817 | 1818 | for (int i = 0; i < tabs.length; i++) { 1819 | this.addTab(this.newTab().setText(tabs[i]).setTag(i)); 1820 | } 1821 | return this; 1822 | } 1823 | 1824 | public void setSelectTab(int tabPos) { 1825 | getTabAt(tabPos).select(); 1826 | } 1827 | 1828 | public MaterialTabLayout setTabMargin(int margin) { 1829 | mTabMargin = dpToPx(margin); 1830 | return this; 1831 | } 1832 | 1833 | public int getTabMargin() { 1834 | return mTabMargin; 1835 | } 1836 | 1837 | public MaterialTabLayout setBottomLineColor(int lineColor) { 1838 | this.mBottomLineColor = lineColor; 1839 | return this; 1840 | } 1841 | 1842 | 1843 | public interface MaterialTabSelectedListener { 1844 | void onTabSelected(Tab tab, boolean reSelected); 1845 | } 1846 | 1847 | public MaterialTabLayout setOnMaterialTabSelectedListener(MaterialTabSelectedListener listener1) { 1848 | this.listener = listener1; 1849 | setOnTabSelectedListener(new MaterialTabSelectedAdapter()); 1850 | // setSelectTab(0); 1851 | return this; 1852 | } 1853 | 1854 | private MaterialTabSelectedListener listener; 1855 | 1856 | public class MaterialTabSelectedAdapter implements OnTabSelectedListener { 1857 | 1858 | @Override 1859 | public void onTabSelected(Tab tab) { 1860 | if (listener != null) { 1861 | listener.onTabSelected(tab, false); 1862 | mLastCheckedPos = tab.getPosition(); 1863 | } 1864 | } 1865 | 1866 | @Override 1867 | public void onTabUnselected(Tab tab) { 1868 | 1869 | } 1870 | 1871 | @Override 1872 | public void onTabReselected(Tab tab) { 1873 | if (listener != null) { 1874 | if (mLastCheckedPos == -1) { 1875 | onTabSelected(tab); 1876 | } else { 1877 | listener.onTabSelected(tab, true); 1878 | } 1879 | } 1880 | } 1881 | } 1882 | 1883 | @Override 1884 | protected void onDraw(Canvas canvas) { 1885 | super.onDraw(canvas); 1886 | 1887 | initPaintIfNeeded(); 1888 | canvas.drawLine(0, getHeight(), getWidth(), getHeight(), mPaint); 1889 | } 1890 | 1891 | private void initPaintIfNeeded() { 1892 | if (mPaint == null) { 1893 | mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 1894 | mPaint.setStyle(Paint.Style.FILL_AND_STROKE); 1895 | mPaint.setStrokeWidth(2); 1896 | mPaint.setColor(mBottomLineColor); 1897 | } 1898 | } 1899 | } 1900 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 14 | 15 | 16 | 17 | 22 | 23 | 24 | 25 | 26 | 30 | 31 | 32 | 33 | 37 | 38 | 43 | 44 | -------------------------------------------------------------------------------- /app/src/main/res/layout/viewpager_item1.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/layout/viewpager_item2.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/layout/viewpager_item3.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/layout/viewpager_item4.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/layout/viewpager_item5.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrme2014/MaterialTabLayout/7fbb0637a776884892f3f49ecdc5e25cf6d8d3c9/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrme2014/MaterialTabLayout/7fbb0637a776884892f3f49ecdc5e25cf6d8d3c9/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrme2014/MaterialTabLayout/7fbb0637a776884892f3f49ecdc5e25cf6d8d3c9/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrme2014/MaterialTabLayout/7fbb0637a776884892f3f49ecdc5e25cf6d8d3c9/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrme2014/MaterialTabLayout/7fbb0637a776884892f3f49ecdc5e25cf6d8d3c9/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrme2014/MaterialTabLayout/7fbb0637a776884892f3f49ecdc5e25cf6d8d3c9/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrme2014/MaterialTabLayout/7fbb0637a776884892f3f49ecdc5e25cf6d8d3c9/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrme2014/MaterialTabLayout/7fbb0637a776884892f3f49ecdc5e25cf6d8d3c9/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrme2014/MaterialTabLayout/7fbb0637a776884892f3f49ecdc5e25cf6d8d3c9/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrme2014/MaterialTabLayout/7fbb0637a776884892f3f49ecdc5e25cf6d8d3c9/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #FF4081 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | MaterialTabLayout 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/test/java/com/mrs/materialtablayout/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.mrs.materialtablayout; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | @Test 14 | public void addition_isCorrect() throws Exception { 15 | assertEquals(4, 2 + 2); 16 | } 17 | } -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | jcenter() 6 | } 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:2.3.0' 9 | 10 | // NOTE: Do not place your application dependencies here; they belong 11 | // in the individual module build.gradle files 12 | } 13 | } 14 | 15 | allprojects { 16 | repositories { 17 | jcenter() 18 | } 19 | } 20 | 21 | task clean(type: Delete) { 22 | delete rootProject.buildDir 23 | } 24 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | org.gradle.jvmargs=-Xmx1536m 13 | 14 | # When configured, Gradle will run in incubating parallel mode. 15 | # This option should only be used with decoupled projects. More details, visit 16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 17 | # org.gradle.parallel=true 18 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrme2014/MaterialTabLayout/7fbb0637a776884892f3f49ecdc5e25cf6d8d3c9/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat Apr 22 14:28:33 CST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-3.5-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /imgs/3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrme2014/MaterialTabLayout/7fbb0637a776884892f3f49ecdc5e25cf6d8d3c9/imgs/3.gif -------------------------------------------------------------------------------- /imgs/4.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrme2014/MaterialTabLayout/7fbb0637a776884892f3f49ecdc5e25cf6d8d3c9/imgs/4.gif -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | --------------------------------------------------------------------------------