mTabViewPool = new Pools.SimplePool<>(12);
220 |
221 | public XTabLayout(Context context) {
222 | this(context, null);
223 | }
224 |
225 | public XTabLayout(Context context, AttributeSet attrs) {
226 | this(context, attrs, 0);
227 | }
228 |
229 | public XTabLayout(Context context, AttributeSet attrs, int defStyleAttr) {
230 | super(context, attrs, defStyleAttr);
231 |
232 | ThemeUtils.checkAppCompatTheme(context);
233 |
234 | // Disable the Scroll Bar
235 | setHorizontalScrollBarEnabled(false);
236 |
237 | // Add the TabStrip
238 | mTabStrip = new XTabLayout.SlidingTabStrip(context);
239 | super.addView(mTabStrip, 0, new LayoutParams(
240 | LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT));
241 |
242 | /* TypedArray a = context.obtainStyledAttributes(attrs, android.support.design.R.styleable.TabLayout,
243 | defStyleAttr, android.support.design.R.style.Widget_Design_TabLayout);*/
244 | TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.XTabLayout,
245 | defStyleAttr, R.style.Widget_Design_TabLayout);
246 |
247 | mTabStrip.setSelectedIndicatorHeight(
248 | a.getDimensionPixelSize(R.styleable.XTabLayout_xTabIndicatorHeight, dpToPx(2)));
249 | mTabStrip.setmSelectedIndicatorWidth(
250 | a.getDimensionPixelSize(R.styleable.XTabLayout_xTabIndicatorWidth, 0));
251 | mTabStrip.setmSelectedIndicatorRoundX(
252 | a.getDimensionPixelSize(R.styleable.XTabLayout_xTabIndicatorRoundX, 0));
253 | mTabStrip.setmSelectedIndicatorRoundY(
254 | a.getDimensionPixelSize(R.styleable.XTabLayout_xTabIndicatorRoundY, 0));
255 | mTabStrip.setSelectedIndicatorColor(a.getColor(R.styleable.XTabLayout_xTabIndicatorColor, 0));
256 |
257 | mTabPaddingStart = mTabPaddingTop = mTabPaddingEnd = mTabPaddingBottom = a
258 | .getDimensionPixelSize(R.styleable.XTabLayout_xTabPadding, 0);
259 | mTabPaddingStart = a.getDimensionPixelSize(R.styleable.XTabLayout_xTabPaddingStart,
260 | mTabPaddingStart);
261 | mTabPaddingTop = a.getDimensionPixelSize(R.styleable.XTabLayout_xTabPaddingTop,
262 | mTabPaddingTop);
263 | mTabPaddingEnd = a.getDimensionPixelSize(R.styleable.XTabLayout_xTabPaddingEnd,
264 | mTabPaddingEnd);
265 | mTabPaddingBottom = a.getDimensionPixelSize(R.styleable.XTabLayout_xTabPaddingBottom,
266 | mTabPaddingBottom);
267 |
268 |
269 | xTabTextAllCaps = a.getBoolean(R.styleable.XTabLayout_xTabTextAllCaps, false);
270 |
271 | mTabTextAppearance = a.getResourceId(R.styleable.XTabLayout_xTabTextAppearance,
272 | R.style.TextAppearance_Design_Tab);
273 | mTabTextSize = a.getDimensionPixelSize(R.styleable.XTabLayout_xTabTextSize, 0);
274 | xTabTextBold = a.getBoolean(R.styleable.XTabLayout_xTabTextBold, false);
275 | mTabSelectedTextSize = a.getDimensionPixelSize(R.styleable.XTabLayout_xTabSelectedTextSize, 0);
276 | xTabTextSelectedBold = a.getBoolean(R.styleable.XTabLayout_xTabTextSelectedBold, false);
277 |
278 | // Text colors/sizes come from the text appearance first
279 | final TypedArray ta = context.obtainStyledAttributes(mTabTextAppearance,
280 | R.styleable.TextAppearance);
281 | try {
282 | if (mTabTextSize == 0) {
283 | mTabTextSize = ta.getDimensionPixelSize(R.styleable.TextAppearance_android_textSize, 0);
284 | }
285 | mTabTextColors = ta.getColorStateList(R.styleable.TextAppearance_android_textColor);
286 | } finally {
287 | ta.recycle();
288 | }
289 |
290 | if (a.hasValue(R.styleable.XTabLayout_xTabTextColor)) {
291 | // If we have an explicit text color set, use it instead
292 | mTabTextColors = a.getColorStateList(R.styleable.XTabLayout_xTabTextColor);
293 | }
294 |
295 | if (a.hasValue(R.styleable.XTabLayout_xTabSelectedTextColor)) {
296 | // We have an explicit selected text color set, so we need to make merge it with the
297 | // current colors. This is exposed so that developers can use theme attributes to set
298 | // this (theme attrs in ColorStateLists are Lollipop+)
299 | final int selected = a.getColor(R.styleable.XTabLayout_xTabSelectedTextColor, 0);
300 | mTabTextColors = createColorStateList(mTabTextColors.getDefaultColor(), selected);
301 | }
302 |
303 | xTabDisplayNum = a.getInt(R.styleable.XTabLayout_xTabDisplayNum, 0);
304 | mRequestedTabMinWidth = a.getDimensionPixelSize(R.styleable.XTabLayout_xTabMinWidth,
305 | INVALID_WIDTH);
306 | mRequestedTabMaxWidth = a.getDimensionPixelSize(R.styleable.XTabLayout_xTabMaxWidth,
307 | INVALID_WIDTH);
308 | xTabBackgroundColor = a.getColor(R.styleable.XTabLayout_xTabBackgroundColor, 0);
309 | xTabSelectedBackgroundColor = a.getColor(R.styleable.XTabLayout_xTabSelectedBackgroundColor, 0);
310 |
311 | mContentInsetStart = a.getDimensionPixelSize(R.styleable.XTabLayout_xTabContentStart, 0);
312 | mMode = a.getInt(R.styleable.XTabLayout_xTabMode, MODE_FIXED);
313 | mTabGravity = a.getInt(R.styleable.XTabLayout_xTabGravity, GRAVITY_FILL);
314 |
315 | dividerWidth = a.getDimensionPixelSize(R.styleable.XTabLayout_xTabDividerWidth, 0);
316 | dividerHeight = a.getDimensionPixelSize(R.styleable.XTabLayout_xTabDividerHeight, 0);
317 | dividerColor = a.getColor(R.styleable.XTabLayout_xTabDividerColor, Color.BLACK);
318 | dividerGravity = a.getInteger(R.styleable.XTabLayout_xTabDividerGravity, DividerDrawable.CENTER);
319 |
320 | xTabDividerWidthWidthText = a.getBoolean(R.styleable.XTabLayout_xTabDividerWidthWidthText, false);
321 |
322 | a.recycle();
323 |
324 | // TODO add attr for these
325 | final Resources res = getResources();
326 | mTabTextMultiLineSize = res.getDimensionPixelSize(R.dimen.design_tab_text_size_2line);
327 | mScrollableTabMinWidth = res.getDimensionPixelSize(R.dimen.design_tab_scrollable_min_width);
328 |
329 | // Now apply the tab mode and gravity
330 | applyModeAndGravity();
331 |
332 | //添加分割线
333 | addDivider();
334 | }
335 |
336 | /**
337 | * 添加分割线
338 | */
339 | private void addDivider() {
340 | post(new Runnable() {
341 | @Override
342 | public void run() {
343 | if (dividerWidth > 0) {
344 | LinearLayout linearLayout = (LinearLayout) getChildAt(0);
345 | linearLayout.setShowDividers(LinearLayout.SHOW_DIVIDER_MIDDLE);
346 | DividerDrawable dividerDrawable = new DividerDrawable(getContext());
347 | dividerDrawable.setDividerSize(dividerWidth, dividerHeight);
348 | dividerDrawable.setColor(dividerColor);
349 | dividerDrawable.setGravity(dividerGravity);
350 | linearLayout.setDividerDrawable(dividerDrawable);
351 | }
352 | }
353 | });
354 | }
355 |
356 | /**
357 | * 设置分割线长宽
358 | *
359 | * @param width
360 | * @param height 当height =0 时,则分割线长度占满
361 | */
362 | public void setDividerSize(int width, int height) {
363 | dividerWidth = width;
364 | dividerHeight = height;
365 | addDivider();
366 | }
367 |
368 | /**
369 | * 设置分割线颜色
370 | *
371 | * @param color
372 | */
373 | public void setDividerColor(int color) {
374 | dividerColor = color;
375 | addDivider();
376 | }
377 |
378 | /**
379 | * 设置分割线位置
380 | */
381 | public void setDividerGravity(int gravity) {
382 | dividerGravity = gravity;
383 | addDivider();
384 |
385 | }
386 |
387 | /**
388 | * 设置字母是否自动小写转大写
389 | */
390 | public void setAllCaps(boolean allCaps) {
391 | xTabTextAllCaps = allCaps;
392 | // invalidate();
393 | }
394 |
395 | /**
396 | * Sets the tab indicator's color for the currently selected tab.
397 | *
398 | * @param color color to use for the indicator
399 | */
400 | public void setSelectedTabIndicatorColor(@ColorInt int color) {
401 | mTabStrip.setSelectedIndicatorColor(color);
402 | }
403 |
404 | /**
405 | * Sets the tab indicator's height for the currently selected tab.
406 | *
407 | * @param height height to use for the indicator in pixels
408 | */
409 | public void setSelectedTabIndicatorHeight(int height) {
410 | mTabStrip.setSelectedIndicatorHeight(height);
411 | }
412 |
413 | public void setxTabDisplayNum(int xTabDisplayNum) {
414 | this.xTabDisplayNum = xTabDisplayNum;
415 | }
416 |
417 | /**
418 | * Set the scroll position of the tabs. This is useful for when the tabs are being displayed as
419 | * part of a scrolling container such as {@link ViewPager}.
420 | *
421 | * Calling this method does not update the selected tab, it is only used for drawing purposes.
422 | *
423 | * @param position current scroll position
424 | * @param positionOffset Value from [0, 1) indicating the offset from {@code position}.
425 | * @param updateSelectedText Whether to update the text's selected state.
426 | */
427 | public void setScrollPosition(int position, float positionOffset, boolean updateSelectedText) {
428 | setScrollPosition(position, positionOffset, updateSelectedText, true);
429 | }
430 |
431 | private void setScrollPosition(int position, float positionOffset, boolean updateSelectedText,
432 | boolean updateIndicatorPosition) {
433 | final int roundedPosition = Math.round(position + positionOffset);
434 | if (roundedPosition < 0 || roundedPosition >= mTabStrip.getChildCount()) {
435 | return;
436 | }
437 |
438 | // Set the indicator position, if enabled
439 | if (updateIndicatorPosition) {
440 | mTabStrip.setIndicatorPositionFromTabPosition(position, positionOffset);
441 | }
442 |
443 | // Now update the scroll position, canceling any running animation
444 | if (mScrollAnimator != null && mScrollAnimator.isRunning()) {
445 | mScrollAnimator.cancel();
446 | }
447 | scrollTo(calculateScrollXForTab(position, positionOffset), 0);
448 |
449 | // Update the 'selected state' view as we scroll, if enabled
450 | if (updateSelectedText) {
451 | setSelectedTabView(roundedPosition);
452 | }
453 | }
454 |
455 |
456 | private float getScrollPosition() {
457 | return mTabStrip.getIndicatorPosition();
458 | }
459 |
460 | /**
461 | * Add a tab to this layout. The tab will be added at the end of the list.
462 | * If this is the first tab to be added it will become the selected tab.
463 | *
464 | * @param tab Tab to add
465 | */
466 | public void addTab(@NonNull XTabLayout.Tab tab) {
467 | addTab(tab, mTabs.isEmpty());
468 | }
469 |
470 | /**
471 | * Add a tab to this layout. The tab will be inserted at position.
472 | * If this is the first tab to be added it will become the selected tab.
473 | *
474 | * @param tab The tab to add
475 | * @param position The new position of the tab
476 | */
477 | public void addTab(@NonNull XTabLayout.Tab tab, int position) {
478 | addTab(tab, position, mTabs.isEmpty());
479 | }
480 |
481 | /**
482 | * Add a tab to this layout. The tab will be added at the end of the list.
483 | *
484 | * @param tab Tab to add
485 | * @param setSelected True if the added tab should become the selected tab.
486 | */
487 | public void addTab(@NonNull XTabLayout.Tab tab, boolean setSelected) {
488 | if (tab.mParent != this) {
489 | throw new IllegalArgumentException("Tab belongs to a different TabLayout.");
490 | }
491 |
492 | addTabView(tab, setSelected);
493 | configureTab(tab, mTabs.size());
494 | if (setSelected) {
495 | tab.select();
496 | }
497 | }
498 |
499 | /**
500 | * Add a tab to this layout. The tab will be inserted at position.
501 | *
502 | * @param tab The tab to add
503 | * @param position The new position of the tab
504 | * @param setSelected True if the added tab should become the selected tab.
505 | */
506 | public void addTab(@NonNull XTabLayout.Tab tab, int position, boolean setSelected) {
507 | if (tab.mParent != this) {
508 | throw new IllegalArgumentException("Tab belongs to a different TabLayout.");
509 | }
510 |
511 | addTabView(tab, position, setSelected);
512 | configureTab(tab, position);
513 | if (setSelected) {
514 | tab.select();
515 | }
516 | }
517 |
518 | private void addTabFromItemView(@NonNull TabItem item) {
519 | final XTabLayout.Tab tab = newTab();
520 | if (item.mText != null) {
521 | tab.setText(item.mText);
522 | }
523 | if (item.mIcon != null) {
524 | tab.setIcon(item.mIcon);
525 | }
526 | if (item.mCustomLayout != 0) {
527 | tab.setCustomView(item.mCustomLayout);
528 | }
529 | addTab(tab);
530 | }
531 |
532 | /**
533 | * Set the {@link TabLayout.OnTabSelectedListener} that will
534 | * handle switching to and from tabs.
535 | *
536 | * @param onTabSelectedListener Listener to handle tab selection events
537 | */
538 | public void setOnTabSelectedListener(XTabLayout.OnTabSelectedListener onTabSelectedListener) {
539 | mOnTabSelectedListener = onTabSelectedListener;
540 | }
541 |
542 | public void addOnTabSelectedListener(XTabLayout.OnTabSelectedListener onTabSelectedListener) {
543 | mOnTabSelectedListenerList.add(onTabSelectedListener);
544 | }
545 |
546 | @NonNull
547 | public XTabLayout.Tab newTab() {
548 | XTabLayout.Tab tab = sTabPool.acquire();
549 | if (tab == null) {
550 | tab = new XTabLayout.Tab();
551 | }
552 | tab.mParent = this;
553 | tab.mView = createTabView(tab);
554 | return tab;
555 | }
556 |
557 | /**
558 | * Returns the number of tabs currently registered with the action bar.
559 | *
560 | * @return Tab count
561 | */
562 | public int getTabCount() {
563 | return mTabs.size();
564 | }
565 |
566 | /**
567 | * Returns the tab at the specified index.
568 | */
569 | @Nullable
570 | public XTabLayout.Tab getTabAt(int index) {
571 | return mTabs.get(index);
572 | }
573 |
574 | /**
575 | * Returns the position of the current selected tab.
576 | *
577 | * @return selected tab position, or {@code -1} if there isn't a selected tab.
578 | */
579 | public int getSelectedTabPosition() {
580 | return mSelectedTab != null ? mSelectedTab.getPosition() : -1;
581 | }
582 |
583 | /**
584 | * Remove a tab from the layout. If the removed tab was selected it will be deselected
585 | * and another tab will be selected if present.
586 | *
587 | * @param tab The tab to remove
588 | */
589 | public void removeTab(XTabLayout.Tab tab) {
590 | if (tab.mParent != this) {
591 | throw new IllegalArgumentException("Tab does not belong to this TabLayout.");
592 | }
593 |
594 | removeTabAt(tab.getPosition());
595 | }
596 |
597 | /**
598 | * Remove a tab from the layout. If the removed tab was selected it will be deselected
599 | * and another tab will be selected if present.
600 | *
601 | * @param position Position of the tab to remove
602 | */
603 | public void removeTabAt(int position) {
604 | final int selectedTabPosition = mSelectedTab != null ? mSelectedTab.getPosition() : 0;
605 | removeTabViewAt(position);
606 |
607 | final XTabLayout.Tab removedTab = mTabs.remove(position);
608 | if (removedTab != null) {
609 | removedTab.reset();
610 | sTabPool.release(removedTab);
611 | }
612 |
613 | final int newTabCount = mTabs.size();
614 | for (int i = position; i < newTabCount; i++) {
615 | mTabs.get(i).setPosition(i);
616 | }
617 |
618 | if (selectedTabPosition == position) {
619 | selectTab(mTabs.isEmpty() ? null : mTabs.get(Math.max(0, position - 1)));
620 | }
621 | }
622 |
623 | /**
624 | * Remove all tabs from the action bar and deselect the current tab.
625 | */
626 | public void removeAllTabs() {
627 | // Remove all the views
628 | for (int i = mTabStrip.getChildCount() - 1; i >= 0; i--) {
629 | removeTabViewAt(i);
630 | }
631 |
632 | for (final Iterator i = mTabs.iterator(); i.hasNext(); ) {
633 | final XTabLayout.Tab tab = i.next();
634 | i.remove();
635 | tab.reset();
636 | sTabPool.release(tab);
637 | }
638 |
639 | mSelectedTab = null;
640 | }
641 |
642 | /**
643 | * Set the behavior mode for the Tabs in this layout. The valid input options are:
644 | *
645 | * - {@link #MODE_FIXED}: Fixed tabs display all tabs concurrently and are best used
646 | * with content that benefits from quick pivots between tabs.
647 | * - {@link #MODE_SCROLLABLE}: Scrollable tabs display a subset of tabs at any given moment,
648 | * and can contain longer tab labels and a larger number of tabs. They are best used for
649 | * browsing contexts in touch interfaces when users don’t need to directly compare the tab
650 | * labels. This mode is commonly used with a {@link ViewPager}.
651 | *
652 | *
653 | * @param mode one of {@link #MODE_FIXED} or {@link #MODE_SCROLLABLE}.
654 | */
655 | public void setTabMode(@TabLayout.Mode int mode) {
656 | if (mode != mMode) {
657 | mMode = mode;
658 | applyModeAndGravity();
659 | }
660 | }
661 |
662 | /**
663 | * Returns the current mode used by this {@link TabLayout}.
664 | *
665 | * @see #setTabMode(int)
666 | */
667 | @TabLayout.Mode
668 | public int getTabMode() {
669 | return mMode;
670 | }
671 |
672 | /**
673 | * Set the gravity to use when laying out the tabs.
674 | *
675 | * @param gravity one of {@link #GRAVITY_CENTER} or {@link #GRAVITY_FILL}.
676 | */
677 | public void setTabGravity(@TabLayout.TabGravity int gravity) {
678 | if (mTabGravity != gravity) {
679 | mTabGravity = gravity;
680 | applyModeAndGravity();
681 | }
682 | }
683 |
684 | /**
685 | * The current gravity used for laying out tabs.
686 | *
687 | * @return one of {@link #GRAVITY_CENTER} or {@link #GRAVITY_FILL}.
688 | */
689 | @TabLayout.TabGravity
690 | public int getTabGravity() {
691 | return mTabGravity;
692 | }
693 |
694 | /**
695 | * Sets the text colors for the different states (normal, selected) used for the tabs.
696 | *
697 | * @see #getTabTextColors()
698 | */
699 | public void setTabTextColors(@Nullable ColorStateList textColor) {
700 | if (mTabTextColors != textColor) {
701 | mTabTextColors = textColor;
702 | updateAllTabs();
703 | }
704 | }
705 |
706 | /**
707 | * Gets the text colors for the different states (normal, selected) used for the tabs.
708 | */
709 | @Nullable
710 | public ColorStateList getTabTextColors() {
711 | return mTabTextColors;
712 | }
713 |
714 | /**
715 | * Sets the text colors for the different states (normal, selected) used for the tabs.
716 | *
717 | */
718 | public void setTabTextColors(int normalColor, int selectedColor) {
719 | setTabTextColors(createColorStateList(normalColor, selectedColor));
720 | }
721 |
722 | /**
723 | * The one-stop shop for setting up this {@link TabLayout} with a {@link ViewPager}.
724 | *
725 | *
This method will link the given ViewPager and this TabLayout together so that any
726 | * changes in one are automatically reflected in the other. This includes adapter changes,
727 | * scroll state changes, and clicks. The tabs displayed in this layout will be populated
728 | * from the ViewPager adapter's page titles.
729 | *
730 | *
After this method is called, you will not need this method again unless you want
731 | * to change the linked ViewPager.
732 | *
733 | *
If the given ViewPager is non-null, it needs to already have a
734 | * {@link PagerAdapter} set.
735 | *
736 | * @param viewPager The ViewPager to link, or {@code null} to clear any previous link.
737 | */
738 | public void setupWithViewPager(@Nullable final ViewPager viewPager) {
739 | if (mViewPager != null && mPageChangeListener != null) {
740 | // If we've already been setup with a ViewPager, remove us from it
741 | mViewPager.removeOnPageChangeListener(mPageChangeListener);
742 | }
743 |
744 | if (viewPager != null) {
745 | final PagerAdapter adapter = viewPager.getAdapter();
746 | if (adapter == null) {
747 | throw new IllegalArgumentException("ViewPager does not have a PagerAdapter set");
748 | }
749 |
750 | mViewPager = viewPager;
751 |
752 | // Add our custom OnPageChangeListener to the ViewPager
753 | if (mPageChangeListener == null) {
754 | mPageChangeListener = new XTabLayout.TabLayoutOnPageChangeListener(this);
755 | }
756 | mPageChangeListener.reset();
757 | viewPager.addOnPageChangeListener(mPageChangeListener);
758 |
759 | // Now we'll add a tab selected listener to set ViewPager's current item
760 | setOnTabSelectedListener(new XTabLayout.ViewPagerOnTabSelectedListener(viewPager));
761 |
762 | // Now we'll populate ourselves from the pager adapter
763 | setPagerAdapter(adapter, true);
764 | } else {
765 | // We've been given a null ViewPager so we need to clear out the internal state,
766 | // listeners and observers
767 | mViewPager = null;
768 | setOnTabSelectedListener(null);
769 | setPagerAdapter(null, true);
770 | }
771 | }
772 |
773 | /**
774 | * @deprecated Use {@link #setupWithViewPager(ViewPager)} to link a TabLayout with a ViewPager
775 | * together. When that method is used, the TabLayout will be automatically updated
776 | * when the {@link PagerAdapter} is changed.
777 | */
778 | @Deprecated
779 | public void setTabsFromPagerAdapter(@Nullable final PagerAdapter adapter) {
780 | setPagerAdapter(adapter, false);
781 | }
782 |
783 | @Override
784 | public boolean shouldDelayChildPressedState() {
785 | // Only delay the pressed state if the tabs can scroll
786 | return getTabScrollRange() > 0;
787 | }
788 |
789 | private int getTabScrollRange() {
790 | return Math.max(0, mTabStrip.getWidth() - getWidth() - getPaddingLeft()
791 | - getPaddingRight());
792 | }
793 |
794 | private void setPagerAdapter(@Nullable final PagerAdapter adapter, final boolean addObserver) {
795 | if (mPagerAdapter != null && mPagerAdapterObserver != null) {
796 | // If we already have a PagerAdapter, unregister our observer
797 | mPagerAdapter.unregisterDataSetObserver(mPagerAdapterObserver);
798 | }
799 |
800 | mPagerAdapter = adapter;
801 |
802 | if (addObserver && adapter != null) {
803 | // Register our observer on the new adapter
804 | if (mPagerAdapterObserver == null) {
805 | mPagerAdapterObserver = new XTabLayout.PagerAdapterObserver();
806 | }
807 | adapter.registerDataSetObserver(mPagerAdapterObserver);
808 | }
809 |
810 | // Finally make sure we reflect the new adapter
811 | populateFromPagerAdapter();
812 | }
813 |
814 | private void populateFromPagerAdapter() {
815 | removeAllTabs();
816 |
817 | if (mPagerAdapter != null) {
818 | final int adapterCount = mPagerAdapter.getCount();
819 |
820 | for (int i = 0; i < adapterCount; i++) {
821 | addTab(newTab().setText(mPagerAdapter.getPageTitle(i)), false);
822 | }
823 |
824 | // Make sure we reflect the currently set ViewPager item
825 | if (mViewPager != null && adapterCount > 0) {
826 | final int curItem = mViewPager.getCurrentItem();
827 | if (curItem != getSelectedTabPosition() && curItem < getTabCount()) {
828 | selectTab(getTabAt(curItem));
829 | }
830 | }
831 | } else {
832 | removeAllTabs();
833 | }
834 | }
835 |
836 | private void updateAllTabs() {
837 | for (int i = 0, z = mTabs.size(); i < z; i++) {
838 | mTabs.get(i).updateView();
839 | }
840 | }
841 |
842 | private XTabLayout.TabView createTabView(@NonNull final XTabLayout.Tab tab) {
843 | XTabLayout.TabView tabView = mTabViewPool != null ? mTabViewPool.acquire() : null;
844 | if (tabView == null) {
845 | tabView = new XTabLayout.TabView(getContext());
846 | }
847 | tabView.setTab(tab);
848 | tabView.setFocusable(true);
849 | tabView.setMinimumWidth(getTabMinWidth());
850 | return tabView;
851 | }
852 |
853 | private void configureTab(XTabLayout.Tab tab, int position) {
854 | tab.setPosition(position);
855 | mTabs.add(position, tab);
856 |
857 | final int count = mTabs.size();
858 | for (int i = position + 1; i < count; i++) {
859 | mTabs.get(i).setPosition(i);
860 | }
861 | }
862 |
863 | private void addTabView(XTabLayout.Tab tab, boolean setSelected) {
864 | final XTabLayout.TabView tabView = tab.mView;
865 | if (mTabSelectedTextSize != 0) {
866 | tabView.post(new Runnable() {
867 | @Override
868 | public void run() {
869 | int tabWidth = tabView.getWidth();
870 | String text = tabView.getText();
871 | if (!TextUtils.isEmpty(text)) {
872 | Paint paint = new Paint();
873 | paint.setTextSize(mTabSelectedTextSize);
874 | Rect rect = new Rect();
875 | paint.getTextBounds(text, 0, text.length(), rect);
876 |
877 | if (tabWidth - rect.width() < dpToPx(SELECTED_TAB_ADD_WIDTH)) {
878 | tabWidth = rect.width() + dpToPx(SELECTED_TAB_ADD_WIDTH);
879 | ViewGroup.LayoutParams layoutParams = tabView.getLayoutParams();
880 | layoutParams.width = tabWidth;
881 | tabView.setLayoutParams(layoutParams);
882 | }
883 | }
884 | }
885 | });
886 | }
887 | mTabStrip.addView(tabView, createLayoutParamsForTabs());
888 | if (setSelected) {
889 | tabView.setSelected(true);
890 | }
891 | }
892 |
893 | private void addTabView(XTabLayout.Tab tab, int position, boolean setSelected) {
894 | final XTabLayout.TabView tabView = tab.mView;
895 | mTabStrip.addView(tabView, position, createLayoutParamsForTabs());
896 | if (setSelected) {
897 | tabView.setSelected(true);
898 | }
899 | }
900 |
901 | @Override
902 | public void addView(View child) {
903 | addViewInternal(child);
904 | }
905 |
906 | @Override
907 | public void addView(View child, int index) {
908 | addViewInternal(child);
909 | }
910 |
911 | @Override
912 | public void addView(View child, ViewGroup.LayoutParams params) {
913 | addViewInternal(child);
914 | }
915 |
916 | @Override
917 | public void addView(View child, int index, ViewGroup.LayoutParams params) {
918 | addViewInternal(child);
919 | }
920 |
921 | private void addViewInternal(final View child) {
922 | if (child instanceof TabItem) {
923 | addTabFromItemView((TabItem) child);
924 | } else {
925 | throw new IllegalArgumentException("Only TabItem instances can be added to TabLayout");
926 | }
927 | }
928 |
929 | private LinearLayout.LayoutParams createLayoutParamsForTabs() {
930 | final LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
931 | LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
932 | updateTabViewLayoutParams(lp);
933 | return lp;
934 | }
935 |
936 | private void updateTabViewLayoutParams(LinearLayout.LayoutParams lp) {
937 | if (mMode == MODE_FIXED && mTabGravity == GRAVITY_FILL) {
938 | lp.width = 0;
939 | lp.weight = 1;
940 | } else {
941 | lp.width = LinearLayout.LayoutParams.WRAP_CONTENT;
942 | lp.weight = 0;
943 | }
944 | }
945 |
946 | private int dpToPx(int dps) {
947 | return Math.round(getResources().getDisplayMetrics().density * dps);
948 | }
949 |
950 | @Override
951 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
952 | // If we have a MeasureSpec which allows us to decide our height, try and use the default
953 | // height
954 | final int idealHeight = dpToPx(getDefaultHeight()) + getPaddingTop() + getPaddingBottom();
955 | switch (MeasureSpec.getMode(heightMeasureSpec)) {
956 | case MeasureSpec.AT_MOST:
957 | heightMeasureSpec = MeasureSpec.makeMeasureSpec(
958 | Math.min(idealHeight, MeasureSpec.getSize(heightMeasureSpec)),
959 | MeasureSpec.EXACTLY);
960 | break;
961 | case MeasureSpec.UNSPECIFIED:
962 | heightMeasureSpec = MeasureSpec.makeMeasureSpec(idealHeight, MeasureSpec.EXACTLY);
963 | break;
964 | }
965 |
966 | final int specWidth = MeasureSpec.getSize(widthMeasureSpec);
967 | if (MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.UNSPECIFIED) {
968 | // If we don't have an unspecified width spec, use the given size to calculate
969 | // the max tab width
970 | Log.w("BBB", "specWidth:" + specWidth);
971 | if (mPagerAdapter != null && xTabDisplayNum != 0) {
972 | if (mPagerAdapter.getCount() == 1 || xTabDisplayNum == 1) {
973 | WindowManager wm = (WindowManager) getContext()
974 | .getSystemService(Context.WINDOW_SERVICE);
975 | mTabMaxWidth = wm.getDefaultDisplay().getWidth();
976 | } else {
977 | mTabMaxWidth = mRequestedTabMaxWidth > 0
978 | ? mRequestedTabMaxWidth
979 | : specWidth - dpToPx(TAB_MIN_WIDTH_MARGIN);
980 | }
981 | } else {
982 | mTabMaxWidth = mRequestedTabMaxWidth > 0
983 | ? mRequestedTabMaxWidth
984 | : specWidth - dpToPx(TAB_MIN_WIDTH_MARGIN);
985 | }
986 | }
987 |
988 | // Now super measure itself using the (possibly) modified height spec
989 | super.onMeasure(widthMeasureSpec, heightMeasureSpec);
990 |
991 | if (getChildCount() == 1) {
992 | // If we're in fixed mode then we need to make the tab strip is the same width as us
993 | // so we don't scroll
994 | final View child = getChildAt(0);
995 | boolean remeasure = false;
996 |
997 | switch (mMode) {
998 | case MODE_SCROLLABLE:
999 | // We only need to resize the child if it's smaller than us. This is similar
1000 | // to fillViewport
1001 | remeasure = child.getMeasuredWidth() < getMeasuredWidth();
1002 | break;
1003 | case MODE_FIXED:
1004 | // Resize the child so that it doesn't scroll
1005 | remeasure = child.getMeasuredWidth() != getMeasuredWidth();
1006 | break;
1007 | }
1008 |
1009 | if (remeasure) {
1010 | // Re-measure the child with a widthSpec set to be exactly our measure width
1011 | int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, getPaddingTop()
1012 | + getPaddingBottom(), child.getLayoutParams().height);
1013 | int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
1014 | getMeasuredWidth(), MeasureSpec.EXACTLY);
1015 | child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
1016 | }
1017 | }
1018 | }
1019 |
1020 | private void removeTabViewAt(int position) {
1021 | final XTabLayout.TabView view = (XTabLayout.TabView) mTabStrip.getChildAt(position);
1022 | mTabStrip.removeViewAt(position);
1023 | if (view != null) {
1024 | view.reset();
1025 | mTabViewPool.release(view);
1026 | }
1027 | requestLayout();
1028 | }
1029 |
1030 | private void animateToTab(int newPosition) {
1031 | if (newPosition == XTabLayout.Tab.INVALID_POSITION) {
1032 | return;
1033 | }
1034 |
1035 | if (getWindowToken() == null || !ViewCompat.isLaidOut(this)
1036 | || mTabStrip.childrenNeedLayout()) {
1037 | // If we don't have a window token, or we haven't been laid out yet just draw the new
1038 | // position now
1039 | setScrollPosition(newPosition, 0f, true);
1040 | return;
1041 | }
1042 |
1043 | final int startScrollX = getScrollX();
1044 | final int targetScrollX = calculateScrollXForTab(newPosition, 0);
1045 | if (startScrollX != targetScrollX) {
1046 | if (mScrollAnimator == null) {
1047 | mScrollAnimator = ViewUtils.createAnimator();
1048 | mScrollAnimator.setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR);
1049 | mScrollAnimator.setDuration(ANIMATION_DURATION);
1050 | mScrollAnimator.setUpdateListener(new ValueAnimatorCompat.AnimatorUpdateListener() {
1051 | @Override
1052 | public void onAnimationUpdate(ValueAnimatorCompat animator) {
1053 | scrollTo(animator.getAnimatedIntValue(), 0);
1054 | }
1055 | });
1056 | }
1057 |
1058 | mScrollAnimator.setIntValues(startScrollX, targetScrollX);
1059 | mScrollAnimator.start();
1060 | }
1061 |
1062 | // Now animate the indicator
1063 | mTabStrip.animateIndicatorToPosition(newPosition, ANIMATION_DURATION);
1064 | }
1065 |
1066 | private void setSelectedTabView(int position) {
1067 | final int tabCount = mTabStrip.getChildCount();
1068 | if (position < tabCount && !mTabStrip.getChildAt(position).isSelected()) {
1069 | for (int i = 0; i < tabCount; i++) {
1070 | final View child = mTabStrip.getChildAt(i);
1071 | child.setSelected(i == position);
1072 | }
1073 | }
1074 | }
1075 |
1076 | void selectTab(XTabLayout.Tab tab) {
1077 | selectTab(tab, true);
1078 | }
1079 |
1080 | void selectTab(XTabLayout.Tab tab, boolean updateIndicator) {
1081 | if (mSelectedTab == tab) {
1082 | if (mSelectedTab != null) {
1083 | if (mOnTabSelectedListener != null) {
1084 | mOnTabSelectedListener.onTabReselected(mSelectedTab);
1085 | }
1086 | for (OnTabSelectedListener onTabSelectedListener : mOnTabSelectedListenerList) {
1087 | onTabSelectedListener.onTabReselected(mSelectedTab);
1088 | }
1089 | animateToTab(tab.getPosition());
1090 | }
1091 | } else {
1092 | if (updateIndicator) {
1093 | final int newPosition = tab != null ? tab.getPosition() : XTabLayout.Tab.INVALID_POSITION;
1094 | if (newPosition != XTabLayout.Tab.INVALID_POSITION) {
1095 | setSelectedTabView(newPosition);
1096 | }
1097 | if ((mSelectedTab == null || mSelectedTab.getPosition() == XTabLayout.Tab.INVALID_POSITION)
1098 | && newPosition != XTabLayout.Tab.INVALID_POSITION) {
1099 | // If we don't currently have a tab, just draw the indicator
1100 | setScrollPosition(newPosition, 0f, true);
1101 | } else {
1102 | animateToTab(newPosition);
1103 | }
1104 | }
1105 | if (mSelectedTab != null && mOnTabSelectedListener != null) {
1106 | mOnTabSelectedListener.onTabUnselected(mSelectedTab);
1107 | }
1108 | for (OnTabSelectedListener onTabSelectedListener : mOnTabSelectedListenerList) {
1109 | onTabSelectedListener.onTabUnselected(mSelectedTab);
1110 | }
1111 | mSelectedTab = tab;
1112 | if (mSelectedTab != null && mOnTabSelectedListener != null) {
1113 | mOnTabSelectedListener.onTabSelected(mSelectedTab);
1114 | }
1115 | for (OnTabSelectedListener onTabSelectedListener : mOnTabSelectedListenerList) {
1116 | onTabSelectedListener.onTabSelected(mSelectedTab);
1117 | }
1118 | }
1119 | }
1120 |
1121 | private int calculateScrollXForTab(int position, float positionOffset) {
1122 | if (mMode == MODE_SCROLLABLE) {
1123 | final View selectedChild = mTabStrip.getChildAt(position);
1124 | final View nextChild = position + 1 < mTabStrip.getChildCount()
1125 | ? mTabStrip.getChildAt(position + 1)
1126 | : null;
1127 | final int selectedWidth = selectedChild != null ? selectedChild.getWidth() : 0;
1128 | final int nextWidth = nextChild != null ? nextChild.getWidth() : 0;
1129 |
1130 | return selectedChild.getLeft()
1131 | + ((int) ((selectedWidth + nextWidth) * positionOffset * 0.5f))
1132 | + (selectedChild.getWidth() / 2)
1133 | - (getWidth() / 2);
1134 | }
1135 | return 0;
1136 | }
1137 |
1138 | private void applyModeAndGravity() {
1139 | int paddingStart = 0;
1140 | if (mMode == MODE_SCROLLABLE) {
1141 | // If we're scrollable, or fixed at start, inset using padding
1142 | paddingStart = Math.max(0, mContentInsetStart - mTabPaddingStart);
1143 | }
1144 | ViewCompat.setPaddingRelative(mTabStrip, paddingStart, 0, 0, 0);
1145 |
1146 | switch (mMode) {
1147 | case MODE_FIXED:
1148 | mTabStrip.setGravity(Gravity.CENTER_HORIZONTAL);
1149 | break;
1150 | case MODE_SCROLLABLE:
1151 | mTabStrip.setGravity(GravityCompat.START);
1152 | break;
1153 | }
1154 |
1155 | updateTabViews(true);
1156 | }
1157 |
1158 | private void updateTabViews(final boolean requestLayout) {
1159 | for (int i = 0; i < mTabStrip.getChildCount(); i++) {
1160 | View child = mTabStrip.getChildAt(i);
1161 | child.setMinimumWidth(getTabMinWidth());
1162 | updateTabViewLayoutParams((LinearLayout.LayoutParams) child.getLayoutParams());
1163 | if (requestLayout) {
1164 | child.requestLayout();
1165 | }
1166 | }
1167 | }
1168 |
1169 | /**
1170 | * A tab in this layout. Instances can be created via {@link #newTab()}.
1171 | */
1172 | public static final class Tab {
1173 |
1174 | /**
1175 | * An invalid position for a tab.
1176 | *
1177 | * @see #getPosition()
1178 | */
1179 | public static final int INVALID_POSITION = -1;
1180 |
1181 | private Object mTag;
1182 | private Drawable mIcon;
1183 | private CharSequence mText;
1184 | private CharSequence mContentDesc;
1185 | private int mPosition = INVALID_POSITION;
1186 | private View mCustomView;
1187 |
1188 | private XTabLayout mParent;
1189 | private XTabLayout.TabView mView;
1190 |
1191 | private Tab() {
1192 | // Private constructor
1193 | }
1194 |
1195 |
1196 | /**
1197 | * @return This Tab's tag object.
1198 | */
1199 | @Nullable
1200 | public Object getTag() {
1201 | return mTag;
1202 | }
1203 |
1204 | public int getTextWidth() {
1205 | return mView.getTextWidth();
1206 | }
1207 |
1208 | /**
1209 | * Give this Tab an arbitrary object to hold for later use.
1210 | *
1211 | * @param tag Object to store
1212 | * @return The current instance for call chaining
1213 | */
1214 | @NonNull
1215 | public XTabLayout.Tab setTag(@Nullable Object tag) {
1216 | mTag = tag;
1217 | return this;
1218 | }
1219 |
1220 |
1221 | /**
1222 | * Returns the custom view used for this tab.
1223 | *
1224 | * @see #setCustomView(View)
1225 | * @see #setCustomView(int)
1226 | */
1227 | @Nullable
1228 | public View getCustomView() {
1229 | return mCustomView;
1230 | }
1231 |
1232 | /**
1233 | * Set a custom view to be used for this tab.
1234 | *
1235 | * If the provided view contains a {@link TextView} with an ID of
1236 | * {@link android.R.id#text1} then that will be updated with the value given
1237 | * {@link ImageView} with ID {@link android.R.id#icon} then it will be updated with
1238 | * the value given to {@link #setIcon(Drawable)}.
1239 | *
1240 | *
1241 | * @param view Custom view to be used as a tab.
1242 | * @return The current instance for call chaining
1243 | */
1244 | @NonNull
1245 | public XTabLayout.Tab setCustomView(@Nullable View view) {
1246 | mCustomView = view;
1247 | updateView();
1248 | return this;
1249 | }
1250 |
1251 | /**
1252 | * Set a custom view to be used for this tab.
1253 | *
1254 | * If the inflated layout contains a {@link TextView} with an ID of
1255 | * {@link android.R.id#text1} then that will be updated with the value given
1256 | * {@link ImageView} with ID {@link android.R.id#icon} then it will be updated with
1257 | * the value given to {@link #setIcon(Drawable)}.
1258 | *
1259 | *
1260 | * @param resId A layout resource to inflate and use as a custom tab view
1261 | * @return The current instance for call chaining
1262 | */
1263 | @NonNull
1264 | public XTabLayout.Tab setCustomView(@LayoutRes int resId) {
1265 | final LayoutInflater inflater = LayoutInflater.from(mView.getContext());
1266 | return setCustomView(inflater.inflate(resId, mView, false));
1267 | }
1268 |
1269 | /**
1270 | * Return the icon associated with this tab.
1271 | *
1272 | * @return The tab's icon
1273 | */
1274 | @Nullable
1275 | public Drawable getIcon() {
1276 | return mIcon;
1277 | }
1278 |
1279 | /**
1280 | * Return the current position of this tab in the action bar.
1281 | *
1282 | * @return Current position, or {@link #INVALID_POSITION} if this tab is not currently in
1283 | * the action bar.
1284 | */
1285 | public int getPosition() {
1286 | return mPosition;
1287 | }
1288 |
1289 | void setPosition(int position) {
1290 | mPosition = position;
1291 | }
1292 |
1293 | /**
1294 | * Return the text of this tab.
1295 | *
1296 | * @return The tab's text
1297 | */
1298 | @Nullable
1299 | public CharSequence getText() {
1300 | return mText;
1301 | }
1302 |
1303 |
1304 | /**
1305 | * Set the icon displayed on this tab.
1306 | *
1307 | * @param icon The drawable to use as an icon
1308 | * @return The current instance for call chaining
1309 | */
1310 | @NonNull
1311 | public XTabLayout.Tab setIcon(@Nullable Drawable icon) {
1312 | mIcon = icon;
1313 | updateView();
1314 | return this;
1315 | }
1316 |
1317 | /**
1318 | * Set the icon displayed on this tab.
1319 | *
1320 | * @param resId A resource ID referring to the icon that should be displayed
1321 | * @return The current instance for call chaining
1322 | */
1323 | @NonNull
1324 | public XTabLayout.Tab setIcon(@DrawableRes int resId) {
1325 | if (mParent == null) {
1326 | throw new IllegalArgumentException("Tab not attached to a TabLayout");
1327 | }
1328 | return setIcon(get().getDrawable(mParent.getContext(), resId));
1329 | }
1330 |
1331 | /**
1332 | * Set the text displayed on this tab. Text may be truncated if there is not room to display
1333 | * the entire string.
1334 | *
1335 | * @param text The text to display
1336 | * @return The current instance for call chaining
1337 | */
1338 | @NonNull
1339 | public XTabLayout.Tab setText(@Nullable CharSequence text) {
1340 | mText = text;
1341 | updateView();
1342 | return this;
1343 | }
1344 |
1345 | /**
1346 | * Set the text displayed on this tab. Text may be truncated if there is not room to display
1347 | * the entire string.
1348 | *
1349 | * @param resId A resource ID referring to the text that should be displayed
1350 | * @return The current instance for call chaining
1351 | */
1352 | @NonNull
1353 | public XTabLayout.Tab setText(@StringRes int resId) {
1354 | if (mParent == null) {
1355 | throw new IllegalArgumentException("Tab not attached to a TabLayout");
1356 | }
1357 | return setText(mParent.getResources().getText(resId));
1358 | }
1359 |
1360 | /**
1361 | * Select this tab. Only valid if the tab has been added to the action bar.
1362 | */
1363 | public void select() {
1364 | if (mParent == null) {
1365 | throw new IllegalArgumentException("Tab not attached to a TabLayout");
1366 | }
1367 | mParent.selectTab(this);
1368 | }
1369 |
1370 | /**
1371 | * Returns true if this tab is currently selected.
1372 | */
1373 | public boolean isSelected() {
1374 | if (mParent == null) {
1375 | throw new IllegalArgumentException("Tab not attached to a TabLayout");
1376 | }
1377 | return mParent.getSelectedTabPosition() == mPosition;
1378 | }
1379 |
1380 | /**
1381 | * Set a description of this tab's content for use in accessibility support. If no content
1382 | * description is provided the title will be used.
1383 | *
1384 | * @param resId A resource ID referring to the description text
1385 | * @return The current instance for call chaining
1386 | * @see #getContentDescription()
1387 | */
1388 | @NonNull
1389 | public XTabLayout.Tab setContentDescription(@StringRes int resId) {
1390 | if (mParent == null) {
1391 | throw new IllegalArgumentException("Tab not attached to a TabLayout");
1392 | }
1393 | return setContentDescription(mParent.getResources().getText(resId));
1394 | }
1395 |
1396 | /**
1397 | * Set a description of this tab's content for use in accessibility support. If no content
1398 | * description is provided the title will be used.
1399 | *
1400 | * @param contentDesc Description of this tab's content
1401 | * @return The current instance for call chaining
1402 | * @see #setContentDescription(int)
1403 | * @see #getContentDescription()
1404 | */
1405 | @NonNull
1406 | public XTabLayout.Tab setContentDescription(@Nullable CharSequence contentDesc) {
1407 | mContentDesc = contentDesc;
1408 | updateView();
1409 | return this;
1410 | }
1411 |
1412 | /**
1413 | * Gets a brief description of this tab's content for use in accessibility support.
1414 | *
1415 | * @return Description of this tab's content
1416 | * @see #setContentDescription(int)
1417 | */
1418 | @Nullable
1419 | public CharSequence getContentDescription() {
1420 | return mContentDesc;
1421 | }
1422 |
1423 | private void updateView() {
1424 | if (mView != null) {
1425 | mView.update();
1426 | }
1427 | }
1428 |
1429 | private void reset() {
1430 | mParent = null;
1431 | mView = null;
1432 | mTag = null;
1433 | mIcon = null;
1434 | mText = null;
1435 | mContentDesc = null;
1436 | mPosition = INVALID_POSITION;
1437 | mCustomView = null;
1438 | }
1439 | }
1440 |
1441 | class TabView extends LinearLayout implements OnLongClickListener {
1442 | private XTabLayout.Tab mTab;
1443 | private TextView mTextView;
1444 | private ImageView mIconView;
1445 |
1446 | private View mCustomView;
1447 | private TextView mCustomTextView;
1448 | private ImageView mCustomIconView;
1449 |
1450 | private int mDefaultMaxLines = 2;
1451 |
1452 | public TabView(Context context) {
1453 | super(context);
1454 | /* if (mTabBackgroundResId != 0) {
1455 | setBackgroundDrawable(
1456 | AppCompatDrawableManager.get().getDrawable(context, mTabBackgroundResId));
1457 | }*/
1458 | ViewCompat.setPaddingRelative(this, mTabPaddingStart, mTabPaddingTop,
1459 | mTabPaddingEnd, mTabPaddingBottom);
1460 | setGravity(Gravity.CENTER);
1461 | setOrientation(VERTICAL);
1462 | setClickable(true);
1463 | }
1464 |
1465 | public String getText() {
1466 | return mTextView.getText().toString();
1467 | }
1468 |
1469 | public int getTextWidth() {
1470 | if (TextUtils.isEmpty(mTextView.getText().toString())) {
1471 | return 0;
1472 | }
1473 | Rect rect = new Rect();
1474 | String content = mTextView.getText().toString();
1475 | mTextView.getPaint().getTextBounds(content, 0, content.length(), rect);
1476 | return rect.width();
1477 | }
1478 |
1479 | @Override
1480 | public boolean performClick() {
1481 | final boolean value = super.performClick();
1482 |
1483 | if (mTab != null) {
1484 | mTab.select();
1485 | return true;
1486 | } else {
1487 | return value;
1488 | }
1489 | }
1490 |
1491 | @Override
1492 | public void setSelected(boolean selected) {
1493 | final boolean changed = (isSelected() != selected);
1494 | super.setSelected(selected);
1495 | if (!selected) {
1496 | if (xTabBackgroundColor != 0) {
1497 | setBackgroundColor(xTabBackgroundColor);
1498 | }
1499 | mTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTabTextSize);
1500 | if (xTabTextBold) {
1501 | mTextView.setTypeface(Typeface.defaultFromStyle(Typeface.BOLD));
1502 | } else {
1503 | mTextView.setTypeface(Typeface.defaultFromStyle(Typeface.NORMAL));
1504 | }
1505 | }
1506 | if (changed && selected) {
1507 | if (xTabSelectedBackgroundColor != 0) {
1508 | setBackgroundColor(xTabSelectedBackgroundColor);
1509 | }
1510 | sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
1511 |
1512 | if (mTextView != null) {
1513 | mTextView.setSelected(selected);
1514 |
1515 | if (mTabSelectedTextSize != 0) {
1516 | mTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTabSelectedTextSize);
1517 | if (xTabTextSelectedBold) {
1518 | mTextView.setTypeface(Typeface.defaultFromStyle(Typeface.BOLD));
1519 | } else {
1520 | mTextView.setTypeface(Typeface.defaultFromStyle(Typeface.NORMAL));
1521 | }
1522 | }
1523 | }
1524 | if (mIconView != null) {
1525 | mIconView.setSelected(selected);
1526 | }
1527 | }
1528 | }
1529 |
1530 | @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
1531 | @Override
1532 | public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
1533 | super.onInitializeAccessibilityEvent(event);
1534 | // This view masquerades as an action bar tab.
1535 | event.setClassName(ActionBar.Tab.class.getName());
1536 | }
1537 |
1538 | @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
1539 | @Override
1540 | public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
1541 | super.onInitializeAccessibilityNodeInfo(info);
1542 | // This view masquerades as an action bar tab.
1543 | info.setClassName(ActionBar.Tab.class.getName());
1544 | }
1545 |
1546 | @Override
1547 | public void onMeasure(final int origWidthMeasureSpec, final int origHeightMeasureSpec) {
1548 | final int specWidthSize = MeasureSpec.getSize(origWidthMeasureSpec);
1549 | final int specWidthMode = MeasureSpec.getMode(origWidthMeasureSpec);
1550 | final int maxWidth = getTabMaxWidth();
1551 |
1552 | final int widthMeasureSpec;
1553 | final int heightMeasureSpec = origHeightMeasureSpec;
1554 |
1555 | if (maxWidth > 0 && (specWidthMode == MeasureSpec.UNSPECIFIED
1556 | || specWidthSize > maxWidth)) {
1557 | // If we have a max width and a given spec which is either unspecified or
1558 | // larger than the max width, update the width spec using the same mode
1559 | widthMeasureSpec = MeasureSpec.makeMeasureSpec(mTabMaxWidth, MeasureSpec.AT_MOST);
1560 | } else {
1561 | // Else, use the original width spec
1562 | widthMeasureSpec = origWidthMeasureSpec;
1563 | }
1564 |
1565 | // Now lets measure
1566 | super.onMeasure(widthMeasureSpec, heightMeasureSpec);
1567 |
1568 | // We need to switch the text size based on whether the text is spanning 2 lines or not
1569 | if (mTextView != null) {
1570 | final Resources res = getResources();
1571 | float textSize = mTabTextSize;
1572 | int maxLines = mDefaultMaxLines;
1573 |
1574 | if (mIconView != null && mIconView.getVisibility() == VISIBLE) {
1575 | // If the icon view is being displayed, we limit the text to 1 line
1576 | maxLines = 1;
1577 | } else if (mTextView != null && mTextView.getLineCount() > 1) {
1578 | // Otherwise when we have text which wraps we reduce the text size
1579 | textSize = mTabTextMultiLineSize;
1580 | }
1581 |
1582 | final float curTextSize = mTextView.getTextSize();
1583 | final int curLineCount = mTextView.getLineCount();
1584 | final int curMaxLines = TextViewCompat.getMaxLines(mTextView);
1585 |
1586 | if (textSize != curTextSize || (curMaxLines >= 0 && maxLines != curMaxLines)) {
1587 | // We've got a new text size and/or max lines...
1588 | boolean updateTextView = true;
1589 |
1590 | if (mMode == MODE_FIXED && textSize > curTextSize && curLineCount == 1) {
1591 | // If we're in fixed mode, going up in text size and currently have 1 line
1592 | // then it's very easy to get into an infinite recursion.
1593 | // To combat that we check to see if the change in text size
1594 | // will cause a line count change. If so, abort the size change.
1595 | final Layout layout = mTextView.getLayout();
1596 | if (layout == null
1597 | || approximateLineWidth(layout, 0, textSize) > layout.getWidth()) {
1598 | updateTextView = false;
1599 | }
1600 | }
1601 |
1602 | if (updateTextView) {
1603 | if (mTextView.isSelected() && mTabSelectedTextSize != 0) {
1604 | mTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTabSelectedTextSize);
1605 | } else {
1606 | mTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTabTextSize);
1607 | }
1608 | // mTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
1609 | mTextView.setMaxLines(maxLines);
1610 | super.onMeasure(widthMeasureSpec, heightMeasureSpec);
1611 | }
1612 | }
1613 | }
1614 | }
1615 |
1616 | private void setTab(@Nullable final XTabLayout.Tab tab) {
1617 | if (tab != mTab) {
1618 | mTab = tab;
1619 | update();
1620 | }
1621 | }
1622 |
1623 | private void reset() {
1624 | setTab(null);
1625 | setSelected(false);
1626 | }
1627 |
1628 | final void update() {
1629 | final XTabLayout.Tab tab = mTab;
1630 | final View custom = tab != null ? tab.getCustomView() : null;
1631 | if (custom != null) {
1632 | final ViewParent customParent = custom.getParent();
1633 | if (customParent != this) {
1634 | if (customParent != null) {
1635 | ((ViewGroup) customParent).removeView(custom);
1636 | }
1637 | addView(custom);
1638 | }
1639 | mCustomView = custom;
1640 | if (mTextView != null) {
1641 | mTextView.setVisibility(GONE);
1642 | }
1643 | if (mIconView != null) {
1644 | mIconView.setVisibility(GONE);
1645 | mIconView.setImageDrawable(null);
1646 | }
1647 |
1648 | mCustomTextView = (TextView) custom.findViewById(android.R.id.text1);
1649 | if (mCustomTextView != null) {
1650 | mDefaultMaxLines = TextViewCompat.getMaxLines(mCustomTextView);
1651 | }
1652 | mCustomIconView = (ImageView) custom.findViewById(android.R.id.icon);
1653 | } else {
1654 | // We do not have a custom view. Remove one if it already exists
1655 | if (mCustomView != null) {
1656 | removeView(mCustomView);
1657 | mCustomView = null;
1658 | }
1659 | mCustomTextView = null;
1660 | mCustomIconView = null;
1661 | }
1662 |
1663 | if (mCustomView == null) {
1664 | // If there isn't a custom view, we'll us our own in-built layouts
1665 | if (mIconView == null) {
1666 | ImageView iconView = (ImageView) LayoutInflater.from(getContext())
1667 | .inflate(R.layout.design_layout_tab_icon, this, false);
1668 | addView(iconView, 0);
1669 | mIconView = iconView;
1670 | }
1671 | if (mTextView == null) {
1672 | TextView textView = (TextView) LayoutInflater.from(getContext())
1673 | .inflate(R.layout.design_layout_tab_text, this, false);
1674 | addView(textView);
1675 | mTextView = textView;
1676 | mDefaultMaxLines = TextViewCompat.getMaxLines(mTextView);
1677 | }
1678 | mTextView.setTextAppearance(getContext(), mTabTextAppearance);
1679 | if (mTabTextColors != null) {
1680 | mTextView.setTextColor(mTabTextColors);
1681 | }
1682 | updateTextAndIcon(mTextView, mIconView);
1683 | } else {
1684 | // Else, we'll see if there is a TextView or ImageView present and update them
1685 | if (mCustomTextView != null || mCustomIconView != null) {
1686 | updateTextAndIcon(mCustomTextView, mCustomIconView);
1687 | }
1688 | }
1689 | }
1690 |
1691 | private void updateTextAndIcon(@Nullable final TextView textView,
1692 | @Nullable final ImageView iconView) {
1693 | final Drawable icon = mTab != null ? mTab.getIcon() : null;
1694 | final CharSequence text = mTab != null ? mTab.getText() : null;
1695 | final CharSequence contentDesc = mTab != null ? mTab.getContentDescription() : null;
1696 |
1697 | if (iconView != null) {
1698 | if (icon != null) {
1699 | iconView.setImageDrawable(icon);
1700 | iconView.setVisibility(VISIBLE);
1701 | setVisibility(VISIBLE);
1702 | } else {
1703 | iconView.setVisibility(GONE);
1704 | iconView.setImageDrawable(null);
1705 | }
1706 | iconView.setContentDescription(contentDesc);
1707 | }
1708 |
1709 | final boolean hasText = !TextUtils.isEmpty(text);
1710 | if (textView != null) {
1711 | if (hasText) {
1712 | textView.setAllCaps(xTabTextAllCaps);
1713 | textView.setText(text);
1714 | textView.setVisibility(VISIBLE);
1715 | setVisibility(VISIBLE);
1716 | } else {
1717 | textView.setVisibility(GONE);
1718 | textView.setText(null);
1719 | }
1720 | textView.setContentDescription(contentDesc);
1721 | }
1722 |
1723 | if (iconView != null) {
1724 | MarginLayoutParams lp = ((MarginLayoutParams) iconView.getLayoutParams());
1725 | int bottomMargin = 0;
1726 | if (hasText && iconView.getVisibility() == VISIBLE) {
1727 | // If we're showing both text and icon, add some margin bottom to the icon
1728 | bottomMargin = dpToPx(DEFAULT_GAP_TEXT_ICON);
1729 | }
1730 | if (bottomMargin != lp.bottomMargin) {
1731 | lp.bottomMargin = bottomMargin;
1732 | iconView.requestLayout();
1733 | }
1734 | }
1735 |
1736 | if (!hasText && !TextUtils.isEmpty(contentDesc)) {
1737 | setOnLongClickListener(this);
1738 | } else {
1739 | setOnLongClickListener(null);
1740 | setLongClickable(false);
1741 | }
1742 | }
1743 |
1744 | @Override
1745 | public boolean onLongClick(View v) {
1746 | final int[] screenPos = new int[2];
1747 | getLocationOnScreen(screenPos);
1748 |
1749 | final Context context = getContext();
1750 | final int width = getWidth();
1751 | final int height = getHeight();
1752 | final int screenWidth = context.getResources().getDisplayMetrics().widthPixels;
1753 |
1754 | Toast cheatSheet = Toast.makeText(context, mTab.getContentDescription(),
1755 | Toast.LENGTH_SHORT);
1756 | // Show under the tab
1757 | cheatSheet.setGravity(Gravity.TOP | Gravity.CENTER_HORIZONTAL,
1758 | (screenPos[0] + width / 2) - screenWidth / 2, height);
1759 |
1760 | cheatSheet.show();
1761 | return true;
1762 | }
1763 |
1764 | public XTabLayout.Tab getTab() {
1765 | return mTab;
1766 | }
1767 |
1768 | /**
1769 | * Approximates a given lines width with the new provided text size.
1770 | */
1771 | private float approximateLineWidth(Layout layout, int line, float textSize) {
1772 | return layout.getLineWidth(line) * (textSize / layout.getPaint().getTextSize());
1773 | }
1774 | }
1775 |
1776 | private class SlidingTabStrip extends LinearLayout {
1777 | private int mSelectedIndicatorHeight;
1778 | private int mSelectedIndicatorWidth;
1779 | private int mSelectedIndicatorRoundX;
1780 | private int mSelectedIndicatorRoundY;
1781 | private final Paint mSelectedIndicatorPaint;
1782 |
1783 | private int mSelectedPosition = -1;
1784 | private float mSelectionOffset;
1785 |
1786 | private int mIndicatorLeft = -1;
1787 | private int mIndicatorRight = -1;
1788 |
1789 | private ValueAnimatorCompat mIndicatorAnimator;
1790 |
1791 | SlidingTabStrip(Context context) {
1792 | super(context);
1793 | setWillNotDraw(false);
1794 | mSelectedIndicatorPaint = new Paint();
1795 | }
1796 |
1797 | void setSelectedIndicatorColor(int color) {
1798 | if (mSelectedIndicatorPaint.getColor() != color) {
1799 | mSelectedIndicatorPaint.setColor(color);
1800 | ViewCompat.postInvalidateOnAnimation(this);
1801 | }
1802 | }
1803 |
1804 | void setSelectedIndicatorHeight(int height) {
1805 | if (mSelectedIndicatorHeight != height) {
1806 | mSelectedIndicatorHeight = height;
1807 | ViewCompat.postInvalidateOnAnimation(this);
1808 | }
1809 | }
1810 |
1811 | void setmSelectedIndicatorWidth(int width) {
1812 | if (mSelectedIndicatorWidth != width) {
1813 | mSelectedIndicatorWidth = width;
1814 | ViewCompat.postInvalidateOnAnimation(this);
1815 | }
1816 | }
1817 |
1818 | public void setmSelectedIndicatorRoundX(int mSelectedIndicatorRoundX) {
1819 | if (this.mSelectedIndicatorRoundX != mSelectedIndicatorRoundX) {
1820 | this.mSelectedIndicatorRoundX = mSelectedIndicatorRoundX;
1821 | ViewCompat.postInvalidateOnAnimation(this);
1822 | }
1823 | }
1824 |
1825 |
1826 | public void setmSelectedIndicatorRoundY(int mSelectedIndicatorRoundY) {
1827 | if (this.mSelectedIndicatorRoundY != mSelectedIndicatorRoundY) {
1828 | this.mSelectedIndicatorRoundY = mSelectedIndicatorRoundY;
1829 | ViewCompat.postInvalidateOnAnimation(this);
1830 | }
1831 | }
1832 |
1833 | boolean childrenNeedLayout() {
1834 | for (int i = 0, z = getChildCount(); i < z; i++) {
1835 | final View child = getChildAt(i);
1836 | if (child.getWidth() <= 0) {
1837 | return true;
1838 | }
1839 | }
1840 | return false;
1841 | }
1842 |
1843 | void setIndicatorPositionFromTabPosition(int position, float positionOffset) {
1844 | if (mIndicatorAnimator != null && mIndicatorAnimator.isRunning()) {
1845 | mIndicatorAnimator.cancel();
1846 | }
1847 |
1848 | mSelectedPosition = position;
1849 | mSelectionOffset = positionOffset;
1850 | updateIndicatorPosition();
1851 | }
1852 |
1853 | float getIndicatorPosition() {
1854 | return mSelectedPosition + mSelectionOffset;
1855 | }
1856 |
1857 | @Override
1858 | protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
1859 | super.onMeasure(widthMeasureSpec, heightMeasureSpec);
1860 |
1861 | if (MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY) {
1862 | // HorizontalScrollView will first measure use with UNSPECIFIED, and then with
1863 | // EXACTLY. Ignore the first call since anything we do will be overwritten anyway
1864 | return;
1865 | }
1866 |
1867 | if (mMode == MODE_FIXED && mTabGravity == GRAVITY_CENTER) {
1868 | final int count = getChildCount();
1869 |
1870 | // First we'll find the widest tab
1871 | int largestTabWidth = 0;
1872 | for (int i = 0, z = count; i < z; i++) {
1873 | View child = getChildAt(i);
1874 | if (child.getVisibility() == VISIBLE) {
1875 | largestTabWidth = Math.max(largestTabWidth, child.getMeasuredWidth());
1876 | }
1877 | }
1878 |
1879 | if (largestTabWidth <= 0) {
1880 | // If we don't have a largest child yet, skip until the next measure pass
1881 | return;
1882 | }
1883 |
1884 | final int gutter = dpToPx(FIXED_WRAP_GUTTER_MIN);
1885 | boolean remeasure = false;
1886 |
1887 | if (largestTabWidth * count <= getMeasuredWidth() - gutter * 2) {
1888 | // If the tabs fit within our width minus gutters, we will set all tabs to have
1889 | // the same width
1890 | for (int i = 0; i < count; i++) {
1891 | final LayoutParams lp =
1892 | (LayoutParams) getChildAt(i).getLayoutParams();
1893 | if (lp.width != largestTabWidth || lp.weight != 0) {
1894 | lp.width = largestTabWidth;
1895 | lp.weight = 0;
1896 | remeasure = true;
1897 | }
1898 | }
1899 | } else {
1900 | // If the tabs will wrap to be larger than the width minus gutters, we need
1901 | // to switch to GRAVITY_FILL
1902 | mTabGravity = GRAVITY_FILL;
1903 | updateTabViews(false);
1904 | remeasure = true;
1905 | }
1906 |
1907 | if (remeasure) {
1908 | // Now re-measure after our changes
1909 | super.onMeasure(widthMeasureSpec, heightMeasureSpec);
1910 | }
1911 | }
1912 | }
1913 |
1914 | @Override
1915 | protected void onLayout(boolean changed, int l, int t, int r, int b) {
1916 | super.onLayout(changed, l, t, r, b);
1917 |
1918 | if (mIndicatorAnimator != null && mIndicatorAnimator.isRunning()) {
1919 | // If we're currently running an animation, lets cancel it and start a
1920 | // new animation with the remaining duration
1921 | mIndicatorAnimator.cancel();
1922 | final long duration = mIndicatorAnimator.getDuration();
1923 |
1924 | animateIndicatorToPosition(mSelectedPosition,
1925 | Math.round((1f - mIndicatorAnimator.getAnimatedFraction()) * duration));
1926 | } else {
1927 | // If we've been layed out, update the indicator position
1928 | updateIndicatorPosition();
1929 | }
1930 | }
1931 |
1932 | private void updateIndicatorPosition() {
1933 | final View selectedTitle = getChildAt(mSelectedPosition);
1934 | int left, right;
1935 | if (selectedTitle != null && selectedTitle.getWidth() > 0) {
1936 | left = selectedTitle.getLeft();
1937 | right = selectedTitle.getRight();
1938 |
1939 | int haftWidth = 0;
1940 | if (mSelectedIndicatorWidth == 0
1941 | && !xTabDividerWidthWidthText) mSelectedIndicatorWidth = maxWidth;
1942 |
1943 | /* int maxWidth = mIndicatorRight - mIndicatorLeft;
1944 | if (maxWidth > mSelectedIndicatorWidth) {
1945 | haftWidth = (maxWidth - mSelectedIndicatorWidth) / 2;
1946 | left += haftWidth;
1947 | right -= haftWidth;
1948 | }*/
1949 | if (mSelectedIndicatorWidth != 0) {
1950 | int maxWidth = mIndicatorRight - mIndicatorLeft;
1951 | if (maxWidth > mSelectedIndicatorWidth) {
1952 | haftWidth = (maxWidth - mSelectedIndicatorWidth) / 2;
1953 | left += haftWidth;
1954 | right -= haftWidth;
1955 | }
1956 | }
1957 |
1958 | if (mSelectionOffset > 0f && mSelectedPosition < getChildCount() - 1) {
1959 | // Draw the selection partway between the tabs
1960 | View nextTitle = getChildAt(mSelectedPosition + 1);
1961 | int nextLeft = nextTitle.getLeft() + haftWidth;
1962 | int nextRight = nextTitle.getRight() - haftWidth;
1963 | left = (int) (mSelectionOffset * nextLeft +
1964 | (1.0f - mSelectionOffset) * left);
1965 | right = (int) (mSelectionOffset * nextRight +
1966 | (1.0f - mSelectionOffset) * right);
1967 | }
1968 | } else {
1969 | left = right = -1;
1970 | }
1971 |
1972 | setIndicatorPosition(left, right);
1973 | }
1974 |
1975 | private void setIndicatorPosition(int left, int right) {
1976 | left = left + mTabPaddingStart;
1977 | right = right - mTabPaddingEnd;
1978 | if (left != mIndicatorLeft || right != mIndicatorRight) {
1979 | // If the indicator's left/right has changed, invalidate
1980 | mIndicatorLeft = left;
1981 | mIndicatorRight = right;
1982 | ViewCompat.postInvalidateOnAnimation(this);
1983 | }
1984 | }
1985 |
1986 | void animateIndicatorToPosition(final int position, int duration) {
1987 |
1988 | if (mIndicatorAnimator != null && mIndicatorAnimator.isRunning()) {
1989 | mIndicatorAnimator.cancel();
1990 | }
1991 |
1992 | final boolean isRtl = ViewCompat.getLayoutDirection(this)
1993 | == ViewCompat.LAYOUT_DIRECTION_RTL;
1994 |
1995 | final View targetView = getChildAt(position);
1996 | if (targetView == null) {
1997 | // If we don't have a view, just update the position now and return
1998 | updateIndicatorPosition();
1999 | return;
2000 | }
2001 |
2002 | final int targetLeft = targetView.getLeft();
2003 | final int targetRight = targetView.getRight();
2004 | final int startLeft;
2005 | final int startRight;
2006 |
2007 | if (Math.abs(position - mSelectedPosition) <= 1) {
2008 | // If the views are adjacent, we'll animate from edge-to-edge
2009 | startLeft = mIndicatorLeft;
2010 | startRight = mIndicatorRight;
2011 | } else {
2012 | // Else, we'll just grow from the nearest edge
2013 | final int offset = dpToPx(MOTION_NON_ADJACENT_OFFSET);
2014 | if (position < mSelectedPosition) {
2015 | // We're going end-to-start
2016 | if (isRtl) {
2017 | startLeft = startRight = targetLeft - offset;
2018 | } else {
2019 | startLeft = startRight = targetRight + offset;
2020 | }
2021 | } else {
2022 | // We're going start-to-end
2023 | if (isRtl) {
2024 | startLeft = startRight = targetRight + offset;
2025 | } else {
2026 | startLeft = startRight = targetLeft - offset;
2027 | }
2028 | }
2029 | }
2030 |
2031 | if (startLeft != targetLeft || startRight != targetRight) {
2032 | ValueAnimatorCompat animator = mIndicatorAnimator = ViewUtils.createAnimator();
2033 | animator.setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR);
2034 | animator.setDuration(duration);
2035 | animator.setFloatValues(0, 1);
2036 | animator.setUpdateListener(new ValueAnimatorCompat.AnimatorUpdateListener() {
2037 | @Override
2038 | public void onAnimationUpdate(ValueAnimatorCompat animator) {
2039 | final float fraction = animator.getAnimatedFraction();
2040 | setIndicatorPosition(
2041 | AnimationUtils.lerp(startLeft, targetLeft, fraction),
2042 | AnimationUtils.lerp(startRight, targetRight, fraction));
2043 | }
2044 | });
2045 | animator.setListener(new ValueAnimatorCompat.AnimatorListenerAdapter() {
2046 | @Override
2047 | public void onAnimationEnd(ValueAnimatorCompat animator) {
2048 | mSelectedPosition = position;
2049 | mSelectionOffset = 0f;
2050 | }
2051 | });
2052 | animator.start();
2053 | }
2054 | }
2055 |
2056 | @Override
2057 | public void draw(Canvas canvas) {
2058 | super.draw(canvas);
2059 |
2060 | // Thick colored underline below the current selection
2061 | if (mIndicatorLeft >= 0 && mIndicatorRight > mIndicatorLeft) {
2062 |
2063 | /* int maxWidth = mIndicatorRight - mIndicatorLeft;
2064 | if (maxWidth > mSelectedIndicatorWidth) {
2065 | mIndicatorLeft += (maxWidth - mSelectedIndicatorWidth) / 2;
2066 | mIndicatorRight -= (maxWidth - mSelectedIndicatorWidth) / 2;
2067 | }*/
2068 | if (mSelectedIndicatorWidth != 0 && !xTabDividerWidthWidthText) {
2069 | int maxWidth = mIndicatorRight - mIndicatorLeft;
2070 | if (maxWidth > mSelectedIndicatorWidth) {
2071 | mIndicatorLeft += (maxWidth - mSelectedIndicatorWidth) / 2;
2072 | mIndicatorRight -= (maxWidth - mSelectedIndicatorWidth) / 2;
2073 | }
2074 | } else {
2075 | int maxWidth = mIndicatorRight - mIndicatorLeft;
2076 | if (maxWidth > mSelectedTab.getTextWidth()) {
2077 | mIndicatorLeft += (maxWidth - mSelectedTab.getTextWidth()) / 2;
2078 | mIndicatorRight -= (maxWidth - mSelectedTab.getTextWidth()) / 2;
2079 | }
2080 | }
2081 | //绘制指示器
2082 | RectF rect = new RectF(mIndicatorLeft, getHeight() - mSelectedIndicatorHeight,
2083 | mIndicatorRight, getHeight());
2084 | int roundX = 0;
2085 | int roundY = 0;
2086 | if (mSelectedIndicatorRoundX > 0) {
2087 | roundX = dpToPx(mSelectedIndicatorRoundX);
2088 | }
2089 | if (mSelectedIndicatorRoundY > 0) {
2090 | roundY = dpToPx(mSelectedIndicatorRoundY);
2091 | }
2092 | canvas.drawRoundRect(rect, roundX, roundY, mSelectedIndicatorPaint);
2093 | // canvas.drawRect(mIndicatorLeft, getHeight() - mSelectedIndicatorHeight,
2094 | // mIndicatorRight, getHeight(), mSelectedIndicatorPaint);
2095 | }
2096 | }
2097 | }
2098 |
2099 | private static ColorStateList createColorStateList(int defaultColor, int selectedColor) {
2100 | final int[][] states = new int[2][];
2101 | final int[] colors = new int[2];
2102 | int i = 0;
2103 |
2104 | states[i] = SELECTED_STATE_SET;
2105 | colors[i] = selectedColor;
2106 | i++;
2107 |
2108 | // Default enabled state
2109 | states[i] = EMPTY_STATE_SET;
2110 | colors[i] = defaultColor;
2111 | i++;
2112 |
2113 | return new ColorStateList(states, colors);
2114 | }
2115 |
2116 | private int getDefaultHeight() {
2117 | boolean hasIconAndText = false;
2118 | for (int i = 0, count = mTabs.size(); i < count; i++) {
2119 | XTabLayout.Tab tab = mTabs.get(i);
2120 | if (tab != null && tab.getIcon() != null && !TextUtils.isEmpty(tab.getText())) {
2121 | hasIconAndText = true;
2122 | break;
2123 | }
2124 | }
2125 | return hasIconAndText ? DEFAULT_HEIGHT_WITH_TEXT_ICON : DEFAULT_HEIGHT;
2126 | }
2127 |
2128 | private int getTabMinWidth() {
2129 | if (mPagerAdapter != null && xTabDisplayNum != 0) {
2130 | WindowManager wm = (WindowManager) getContext()
2131 | .getSystemService(Context.WINDOW_SERVICE);
2132 | if (mPagerAdapter.getCount() == 1 || xTabDisplayNum == 1) {
2133 | return wm.getDefaultDisplay().getWidth();
2134 | } else if (mPagerAdapter.getCount() < xTabDisplayNum) {
2135 | return wm.getDefaultDisplay().getWidth() / mPagerAdapter.getCount();
2136 | } else {
2137 | return wm.getDefaultDisplay().getWidth() / xTabDisplayNum;
2138 | }
2139 | }
2140 | if (xTabDisplayNum != 0) {
2141 | WindowManager wm = (WindowManager) getContext()
2142 | .getSystemService(Context.WINDOW_SERVICE);
2143 | return wm.getDefaultDisplay().getWidth() / xTabDisplayNum;
2144 | }
2145 | if (mRequestedTabMinWidth != INVALID_WIDTH) {
2146 | // If we have been given a min width, use it
2147 | //默认再加上一点边距
2148 | return mRequestedTabMinWidth;
2149 | }
2150 | // Else, we'll use the default value
2151 | return mMode == MODE_SCROLLABLE ? mScrollableTabMinWidth : 0;
2152 | }
2153 |
2154 | @Override
2155 | public LayoutParams generateLayoutParams(AttributeSet attrs) {
2156 | // We don't care about the layout params of any views added to us, since we don't actually
2157 | // add them. The only view we add is the SlidingTabStrip, which is done manually.
2158 | // We return the default layout params so that we don't blow up if we're given a TabItem
2159 | // without android:layout_* values.
2160 | return generateDefaultLayoutParams();
2161 | }
2162 |
2163 | private int getTabMaxWidth() {
2164 | return mTabMaxWidth;
2165 | }
2166 |
2167 | /**
2168 | * A {@link ViewPager.OnPageChangeListener} class which contains the
2169 | * necessary calls back to the provided {@link TabLayout} so that the tab position is
2170 | * kept in sync.
2171 | *
2172 | *
This class stores the provided TabLayout weakly, meaning that you can use
2173 | * {@link ViewPager#addOnPageChangeListener(ViewPager.OnPageChangeListener)
2174 | * addOnPageChangeListener(OnPageChangeListener)} without removing the listener and
2175 | * not cause a leak.
2176 | */
2177 | public static class TabLayoutOnPageChangeListener implements ViewPager.OnPageChangeListener {
2178 | private final WeakReference mTabLayoutRef;
2179 | private int mPreviousScrollState;
2180 | private int mScrollState;
2181 |
2182 | public TabLayoutOnPageChangeListener(XTabLayout tabLayout) {
2183 | mTabLayoutRef = new WeakReference<>(tabLayout);
2184 | }
2185 |
2186 | @Override
2187 | public void onPageScrollStateChanged(int state) {
2188 | mPreviousScrollState = mScrollState;
2189 | mScrollState = state;
2190 | }
2191 |
2192 | @Override
2193 | public void onPageScrolled(int position, float positionOffset,
2194 | int positionOffsetPixels) {
2195 | final XTabLayout tabLayout = mTabLayoutRef.get();
2196 | if (tabLayout != null) {
2197 | // Only update the text selection if we're not settling, or we are settling after
2198 | // being dragged
2199 | final boolean updateText = mScrollState != SCROLL_STATE_SETTLING ||
2200 | mPreviousScrollState == SCROLL_STATE_DRAGGING;
2201 | // Update the indicator if we're not settling after being idle. This is caused
2202 | // from a setCurrentItem() call and will be handled by an animation from
2203 | // onPageSelected() instead.
2204 | final boolean updateIndicator = !(mScrollState == SCROLL_STATE_SETTLING
2205 | && mPreviousScrollState == SCROLL_STATE_IDLE);
2206 | tabLayout.setScrollPosition(position, positionOffset, updateText, updateIndicator);
2207 | }
2208 | }
2209 |
2210 | @Override
2211 | public void onPageSelected(int position) {
2212 | final XTabLayout tabLayout = mTabLayoutRef.get();
2213 | if (tabLayout != null && tabLayout.getSelectedTabPosition() != position) {
2214 | // Select the tab, only updating the indicator if we're not being dragged/settled
2215 | // (since onPageScrolled will handle that).
2216 | final boolean updateIndicator = mScrollState == SCROLL_STATE_IDLE
2217 | || (mScrollState == SCROLL_STATE_SETTLING
2218 | && mPreviousScrollState == SCROLL_STATE_IDLE);
2219 | tabLayout.selectTab(tabLayout.getTabAt(position), updateIndicator);
2220 | }
2221 | }
2222 |
2223 | private void reset() {
2224 | mPreviousScrollState = mScrollState = SCROLL_STATE_IDLE;
2225 | }
2226 | }
2227 |
2228 | /**
2229 | * A {@link TabLayout.OnTabSelectedListener} class which contains the necessary calls back
2230 | * to the provided {@link ViewPager} so that the tab position is kept in sync.
2231 | */
2232 | public static class ViewPagerOnTabSelectedListener implements XTabLayout.OnTabSelectedListener {
2233 | private final ViewPager mViewPager;
2234 |
2235 | public ViewPagerOnTabSelectedListener(ViewPager viewPager) {
2236 | mViewPager = viewPager;
2237 | }
2238 |
2239 | @Override
2240 | public void onTabSelected(XTabLayout.Tab tab) {
2241 | mViewPager.setCurrentItem(tab.getPosition());
2242 | }
2243 |
2244 | @Override
2245 | public void onTabUnselected(XTabLayout.Tab tab) {
2246 | // No-op
2247 | }
2248 |
2249 | @Override
2250 | public void onTabReselected(XTabLayout.Tab tab) {
2251 | // No-op
2252 | }
2253 | }
2254 |
2255 | private class PagerAdapterObserver extends DataSetObserver {
2256 | @Override
2257 | public void onChanged() {
2258 | populateFromPagerAdapter();
2259 | }
2260 |
2261 | @Override
2262 | public void onInvalidated() {
2263 | populateFromPagerAdapter();
2264 | }
2265 | }
2266 |
2267 | }
2268 |
--------------------------------------------------------------------------------