Skip to content

Commit

Permalink
[SnackBar] Handle anchor view properly so no memory leak will happen
Browse files Browse the repository at this point in the history
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
  • Loading branch information
drchen authored and dsn5ft committed Jul 2, 2021
1 parent 9ebf1a1 commit 58ceeab
Showing 1 changed file with 93 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
import com.google.android.material.resources.MaterialResources;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;

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

private int duration;
private boolean gestureInsetBottomIgnored;
@Nullable private View anchorView;

@Nullable
private Anchor anchor;

private boolean anchorViewLayoutListenerEnabled = false;
private final OnGlobalLayoutListener anchorViewLayoutListener =
new OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
if (!anchorViewLayoutListenerEnabled) {
return;
}
extraBottomMarginAnchorView = calculateBottomMarginForAnchorView();
updateMargins();
}
};

@RequiresApi(VERSION_CODES.Q)
private final Runnable bottomMarginGestureInsetRunnable =
Expand Down Expand Up @@ -451,7 +444,7 @@ private void updateMargins() {
}

int extraBottomMargin =
anchorView != null ? extraBottomMarginAnchorView : extraBottomMarginWindowInset;
getAnchorView() != null ? extraBottomMarginAnchorView : extraBottomMarginWindowInset;
MarginLayoutParams marginParams = (MarginLayoutParams) layoutParams;
marginParams.bottomMargin = originalMargins.bottom + extraBottomMargin;
marginParams.leftMargin = originalMargins.left + extraLeftMarginWindowInset;
Expand Down Expand Up @@ -566,15 +559,16 @@ public B setAnimationMode(@AnimationMode int animationMode) {
*/
@Nullable
public View getAnchorView() {
return anchorView;
return anchor == null ? null : anchor.getAnchorView();
}

/** Sets the view the {@link BaseTransientBottomBar} should be anchored above. */
@NonNull
public B setAnchorView(@Nullable View anchorView) {
ViewUtils.removeOnGlobalLayoutListener(this.anchorView, anchorViewLayoutListener);
this.anchorView = anchorView;
ViewUtils.addOnGlobalLayoutListener(this.anchorView, anchorViewLayoutListener);
if (this.anchor != null) {
this.anchor.unanchor();
}
this.anchor = anchorView == null ? null : Anchor.anchor(this, anchorView);
return (B) this;
}

Expand Down Expand Up @@ -768,8 +762,7 @@ public void run() {
setUpBehavior((CoordinatorLayout.LayoutParams) lp);
}

extraBottomMarginAnchorView = calculateBottomMarginForAnchorView();
updateMargins();
recalculateAndUpdateMargins();

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

private void recalculateAndUpdateMargins() {
extraBottomMarginAnchorView = calculateBottomMarginForAnchorView();
updateMargins();
}

private int calculateBottomMarginForAnchorView() {
if (anchorView == null) {
if (getAnchorView() == null) {
return 0;
}

int[] anchorViewLocation = new int[2];
anchorView.getLocationOnScreen(anchorViewLocation);
getAnchorView().getLocationOnScreen(anchorViewLocation);
int anchorViewAbsoluteYTop = anchorViewLocation[1];

int[] targetParentLocation = new int[2];
Expand Down Expand Up @@ -1096,9 +1094,6 @@ void onViewHidden(int event) {
}
}

// Reset anchor view and onGlobalLayoutListener so they won't be leaked.
setAnchorView(null);

// Lastly, hide and remove the view from the parent (if attached)
ViewParent parent = view.getParent();
if (parent instanceof ViewGroup) {
Expand Down Expand Up @@ -1362,4 +1357,77 @@ public void onInterceptTouchEvent(
}
}
}

@SuppressWarnings("rawtypes") // Generic type of BaseTransientBottomBar doesn't matter here.
static class Anchor
implements android.view.View.OnAttachStateChangeListener, OnGlobalLayoutListener {
@NonNull
private final WeakReference<BaseTransientBottomBar> transientBottomBar;

@NonNull
private final WeakReference<View> anchorView;

static Anchor anchor(
@NonNull BaseTransientBottomBar transientBottomBar, @NonNull View anchorView) {
Anchor anchor = new Anchor(transientBottomBar, anchorView);
if (ViewCompat.isAttachedToWindow(anchorView)) {
ViewUtils.addOnGlobalLayoutListener(anchorView, anchor);
}
anchorView.addOnAttachStateChangeListener(anchor);
return anchor;
}

private Anchor(
@NonNull BaseTransientBottomBar transientBottomBar, @NonNull View anchorView) {
this.transientBottomBar = new WeakReference<>(transientBottomBar);
this.anchorView = new WeakReference<>(anchorView);
}

@Override
public void onViewAttachedToWindow(View anchorView) {
if (unanchorIfNoTransientBottomBar()) {
return;
}
ViewUtils.addOnGlobalLayoutListener(anchorView, this);
}

@Override
public void onViewDetachedFromWindow(View anchorView) {
if (unanchorIfNoTransientBottomBar()) {
return;
}
ViewUtils.removeOnGlobalLayoutListener(anchorView, this);
}

@Override
public void onGlobalLayout() {
if (unanchorIfNoTransientBottomBar()
|| !transientBottomBar.get().anchorViewLayoutListenerEnabled) {
return;
}
transientBottomBar.get().recalculateAndUpdateMargins();
}

@Nullable
View getAnchorView() {
return anchorView.get();
}

private boolean unanchorIfNoTransientBottomBar() {
if (transientBottomBar.get() == null) {
unanchor();
return true;
}
return false;
}

void unanchor() {
if (anchorView.get() != null) {
anchorView.get().removeOnAttachStateChangeListener(this);
ViewUtils.removeOnGlobalLayoutListener(anchorView.get(), this);
}
anchorView.clear();
transientBottomBar.clear();
}
}
}

0 comments on commit 58ceeab

Please sign in to comment.