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 | *
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 | *
102 | *
103 | * @see Tabs
104 | */
105 | public class MyTabLayout 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 MyTabLayout} 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 MyTabLayout}.
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 MyTabLayout(Context context) {
232 | this(context, null);
233 | }
234 |
235 | public MyTabLayout(Context context, AttributeSet attrs) {
236 | this(context, attrs, 0);
237 | }
238 |
239 | public MyTabLayout(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 MyTabLayout}.
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 MyTabLayout} 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 MyTabLayout.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 MyTabLayout 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 MyTabLayout 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 MyTabLayout mParent;
954 |
955 | Tab(MyTabLayout 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 MyTabLayout} so that the tab position is
1732 | * kept in sync.
1733 | *