Add an API to listen for window attach/detach events on a View.
Fix bug 3312949 - inconsistent state in MenuPopupHelper
Change-Id: Ie802ada3f8de4cf71c92fcc7c6abce9ba85e7b75
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 48451ba..eefce06 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -74,6 +74,7 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.WeakHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
/**
* <p>
@@ -2099,6 +2100,11 @@
private ArrayList<OnLayoutChangeListener> mOnLayoutChangeListeners;
/**
+ * Listeners for attach events.
+ */
+ private CopyOnWriteArrayList<OnAttachStateChangeListener> mOnAttachStateChangeListeners;
+
+ /**
* Listener used to dispatch click events.
* This field should be made private, so it is hidden from the SDK.
* {@hide}
@@ -2996,6 +3002,37 @@
}
/**
+ * Add a listener for attach state changes.
+ *
+ * This listener will be called whenever this view is attached or detached
+ * from a window. Remove the listener using
+ * {@link #removeOnAttachStateChangeListener(OnAttachStateChangeListener)}.
+ *
+ * @param listener Listener to attach
+ * @see #removeOnAttachStateChangeListener(OnAttachStateChangeListener)
+ */
+ public void addOnAttachStateChangeListener(OnAttachStateChangeListener listener) {
+ if (mOnAttachStateChangeListeners == null) {
+ mOnAttachStateChangeListeners = new CopyOnWriteArrayList<OnAttachStateChangeListener>();
+ }
+ mOnAttachStateChangeListeners.add(listener);
+ }
+
+ /**
+ * Remove a listener for attach state changes. The listener will receive no further
+ * notification of window attach/detach events.
+ *
+ * @param listener Listener to remove
+ * @see #addOnAttachStateChangeListener(OnAttachStateChangeListener)
+ */
+ public void removeOnAttachStateChangeListener(OnAttachStateChangeListener listener) {
+ if (mOnAttachStateChangeListeners == null) {
+ return;
+ }
+ mOnAttachStateChangeListeners.remove(listener);
+ }
+
+ /**
* Returns the focus-change callback registered for this view.
*
* @return The callback, or null if one is not registered.
@@ -7953,6 +7990,19 @@
}
performCollectViewAttributes(visibility);
onAttachedToWindow();
+
+ final CopyOnWriteArrayList<OnAttachStateChangeListener> listeners =
+ mOnAttachStateChangeListeners;
+ if (listeners != null && listeners.size() > 0) {
+ // NOTE: because of the use of CopyOnWriteArrayList, we *must* use an iterator to
+ // perform the dispatching. The iterator is a safe guard against listeners that
+ // could mutate the list by calling the various add/remove methods. This prevents
+ // the array from being modified while we iterate it.
+ for (OnAttachStateChangeListener listener : listeners) {
+ listener.onViewAttachedToWindow(this);
+ }
+ }
+
int vis = info.mWindowVisibility;
if (vis != GONE) {
onWindowVisibilityChanged(vis);
@@ -7974,6 +8024,18 @@
onDetachedFromWindow();
+ final CopyOnWriteArrayList<OnAttachStateChangeListener> listeners =
+ mOnAttachStateChangeListeners;
+ if (listeners != null && listeners.size() > 0) {
+ // NOTE: because of the use of CopyOnWriteArrayList, we *must* use an iterator to
+ // perform the dispatching. The iterator is a safe guard against listeners that
+ // could mutate the list by calling the various add/remove methods. This prevents
+ // the array from being modified while we iterate it.
+ for (OnAttachStateChangeListener listener : listeners) {
+ listener.onViewDetachedFromWindow(this);
+ }
+ }
+
if ((mPrivateFlags & SCROLL_CONTAINER_ADDED) != 0) {
mAttachInfo.mScrollContainers.remove(this);
mPrivateFlags &= ~SCROLL_CONTAINER_ADDED;
@@ -11767,6 +11829,23 @@
public void onSystemUiVisibilityChange(int visibility);
}
+ /**
+ * Interface definition for a callback to be invoked when this view is attached
+ * or detached from its window.
+ */
+ public interface OnAttachStateChangeListener {
+ /**
+ * Called when the view is attached to a window.
+ * @param v The view that was attached
+ */
+ public void onViewAttachedToWindow(View v);
+ /**
+ * Called when the view is detached from a window.
+ * @param v The view that was detached
+ */
+ public void onViewDetachedFromWindow(View v);
+ }
+
private final class UnsetPressedState implements Runnable {
public void run() {
setPressed(false);
diff --git a/core/java/com/android/internal/view/menu/MenuPopupHelper.java b/core/java/com/android/internal/view/menu/MenuPopupHelper.java
index 6c9e7bb..04a059e 100644
--- a/core/java/com/android/internal/view/menu/MenuPopupHelper.java
+++ b/core/java/com/android/internal/view/menu/MenuPopupHelper.java
@@ -36,14 +36,15 @@
* @hide
*/
public class MenuPopupHelper implements AdapterView.OnItemClickListener, View.OnKeyListener,
- ViewTreeObserver.OnGlobalLayoutListener, PopupWindow.OnDismissListener {
+ ViewTreeObserver.OnGlobalLayoutListener, PopupWindow.OnDismissListener,
+ View.OnAttachStateChangeListener {
private static final String TAG = "MenuPopupHelper";
private Context mContext;
private ListPopupWindow mPopup;
private MenuBuilder mMenu;
private int mPopupMaxWidth;
- private WeakReference<View> mAnchorView;
+ private View mAnchorView;
private boolean mOverflowOnly;
private ViewTreeObserver mTreeObserver;
@@ -66,13 +67,11 @@
final DisplayMetrics metrics = context.getResources().getDisplayMetrics();
mPopupMaxWidth = metrics.widthPixels / 2;
- if (anchorView != null) {
- mAnchorView = new WeakReference<View>(anchorView);
- }
+ mAnchorView = anchorView;
}
public void setAnchorView(View anchor) {
- mAnchorView = new WeakReference<View>(anchor);
+ mAnchorView = anchor;
}
public void show() {
@@ -92,19 +91,19 @@
mPopup.setAdapter(adapter);
mPopup.setModal(true);
- View anchor = mAnchorView != null ? mAnchorView.get() : null;
+ View anchor = mAnchorView;
if (anchor == null && mMenu instanceof SubMenuBuilder) {
SubMenuBuilder subMenu = (SubMenuBuilder) mMenu;
final MenuItemImpl itemImpl = (MenuItemImpl) subMenu.getItem();
anchor = itemImpl.getItemView(MenuBuilder.TYPE_ACTION_BUTTON, null);
- mAnchorView = new WeakReference<View>(anchor);
+ mAnchorView = anchor;
}
if (anchor != null) {
- if (mTreeObserver == null) {
- mTreeObserver = anchor.getViewTreeObserver();
- mTreeObserver.addOnGlobalLayoutListener(this);
- }
+ final boolean addGlobalListener = mTreeObserver == null;
+ mTreeObserver = anchor.getViewTreeObserver(); // Refresh to latest
+ if (addGlobalListener) mTreeObserver.addOnGlobalLayoutListener(this);
+ anchor.addOnAttachStateChangeListener(this);
mPopup.setAnchorView(anchor);
} else {
return false;
@@ -125,10 +124,12 @@
public void onDismiss() {
mPopup = null;
- if (mTreeObserver != null && mTreeObserver.isAlive()) {
+ if (mTreeObserver != null) {
+ if (!mTreeObserver.isAlive()) mTreeObserver = mAnchorView.getViewTreeObserver();
mTreeObserver.removeGlobalOnLayoutListener(this);
+ mTreeObserver = null;
}
- mTreeObserver = null;
+ mAnchorView.removeOnAttachStateChangeListener(this);
}
public boolean isShowing() {
@@ -187,13 +188,8 @@
@Override
public void onGlobalLayout() {
- if (!isShowing()) {
- if (mTreeObserver.isAlive()) {
- mTreeObserver.removeGlobalOnLayoutListener(this);
- }
- mTreeObserver = null;
- } else {
- final View anchor = mAnchorView != null ? mAnchorView.get() : null;
+ if (isShowing()) {
+ final View anchor = mAnchorView;
if (anchor == null || !anchor.isShown()) {
dismiss();
} else if (isShowing()) {
@@ -202,4 +198,17 @@
}
}
}
+
+ @Override
+ public void onViewAttachedToWindow(View v) {
+ }
+
+ @Override
+ public void onViewDetachedFromWindow(View v) {
+ if (mTreeObserver != null) {
+ if (!mTreeObserver.isAlive()) mTreeObserver = v.getViewTreeObserver();
+ mTreeObserver.removeGlobalOnLayoutListener(this);
+ }
+ v.removeOnAttachStateChangeListener(this);
+ }
}