5858import androidx .annotation .RestrictTo ;
5959import androidx .annotation .StringRes ;
6060import com .google .android .material .animation .AnimationUtils ;
61+ import com .google .android .material .badge .BadgeDrawable ;
62+ import com .google .android .material .badge .BadgeUtils ;
6163import com .google .android .material .internal .ThemeEnforcement ;
6264import com .google .android .material .internal .ViewUtils ;
6365import com .google .android .material .resources .MaterialResources ;
8486import android .view .ViewParent ;
8587import android .view .accessibility .AccessibilityEvent ;
8688import android .view .accessibility .AccessibilityNodeInfo ;
89+ import android .widget .FrameLayout ;
8790import android .widget .HorizontalScrollView ;
8891import android .widget .ImageView ;
8992import android .widget .LinearLayout ;
@@ -232,17 +235,16 @@ public class TabLayout extends HorizontalScrollView {
232235 public @interface Mode {}
233236
234237 /**
235- * If a tab is instantiated with {@link Tab#setText(CharSequence)}, and this mode is set,
236- * the text will be saved and utilized for the content description, but no visible labels will be
237- * created.
238+ * If a tab is instantiated with {@link Tab#setText(CharSequence)}, and this mode is set, the text
239+ * will be saved and utilized for the content description, but no visible labels will be created.
238240 *
239241 * @see Tab#setTabLabelVisibility(int)
240242 */
241243 public static final int TAB_LABEL_VISIBILITY_UNLABELED = 0 ;
242244
243245 /**
244- * This mode is set by default. If a tab is instantiated with {@link
245- * Tab#setText(CharSequence)}, a visible label will be created.
246+ * This mode is set by default. If a tab is instantiated with {@link Tab#setText(CharSequence)}, a
247+ * visible label will be created.
246248 *
247249 * @see Tab#setTabLabelVisibility(int)
248250 */
@@ -272,9 +274,8 @@ public class TabLayout extends HorizontalScrollView {
272274 /** @hide */
273275 @ RestrictTo (LIBRARY_GROUP )
274276 @ IntDef (
275- flag = true ,
276- value = {GRAVITY_FILL , GRAVITY_CENTER }
277- )
277+ flag = true ,
278+ value = {GRAVITY_FILL , GRAVITY_CENTER })
278279 @ Retention (RetentionPolicy .SOURCE )
279280 public @interface TabGravity {}
280281
@@ -331,13 +332,12 @@ public class TabLayout extends HorizontalScrollView {
331332 /** @hide */
332333 @ RestrictTo (LIBRARY_GROUP )
333334 @ IntDef (
334- value = {
335- INDICATOR_GRAVITY_BOTTOM ,
336- INDICATOR_GRAVITY_CENTER ,
337- INDICATOR_GRAVITY_TOP ,
338- INDICATOR_GRAVITY_STRETCH
339- }
340- )
335+ value = {
336+ INDICATOR_GRAVITY_BOTTOM ,
337+ INDICATOR_GRAVITY_CENTER ,
338+ INDICATOR_GRAVITY_TOP ,
339+ INDICATOR_GRAVITY_STRETCH
340+ })
341341 @ Retention (RetentionPolicy .SOURCE )
342342 public @interface TabIndicatorGravity {}
343343
@@ -2001,6 +2001,12 @@ public Tab setIcon(@Nullable Drawable icon) {
20012001 parent .updateTabViews (true );
20022002 }
20032003 updateView ();
2004+ if (BadgeUtils .USE_COMPAT_PARENT
2005+ && view .hasBadgeDrawable ()
2006+ && view .badgeDrawable .isVisible ()) {
2007+ // Invalidate the TabView if icon visibility has changed and a badge is displayed.
2008+ view .invalidate ();
2009+ }
20042010 return this ;
20052011 }
20062012
@@ -2053,6 +2059,34 @@ public Tab setText(@StringRes int resId) {
20532059 return setText (parent .getResources ().getText (resId ));
20542060 }
20552061
2062+ /**
2063+ * Initializes (if needed) and shows a {@link BadgeDrawable}. Creates an instance of
2064+ * BadgeDrawable if none exists. For convenience, also returns the associated instance of
2065+ * BadgeDrawable.
2066+ *
2067+ * @return an instance of BadgeDrawable associated with {@code Tab}.
2068+ */
2069+ public BadgeDrawable showBadge () {
2070+ return view .showBadge ();
2071+ }
2072+
2073+ /**
2074+ * Removes the {@link BadgeDrawable}. Do nothing if none exists. Consider changing the
2075+ * visibility of the {@link BadgeDrawable} if you only want to hide it temporarily.
2076+ */
2077+ public void removeBadge () {
2078+ view .removeBadge ();
2079+ }
2080+
2081+ /**
2082+ * Returns an instance of {@link BadgeDrawable} associated with this tab, null if none was
2083+ * initialized.
2084+ */
2085+ @ Nullable
2086+ public BadgeDrawable getBadge () {
2087+ return view .getBadge ();
2088+ }
2089+
20562090 /**
20572091 * Sets the visibility mode for the Labels in this Tab. The valid input options are:
20582092 *
@@ -2062,8 +2096,8 @@ public Tab setText(@StringRes int resId) {
20622096 * <li>{@link #TAB_LABEL_VISIBILITY_LABELED}: Tabs will appear labeled if text is set.
20632097 * </ul>
20642098 *
2065- * @param mode one of {@link #TAB_LABEL_VISIBILITY_UNLABELED}
2066- * or {@link #TAB_LABEL_VISIBILITY_LABELED}.
2099+ * @param mode one of {@link #TAB_LABEL_VISIBILITY_UNLABELED} or {@link
2100+ * #TAB_LABEL_VISIBILITY_LABELED}.
20672101 * @return The current instance for call chaining.
20682102 */
20692103 public Tab setTabLabelVisibility (@ LabelVisibility int mode ) {
@@ -2072,14 +2106,20 @@ public Tab setTabLabelVisibility(@LabelVisibility int mode) {
20722106 parent .updateTabViews (true );
20732107 }
20742108 this .updateView ();
2109+ if (BadgeUtils .USE_COMPAT_PARENT
2110+ && view .hasBadgeDrawable ()
2111+ && view .badgeDrawable .isVisible ()) {
2112+ // Invalidate the TabView if label visibility has changed and a badge is displayed.
2113+ view .invalidate ();
2114+ }
20752115 return this ;
20762116 }
20772117
20782118 /**
20792119 * Gets the visibility mode for the Labels in this Tab.
20802120 *
2081- * @return the label visibility mode, one of {@link #TAB_LABEL_VISIBILITY_UNLABELED} or
2082- * {@link #TAB_LABEL_VISIBILITY_LABELED}.
2121+ * @return the label visibility mode, one of {@link #TAB_LABEL_VISIBILITY_UNLABELED} or {@link
2122+ * #TAB_LABEL_VISIBILITY_LABELED}.
20832123 * @see #setTabLabelVisibility(int)
20842124 */
20852125 @ LabelVisibility
@@ -2172,6 +2212,8 @@ class TabView extends LinearLayout {
21722212 private Tab tab ;
21732213 private TextView textView ;
21742214 private ImageView iconView ;
2215+ private View badgeAnchorView ;
2216+ private BadgeDrawable badgeDrawable ;
21752217
21762218 private View customView ;
21772219 private TextView customTextView ;
@@ -2438,12 +2480,7 @@ final void update() {
24382480 if (customView == null ) {
24392481 // If there isn't a custom view, we'll us our own in-built layouts
24402482 if (this .iconView == null ) {
2441- ImageView iconView =
2442- (ImageView )
2443- LayoutInflater .from (getContext ())
2444- .inflate (R .layout .design_layout_tab_icon , this , false );
2445- addView (iconView , 0 );
2446- this .iconView = iconView ;
2483+ inflateAndAddDefaultIconView ();
24472484 }
24482485 final Drawable icon =
24492486 (tab != null && tab .getIcon () != null )
@@ -2457,19 +2494,18 @@ final void update() {
24572494 }
24582495
24592496 if (this .textView == null ) {
2460- TextView textView =
2461- (TextView )
2462- LayoutInflater .from (getContext ())
2463- .inflate (R .layout .design_layout_tab_text , this , false );
2464- addView (textView );
2465- this .textView = textView ;
2497+ inflateAndAddDefaultTextView ();
24662498 defaultMaxLines = TextViewCompat .getMaxLines (this .textView );
24672499 }
24682500 TextViewCompat .setTextAppearance (this .textView , tabTextAppearance );
24692501 if (tabTextColors != null ) {
24702502 this .textView .setTextColor (tabTextColors );
24712503 }
24722504 updateTextAndIcon (this .textView , this .iconView );
2505+
2506+ tryUpdateBadgeAnchor ();
2507+ addOnLayoutChangeListener (iconView );
2508+ addOnLayoutChangeListener (textView );
24732509 } else {
24742510 // Else, we'll see if there is a TextView or ImageView present and update them
24752511 if (customTextView != null || customIconView != null ) {
@@ -2486,6 +2522,153 @@ final void update() {
24862522 setSelected (tab != null && tab .isSelected ());
24872523 }
24882524
2525+ private void inflateAndAddDefaultIconView () {
2526+ ViewGroup iconViewParent = this ;
2527+ if (BadgeUtils .USE_COMPAT_PARENT ) {
2528+ iconViewParent = createPreApi18BadgeAnchorRoot ();
2529+ addView (iconViewParent , 0 );
2530+ }
2531+ this .iconView =
2532+ (ImageView )
2533+ LayoutInflater .from (getContext ())
2534+ .inflate (R .layout .design_layout_tab_icon , iconViewParent , false );
2535+ iconViewParent .addView (iconView , 0 );
2536+ }
2537+
2538+ private void inflateAndAddDefaultTextView () {
2539+ ViewGroup textViewParent = this ;
2540+ if (BadgeUtils .USE_COMPAT_PARENT ) {
2541+ textViewParent = createPreApi18BadgeAnchorRoot ();
2542+ addView (textViewParent );
2543+ }
2544+ this .textView =
2545+ (TextView )
2546+ LayoutInflater .from (getContext ())
2547+ .inflate (R .layout .design_layout_tab_text , textViewParent , false );
2548+ textViewParent .addView (textView );
2549+ }
2550+
2551+ private FrameLayout createPreApi18BadgeAnchorRoot () {
2552+ FrameLayout frameLayout = new FrameLayout (getContext ());
2553+ FrameLayout .LayoutParams layoutparams =
2554+ new FrameLayout .LayoutParams (
2555+ ViewGroup .LayoutParams .WRAP_CONTENT , ViewGroup .LayoutParams .WRAP_CONTENT );
2556+ frameLayout .setLayoutParams (layoutparams );
2557+ return frameLayout ;
2558+ }
2559+
2560+ /**
2561+ * Initializes and shows a {@link BadgeDrawable} associated with this view. Does not create a
2562+ * new instance of BadgeDrawable if an instance already exists. For convenience, also returns
2563+ * the associated instance of BadgeDrawable.
2564+ *
2565+ * @return an instance of BadgeDrawable associated with {@code Tab}.
2566+ */
2567+ private BadgeDrawable showBadge () {
2568+ // Creates a new instance if one is not already initialized for this TabView.
2569+ if (badgeDrawable == null ) {
2570+ badgeDrawable = BadgeDrawable .create (getContext ());
2571+ }
2572+ badgeDrawable .setVisible (true );
2573+ tryUpdateBadgeAnchor ();
2574+ return badgeDrawable ;
2575+ }
2576+
2577+ @ Nullable
2578+ private BadgeDrawable getBadge () {
2579+ return badgeDrawable ;
2580+ }
2581+
2582+ private void removeBadge () {
2583+ if (badgeAnchorView != null ) {
2584+ tryRemoveBadgeFromAnchor ();
2585+ }
2586+ badgeDrawable = null ;
2587+ }
2588+
2589+ private void addOnLayoutChangeListener (final View view ) {
2590+ if (view == null ) {
2591+ return ;
2592+ }
2593+ view .addOnLayoutChangeListener (
2594+ new OnLayoutChangeListener () {
2595+ @ Override
2596+ public void onLayoutChange (
2597+ View v ,
2598+ int left ,
2599+ int top ,
2600+ int right ,
2601+ int bottom ,
2602+ int oldLeft ,
2603+ int oldTop ,
2604+ int oldRight ,
2605+ int oldBottom ) {
2606+ if (view .getVisibility () == VISIBLE ) {
2607+ tryUpdateBadgeDrawableBounds (view );
2608+ }
2609+ }
2610+ });
2611+ }
2612+
2613+ private void tryUpdateBadgeAnchor () {
2614+ if (!hasBadgeDrawable ()) {
2615+ return ;
2616+ }
2617+ if (customView != null ) {
2618+ // TODO: Support badging on custom tab views.
2619+ tryRemoveBadgeFromAnchor ();
2620+ } else {
2621+ if (iconView != null && tab .getIcon () != null ) {
2622+ if (badgeAnchorView != iconView ) {
2623+ tryRemoveBadgeFromAnchor ();
2624+ // Anchor badge to icon.
2625+ tryAttachBadgeToAnchor (iconView );
2626+ } else {
2627+ tryUpdateBadgeDrawableBounds (iconView );
2628+ }
2629+ } else if (textView != null
2630+ && tab .getTabLabelVisibility () == TAB_LABEL_VISIBILITY_LABELED ) {
2631+ if (badgeAnchorView != textView ) {
2632+ tryRemoveBadgeFromAnchor ();
2633+ // Anchor badge to label.
2634+ tryAttachBadgeToAnchor (textView );
2635+ } else {
2636+ tryUpdateBadgeDrawableBounds (textView );
2637+ }
2638+ } else {
2639+ tryRemoveBadgeFromAnchor ();
2640+ }
2641+ }
2642+ }
2643+
2644+ private void tryAttachBadgeToAnchor (View anchorView ) {
2645+ if (!hasBadgeDrawable ()) {
2646+ return ;
2647+ }
2648+ if (anchorView != null ) {
2649+ // Avoid clipping a badge if it's displayed.
2650+ setClipChildren (false );
2651+ setClipToPadding (false );
2652+ BadgeUtils .attachBadgeDrawable (
2653+ badgeDrawable , anchorView , getCustomParentForBadge (anchorView ));
2654+ badgeAnchorView = anchorView ;
2655+ }
2656+ }
2657+
2658+ private void tryRemoveBadgeFromAnchor () {
2659+ if (!hasBadgeDrawable ()) {
2660+ return ;
2661+ }
2662+ if (badgeAnchorView != null ) {
2663+ // Clip children / view to padding when no badge is displayed.
2664+ setClipChildren (true );
2665+ setClipToPadding (true );
2666+ BadgeUtils .detachBadgeDrawable (
2667+ badgeDrawable , badgeAnchorView , getCustomParentForBadge (badgeAnchorView ));
2668+ badgeAnchorView = null ;
2669+ }
2670+ }
2671+
24892672 final void updateOrientation () {
24902673 setOrientation (inlineLabel ? HORIZONTAL : VERTICAL );
24912674 if (customTextView != null || customIconView != null ) {
@@ -2560,6 +2743,25 @@ private void updateTextAndIcon(
25602743 TooltipCompat .setTooltipText (this , hasText ? null : contentDesc );
25612744 }
25622745
2746+ private void tryUpdateBadgeDrawableBounds (View anchor ) {
2747+ // Check that this view is the badge's current anchor view.
2748+ if (hasBadgeDrawable () && anchor == badgeAnchorView ) {
2749+ BadgeUtils .setBadgeDrawableBounds (badgeDrawable , anchor , getCustomParentForBadge (anchor ));
2750+ }
2751+ }
2752+
2753+ private boolean hasBadgeDrawable () {
2754+ return badgeDrawable != null ;
2755+ }
2756+
2757+ @ Nullable
2758+ private FrameLayout getCustomParentForBadge (View anchor ) {
2759+ if (anchor != iconView && anchor != textView ) {
2760+ return null ;
2761+ }
2762+ return BadgeUtils .USE_COMPAT_PARENT ? ((FrameLayout ) anchor .getParent ()) : null ;
2763+ }
2764+
25632765 /**
25642766 * Calculates the width of the TabView's content.
25652767 *
0 commit comments