Skip to content

Commit 58ceeab

Browse files
drchendsn5ft
authored andcommitted
[SnackBar] Handle anchor view properly so no memory leak will happen
The anchor view can be detached even when the snack bar (or any transient bottom bar) is showing. If this situation happens the global layout listener it registers with the anchor view will become not removable due to a bug/intended behavior of Android View's implementation. We need to remove the listener when the anchor view is detached to fix the issue. This CL also refactors the whole implementation of anchor view and consolidates the anchoring/unanchoring logic to improve readability and robustness of it. Resolves #2042 PiperOrigin-RevId: 382603130
1 parent 9ebf1a1 commit 58ceeab

File tree

1 file changed

+93
-25
lines changed

1 file changed

+93
-25
lines changed

lib/java/com/google/android/material/snackbar/BaseTransientBottomBar.java

Lines changed: 93 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@
8383
import com.google.android.material.resources.MaterialResources;
8484
import java.lang.annotation.Retention;
8585
import java.lang.annotation.RetentionPolicy;
86+
import java.lang.ref.WeakReference;
8687
import java.util.ArrayList;
8788
import java.util.List;
8889

@@ -262,19 +263,11 @@ public boolean handleMessage(@NonNull Message message) {
262263

263264
private int duration;
264265
private boolean gestureInsetBottomIgnored;
265-
@Nullable private View anchorView;
266+
267+
@Nullable
268+
private Anchor anchor;
269+
266270
private boolean anchorViewLayoutListenerEnabled = false;
267-
private final OnGlobalLayoutListener anchorViewLayoutListener =
268-
new OnGlobalLayoutListener() {
269-
@Override
270-
public void onGlobalLayout() {
271-
if (!anchorViewLayoutListenerEnabled) {
272-
return;
273-
}
274-
extraBottomMarginAnchorView = calculateBottomMarginForAnchorView();
275-
updateMargins();
276-
}
277-
};
278271

279272
@RequiresApi(VERSION_CODES.Q)
280273
private final Runnable bottomMarginGestureInsetRunnable =
@@ -451,7 +444,7 @@ private void updateMargins() {
451444
}
452445

453446
int extraBottomMargin =
454-
anchorView != null ? extraBottomMarginAnchorView : extraBottomMarginWindowInset;
447+
getAnchorView() != null ? extraBottomMarginAnchorView : extraBottomMarginWindowInset;
455448
MarginLayoutParams marginParams = (MarginLayoutParams) layoutParams;
456449
marginParams.bottomMargin = originalMargins.bottom + extraBottomMargin;
457450
marginParams.leftMargin = originalMargins.left + extraLeftMarginWindowInset;
@@ -566,15 +559,16 @@ public B setAnimationMode(@AnimationMode int animationMode) {
566559
*/
567560
@Nullable
568561
public View getAnchorView() {
569-
return anchorView;
562+
return anchor == null ? null : anchor.getAnchorView();
570563
}
571564

572565
/** Sets the view the {@link BaseTransientBottomBar} should be anchored above. */
573566
@NonNull
574567
public B setAnchorView(@Nullable View anchorView) {
575-
ViewUtils.removeOnGlobalLayoutListener(this.anchorView, anchorViewLayoutListener);
576-
this.anchorView = anchorView;
577-
ViewUtils.addOnGlobalLayoutListener(this.anchorView, anchorViewLayoutListener);
568+
if (this.anchor != null) {
569+
this.anchor.unanchor();
570+
}
571+
this.anchor = anchorView == null ? null : Anchor.anchor(this, anchorView);
578572
return (B) this;
579573
}
580574

@@ -768,8 +762,7 @@ public void run() {
768762
setUpBehavior((CoordinatorLayout.LayoutParams) lp);
769763
}
770764

771-
extraBottomMarginAnchorView = calculateBottomMarginForAnchorView();
772-
updateMargins();
765+
recalculateAndUpdateMargins();
773766

774767
// Set view to INVISIBLE so it doesn't flash on the screen before the inset adjustment is
775768
// handled and the enter animation is started
@@ -861,18 +854,23 @@ public void onDragStateChanged(int state) {
861854
clp.setBehavior(behavior);
862855
// Also set the inset edge so that views can dodge the bar correctly, but only if there is
863856
// no anchor view.
864-
if (anchorView == null) {
857+
if (getAnchorView() == null) {
865858
clp.insetEdge = Gravity.BOTTOM;
866859
}
867860
}
868861

862+
private void recalculateAndUpdateMargins() {
863+
extraBottomMarginAnchorView = calculateBottomMarginForAnchorView();
864+
updateMargins();
865+
}
866+
869867
private int calculateBottomMarginForAnchorView() {
870-
if (anchorView == null) {
868+
if (getAnchorView() == null) {
871869
return 0;
872870
}
873871

874872
int[] anchorViewLocation = new int[2];
875-
anchorView.getLocationOnScreen(anchorViewLocation);
873+
getAnchorView().getLocationOnScreen(anchorViewLocation);
876874
int anchorViewAbsoluteYTop = anchorViewLocation[1];
877875

878876
int[] targetParentLocation = new int[2];
@@ -1096,9 +1094,6 @@ void onViewHidden(int event) {
10961094
}
10971095
}
10981096

1099-
// Reset anchor view and onGlobalLayoutListener so they won't be leaked.
1100-
setAnchorView(null);
1101-
11021097
// Lastly, hide and remove the view from the parent (if attached)
11031098
ViewParent parent = view.getParent();
11041099
if (parent instanceof ViewGroup) {
@@ -1362,4 +1357,77 @@ public void onInterceptTouchEvent(
13621357
}
13631358
}
13641359
}
1360+
1361+
@SuppressWarnings("rawtypes") // Generic type of BaseTransientBottomBar doesn't matter here.
1362+
static class Anchor
1363+
implements android.view.View.OnAttachStateChangeListener, OnGlobalLayoutListener {
1364+
@NonNull
1365+
private final WeakReference<BaseTransientBottomBar> transientBottomBar;
1366+
1367+
@NonNull
1368+
private final WeakReference<View> anchorView;
1369+
1370+
static Anchor anchor(
1371+
@NonNull BaseTransientBottomBar transientBottomBar, @NonNull View anchorView) {
1372+
Anchor anchor = new Anchor(transientBottomBar, anchorView);
1373+
if (ViewCompat.isAttachedToWindow(anchorView)) {
1374+
ViewUtils.addOnGlobalLayoutListener(anchorView, anchor);
1375+
}
1376+
anchorView.addOnAttachStateChangeListener(anchor);
1377+
return anchor;
1378+
}
1379+
1380+
private Anchor(
1381+
@NonNull BaseTransientBottomBar transientBottomBar, @NonNull View anchorView) {
1382+
this.transientBottomBar = new WeakReference<>(transientBottomBar);
1383+
this.anchorView = new WeakReference<>(anchorView);
1384+
}
1385+
1386+
@Override
1387+
public void onViewAttachedToWindow(View anchorView) {
1388+
if (unanchorIfNoTransientBottomBar()) {
1389+
return;
1390+
}
1391+
ViewUtils.addOnGlobalLayoutListener(anchorView, this);
1392+
}
1393+
1394+
@Override
1395+
public void onViewDetachedFromWindow(View anchorView) {
1396+
if (unanchorIfNoTransientBottomBar()) {
1397+
return;
1398+
}
1399+
ViewUtils.removeOnGlobalLayoutListener(anchorView, this);
1400+
}
1401+
1402+
@Override
1403+
public void onGlobalLayout() {
1404+
if (unanchorIfNoTransientBottomBar()
1405+
|| !transientBottomBar.get().anchorViewLayoutListenerEnabled) {
1406+
return;
1407+
}
1408+
transientBottomBar.get().recalculateAndUpdateMargins();
1409+
}
1410+
1411+
@Nullable
1412+
View getAnchorView() {
1413+
return anchorView.get();
1414+
}
1415+
1416+
private boolean unanchorIfNoTransientBottomBar() {
1417+
if (transientBottomBar.get() == null) {
1418+
unanchor();
1419+
return true;
1420+
}
1421+
return false;
1422+
}
1423+
1424+
void unanchor() {
1425+
if (anchorView.get() != null) {
1426+
anchorView.get().removeOnAttachStateChangeListener(this);
1427+
ViewUtils.removeOnGlobalLayoutListener(anchorView.get(), this);
1428+
}
1429+
anchorView.clear();
1430+
transientBottomBar.clear();
1431+
}
1432+
}
13651433
}

0 commit comments

Comments
 (0)