Skip to content

Commit 5fdfd9d

Browse files
wcshidsn5ft
authored andcommitted
Begin integrating BadgeDrawable into TabLayout.
PiperOrigin-RevId: 250348653
1 parent 3e0a88d commit 5fdfd9d

File tree

2 files changed

+244
-31
lines changed

2 files changed

+244
-31
lines changed

lib/java/com/google/android/material/tabs/TabLayout.java

Lines changed: 233 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@
5858
import androidx.annotation.RestrictTo;
5959
import androidx.annotation.StringRes;
6060
import com.google.android.material.animation.AnimationUtils;
61+
import com.google.android.material.badge.BadgeDrawable;
62+
import com.google.android.material.badge.BadgeUtils;
6163
import com.google.android.material.internal.ThemeEnforcement;
6264
import com.google.android.material.internal.ViewUtils;
6365
import com.google.android.material.resources.MaterialResources;
@@ -84,6 +86,7 @@
8486
import android.view.ViewParent;
8587
import android.view.accessibility.AccessibilityEvent;
8688
import android.view.accessibility.AccessibilityNodeInfo;
89+
import android.widget.FrameLayout;
8790
import android.widget.HorizontalScrollView;
8891
import android.widget.ImageView;
8992
import 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

Comments
 (0)