Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit b23976ef authored by Oren Blasberg's avatar Oren Blasberg
Browse files

MenuPopupHelper: Factor out a MenuPopup interface.

Move much of the responsibility into implementations of this interface.
Delegate functionality to it where appropriate.
Provide a standard (non-cascading) implementation for this interface.

This CL should have NO BEHAVIOR CHANGES.

A follow-up CL will provide a cascading implementation, whereby a
config variable will enable submenus to open side by side with their
parent menus. That CL will be the first with functional/ actual behavior
changes.

Bug: 20127825
Change-Id: Iecac2d340dd8750ebe4e99162d447c9411f09227
parent 008d6d03
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -25,7 +25,7 @@ import android.view.ViewParent;
import com.android.internal.view.menu.ShowableListMenu;

/**
 * Abstract class that forwards touch events to a {@link ListPopupWindow}.
 * Abstract class that forwards touch events to a {@link ShowableListMenu}.
 *
 * @hide
 */
+122 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2015 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.internal.view.menu;

import android.content.Context;
import android.view.View;
import android.view.View.MeasureSpec;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.ListAdapter;
import android.widget.PopupWindow;

/**
 * Base class for a menu popup abstraction - i.e., some type of menu, housed in a popup window
 * environment.
 *
 * @hide
 */
public abstract class MenuPopup implements ShowableListMenu, MenuPresenter {

    public abstract void setForceShowIcon(boolean forceShow);

    /**
     * Adds the given menu to the popup. If this is the first menu shown it'll be displayed; if it's
     * a submenu it will be displayed adjacent to the most recent menu (if supported by the
     * implementation).
     *
     * @param menu
     */
    public abstract void addMenu(MenuBuilder menu);

    public abstract void setGravity(int dropDownGravity);

    public abstract void setAnchorView(View anchor);

    /**
     * Set a listener to receive a callback when the popup is dismissed.
     *
     * @param listener Listener that will be notified when the popup is dismissed.
     */
    public abstract void setOnDismissListener(PopupWindow.OnDismissListener listener);

    @Override
    public void initForMenu(Context context, MenuBuilder menu) {
        // Don't need to do anything; we added as a presenter in the constructor.
    }

    @Override
    public MenuView getMenuView(ViewGroup root) {
        throw new UnsupportedOperationException("MenuPopups manage their own views");
    }

    @Override
    public boolean expandItemActionView(MenuBuilder menu, MenuItemImpl item) {
        return false;
    }

    @Override
    public boolean collapseItemActionView(MenuBuilder menu, MenuItemImpl item) {
        return false;
    }

    @Override
    public int getId() {
        return 0;
    }

    /**
     * Measures the width of the given menu view.
     *
     * @param view The view to measure.
     * @return The width.
     */
    protected static int measureIndividualMenuWidth(ListAdapter adapter, ViewGroup parent,
            Context context, int maxAllowedWidth) {
        // Menus don't tend to be long, so this is more sane than it looks.
        int maxWidth = 0;
        View itemView = null;
        int itemType = 0;

        final int widthMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
        final int heightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
        final int count = adapter.getCount();
        for (int i = 0; i < count; i++) {
            final int positionType = adapter.getItemViewType(i);
            if (positionType != itemType) {
                itemType = positionType;
                itemView = null;
            }

            if (parent == null) {
                parent = new FrameLayout(context);
            }

            itemView = adapter.getView(i, itemView, parent);
            itemView.measure(widthMeasureSpec, heightMeasureSpec);

            final int itemWidth = itemView.getMeasuredWidth();
            if (itemWidth >= maxAllowedWidth) {
                return maxAllowedWidth;
            } else if (itemWidth > maxWidth) {
                maxWidth = itemWidth;
            }
        }

        return maxWidth;
    }
}
 No newline at end of file
+35 −148
Original line number Diff line number Diff line
@@ -17,57 +17,29 @@
package com.android.internal.view.menu;

import android.content.Context;
import android.content.res.Resources;
import android.os.Parcelable;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.View.MeasureSpec;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.FrameLayout;
import android.widget.ListAdapter;
import android.widget.MenuPopupWindow;
import android.widget.PopupWindow;

import java.util.ArrayList;

/**
 * Presents a menu as a small, simple popup anchored to another view.
 *
 * @hide
 */
public class MenuPopupHelper implements AdapterView.OnItemClickListener, View.OnKeyListener,
        ViewTreeObserver.OnGlobalLayoutListener, PopupWindow.OnDismissListener,
        View.OnAttachStateChangeListener, MenuPresenter {
    static final int ITEM_LAYOUT = com.android.internal.R.layout.popup_menu_item_layout;

public class MenuPopupHelper implements ViewTreeObserver.OnGlobalLayoutListener,
        PopupWindow.OnDismissListener, View.OnAttachStateChangeListener, MenuPresenter {
    private final Context mContext;
    private final LayoutInflater mInflater;
    private final MenuBuilder mMenu;
    private final MenuAdapter mAdapter;
    private final boolean mOverflowOnly;
    private final int mPopupMaxWidth;
    private final int mPopupStyleAttr;
    private final int mPopupStyleRes;

    private View mAnchorView;
    private MenuPopupWindow mPopup;
    private MenuPopup mPopup;
    private ViewTreeObserver mTreeObserver;
    private Callback mPresenterCallback;

    boolean mForceShowIcon;

    private ViewGroup mMeasureParent;

    /** Whether the cached content width value is valid. */
    private boolean mHasContentWidth;

    /** Cached content width from {@link #measureContentWidth}. */
    private int mContentWidth;

    private int mDropDownGravity = Gravity.NO_GRAVITY;

@@ -87,33 +59,37 @@ public class MenuPopupHelper implements AdapterView.OnItemClickListener, View.On
    public MenuPopupHelper(Context context, MenuBuilder menu, View anchorView,
            boolean overflowOnly, int popupStyleAttr, int popupStyleRes) {
        mContext = context;
        mInflater = LayoutInflater.from(context);
        mMenu = menu;
        mAdapter = new MenuAdapter(mMenu, mInflater, overflowOnly);
        mOverflowOnly = overflowOnly;
        mPopupStyleAttr = popupStyleAttr;
        mPopupStyleRes = popupStyleRes;

        final Resources res = context.getResources();
        mPopupMaxWidth = Math.max(res.getDisplayMetrics().widthPixels / 2,
                res.getDimensionPixelSize(com.android.internal.R.dimen.config_prefDialogWidth));

        mAnchorView = anchorView;
        mPopup = createMenuPopup();
    }

        // Present the menu using our context, not the menu builder's context.
        menu.addMenuPresenter(this, context);
    private MenuPopup createMenuPopup() {
        if (mContext.getResources().getBoolean(
                com.android.internal.R.bool.config_enableCascadingSubmenus)) {
            // TODO: Return a Cascading implementation of MenuPopup instead.
            return new StandardMenuPopup(
                    mContext, mMenu, mAnchorView, mPopupStyleAttr, mPopupStyleRes, mOverflowOnly);
        }
        return new StandardMenuPopup(
                mContext, mMenu, mAnchorView, mPopupStyleAttr, mPopupStyleRes, mOverflowOnly);
    }

    public void setAnchorView(View anchor) {
        mAnchorView = anchor;
        mPopup.setAnchorView(anchor);
    }

    public void setForceShowIcon(boolean forceShow) {
        mForceShowIcon = forceShow;
        mPopup.setForceShowIcon(forceShow);
    }

    public void setGravity(int gravity) {
        mDropDownGravity = gravity;
        mPopup.setGravity(gravity);
    }

    public int getGravity() {
@@ -126,28 +102,21 @@ public class MenuPopupHelper implements AdapterView.OnItemClickListener, View.On
        }
    }

    public MenuPopupWindow getPopup() {
    public ShowableListMenu getPopup() {
        return mPopup;
    }

    /**
     * Attempts to show the popup anchored to the view specified by
     * {@link #setAnchorView(View)}.
     * Attempts to show the popup anchored to the view specified by {@link #setAnchorView(View)}.
     *
     * @return {@code true} if the popup was shown or was already showing prior
     *         to calling this method, {@code false} otherwise
     * @return {@code true} if the popup was shown or was already showing prior to calling this
     *         method, {@code false} otherwise
     */
    public boolean tryShow() {
        if (isShowing()) {
            return true;
        }

        mPopup = new MenuPopupWindow(mContext, null, mPopupStyleAttr, mPopupStyleRes);
        mPopup.setOnDismissListener(this);
        mPopup.setOnItemClickListener(this);
        mPopup.setAdapter(mAdapter);
        mPopup.setModal(true);

        final View anchor = mAnchorView;
        if (anchor != null) {
            final boolean addGlobalListener = mTreeObserver == null;
@@ -155,20 +124,19 @@ public class MenuPopupHelper implements AdapterView.OnItemClickListener, View.On
            if (addGlobalListener) mTreeObserver.addOnGlobalLayoutListener(this);
            anchor.addOnAttachStateChangeListener(this);
            mPopup.setAnchorView(anchor);
            mPopup.setDropDownGravity(mDropDownGravity);
            mPopup.setGravity(mDropDownGravity);
        } else {
            return false;
        }

        if (!mHasContentWidth) {
            mContentWidth = measureContentWidth();
            mHasContentWidth = true;
        }
        // In order for subclasses of MenuPopupHelper to satisfy the OnDismissedListener interface,
        // we must set the listener to this outer Helper rather than to the inner MenuPopup.
        // Not to worry -- the inner MenuPopup will call our own #onDismiss method after it's done
        // its own handling.
        mPopup.setOnDismissListener(this);

        mPopup.setContentWidth(mContentWidth);
        mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
        mPopup.addMenu(mMenu);
        mPopup.show();
        mPopup.getListView().setOnKeyListener(this);
        return true;
    }

@@ -181,7 +149,6 @@ public class MenuPopupHelper implements AdapterView.OnItemClickListener, View.On
    @Override
    public void onDismiss() {
        mPopup = null;
        mMenu.close();
        if (mTreeObserver != null) {
            if (!mTreeObserver.isAlive()) mTreeObserver = mAnchorView.getViewTreeObserver();
            mTreeObserver.removeGlobalOnLayoutListener(this);
@@ -194,56 +161,6 @@ public class MenuPopupHelper implements AdapterView.OnItemClickListener, View.On
        return mPopup != null && mPopup.isShowing();
    }

    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        MenuAdapter adapter = mAdapter;
        adapter.mAdapterMenu.performItemAction(adapter.getItem(position), 0);
    }

    @Override
    public boolean onKey(View v, int keyCode, KeyEvent event) {
        if (event.getAction() == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_MENU) {
            dismiss();
            return true;
        }
        return false;
    }

    private int measureContentWidth() {
        // Menus don't tend to be long, so this is more sane than it looks.
        int maxWidth = 0;
        View itemView = null;
        int itemType = 0;

        final ListAdapter adapter = mAdapter;
        final int widthMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
        final int heightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
        final int count = adapter.getCount();
        for (int i = 0; i < count; i++) {
            final int positionType = adapter.getItemViewType(i);
            if (positionType != itemType) {
                itemType = positionType;
                itemView = null;
            }

            if (mMeasureParent == null) {
                mMeasureParent = new FrameLayout(mContext);
            }

            itemView = adapter.getView(i, itemView, mMeasureParent);
            itemView.measure(widthMeasureSpec, heightMeasureSpec);

            final int itemWidth = itemView.getMeasuredWidth();
            if (itemWidth >= mPopupMaxWidth) {
                return mPopupMaxWidth;
            } else if (itemWidth > maxWidth) {
                maxWidth = itemWidth;
            }
        }

        return maxWidth;
    }

    @Override
    public void onGlobalLayout() {
        if (isShowing()) {
@@ -282,54 +199,22 @@ public class MenuPopupHelper implements AdapterView.OnItemClickListener, View.On

    @Override
    public void updateMenuView(boolean cleared) {
        mHasContentWidth = false;

        if (mAdapter != null) {
            mAdapter.notifyDataSetChanged();
        }
        mPopup.updateMenuView(cleared);
    }

    @Override
    public void setCallback(Callback cb) {
        mPresenterCallback = cb;
        mPopup.setCallback(cb);
    }

    @Override
    public boolean onSubMenuSelected(SubMenuBuilder subMenu) {
        if (subMenu.hasVisibleItems()) {
            MenuPopupHelper subPopup = new MenuPopupHelper(mContext, subMenu, mAnchorView);
            subPopup.setCallback(mPresenterCallback);

            boolean preserveIconSpacing = false;
            final int count = subMenu.size();
            for (int i = 0; i < count; i++) {
                MenuItem childItem = subMenu.getItem(i);
                if (childItem.isVisible() && childItem.getIcon() != null) {
                    preserveIconSpacing = true;
                    break;
                }
            }
            subPopup.setForceShowIcon(preserveIconSpacing);

            if (subPopup.tryShow()) {
                if (mPresenterCallback != null) {
                    mPresenterCallback.onOpenSubMenu(subMenu);
                }
                return true;
            }
        }
        return false;
        return mPopup.onSubMenuSelected(subMenu);
    }

    @Override
    public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) {
        // Only care about the (sub)menu we're presenting.
        if (menu != mMenu) return;

        dismiss();
        if (mPresenterCallback != null) {
            mPresenterCallback.onCloseMenu(menu, allMenusAreClosing);
        }
        mPopup.onCloseMenu(menu, allMenusAreClosing);
    }

    @Override
@@ -337,10 +222,12 @@ public class MenuPopupHelper implements AdapterView.OnItemClickListener, View.On
        return false;
    }

    @Override
    public boolean expandItemActionView(MenuBuilder menu, MenuItemImpl item) {
        return false;
    }

    @Override
    public boolean collapseItemActionView(MenuBuilder menu, MenuItemImpl item) {
        return false;
    }
+246 −0
Original line number Diff line number Diff line
package com.android.internal.view.menu;

import android.content.Context;
import android.content.res.Resources;
import android.os.Parcelable;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.View.OnKeyListener;
import android.widget.AdapterView;
import android.widget.ListView;
import android.widget.MenuPopupWindow;
import android.widget.PopupWindow;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.PopupWindow.OnDismissListener;

import com.android.internal.util.Preconditions;

/**
 * A standard menu popup in which when a submenu is opened, it replaces its parent menu in the
 * viewport.
 */
final class StandardMenuPopup extends MenuPopup implements OnDismissListener, OnItemClickListener,
        MenuPresenter, OnKeyListener {

    private final Context mContext;
    private final LayoutInflater mInflater;
    private final MenuBuilder mMenu;
    private final MenuAdapter mAdapter;
    private final boolean mOverflowOnly;
    private final int mPopupMaxWidth;
    private final int mPopupStyleAttr;
    private final int mPopupStyleRes;

    private PopupWindow.OnDismissListener mOnDismissListener;

    private View mAnchorView;
    private MenuPopupWindow mPopup;
    private Callback mPresenterCallback;

    private ViewGroup mMeasureParent;

    /** Whether the cached content width value is valid. */
    private boolean mHasContentWidth;

    /** Cached content width. */
    private int mContentWidth;

    private int mDropDownGravity = Gravity.NO_GRAVITY;

    public StandardMenuPopup(Context context, MenuBuilder menu, View anchorView, int popupStyleAttr,
            int popupStyleRes, boolean overflowOnly) {
        mContext = Preconditions.checkNotNull(context);
        mInflater = LayoutInflater.from(context);
        mMenu = menu;
        mOverflowOnly = overflowOnly;
        mAdapter = new MenuAdapter(menu, mInflater, mOverflowOnly);
        mPopupStyleAttr = popupStyleAttr;
        mPopupStyleRes = popupStyleRes;

        final Resources res = context.getResources();
        mPopupMaxWidth = Math.max(res.getDisplayMetrics().widthPixels / 2,
                res.getDimensionPixelSize(com.android.internal.R.dimen.config_prefDialogWidth));

        mAnchorView = anchorView;

        mPopup = new MenuPopupWindow(mContext, null, mPopupStyleAttr, mPopupStyleRes);

        // Present the menu using our context, not the menu builder's context.
        menu.addMenuPresenter(this, context);
    }

    @Override
    public void setForceShowIcon(boolean forceShow) {
        mAdapter.setForceShowIcon(forceShow);
    }

    @Override
    public void setGravity(int gravity) {
        mDropDownGravity = gravity;
    }

    private boolean tryShow() {
        if (isShowing()) {
            return true;
        }

        mPopup.setOnDismissListener(this);
        mPopup.setOnItemClickListener(this);
        mPopup.setAdapter(mAdapter);
        mPopup.setModal(true);

        final View anchor = mAnchorView;
        if (anchor != null) {
            mPopup.setAnchorView(anchor);
            mPopup.setDropDownGravity(mDropDownGravity);
        } else {
            return false;
        }

        if (!mHasContentWidth) {
            mContentWidth = measureIndividualMenuWidth(
                    mAdapter, mMeasureParent, mContext, mPopupMaxWidth);
            mHasContentWidth = true;
        }

        mPopup.setContentWidth(mContentWidth);
        mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
        mPopup.show();
        mPopup.getListView().setOnKeyListener(this);
        return true;
    }

    @Override
    public void show() {
        if (!tryShow()) {
            throw new IllegalStateException("MenuPopupHelper cannot be used without an anchor");
        }
    }

    @Override
    public void dismiss() {
        if (isShowing()) {
            mPopup.dismiss();
        }
    }

    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        MenuAdapter adapter = mAdapter;
        adapter.mAdapterMenu.performItemAction(adapter.getItem(position), 0);
    }

    @Override
    public void addMenu(MenuBuilder menu) {
        // No-op: standard implementation has only one menu which is set in the constructor.
    }

    @Override
    public boolean isShowing() {
        return mPopup != null && mPopup.isShowing();
    }

    @Override
    public void onDismiss() {
        mPopup = null;
        mMenu.close();

        mOnDismissListener.onDismiss();
    }

    @Override
    public void updateMenuView(boolean cleared) {
        mHasContentWidth = false;

        if (mAdapter != null) {
            mAdapter.notifyDataSetChanged();
        }
    }

    @Override
    public void setCallback(Callback cb) {
        mPresenterCallback = cb;
    }

    @Override
    public boolean onSubMenuSelected(SubMenuBuilder subMenu) {
        if (subMenu.hasVisibleItems()) {
            MenuPopupHelper subPopup = new MenuPopupHelper(
                    mContext, subMenu, mAnchorView, mOverflowOnly, mPopupStyleAttr, mPopupStyleRes);
            subPopup.setCallback(mPresenterCallback);

            boolean preserveIconSpacing = false;
            final int count = subMenu.size();
            for (int i = 0; i < count; i++) {
                MenuItem childItem = subMenu.getItem(i);
                if (childItem.isVisible() && childItem.getIcon() != null) {
                    preserveIconSpacing = true;
                    break;
                }
            }
            subPopup.setForceShowIcon(preserveIconSpacing);

            if (subPopup.tryShow()) {
                if (mPresenterCallback != null) {
                    mPresenterCallback.onOpenSubMenu(subMenu);
                }
                return true;
            }
        }
        return false;
    }

    @Override
    public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) {
        // Only care about the (sub)menu we're presenting.
        if (menu != mMenu) return;

        dismiss();
        if (mPresenterCallback != null) {
            mPresenterCallback.onCloseMenu(menu, allMenusAreClosing);
        }
    }

    @Override
    public boolean flagActionItems() {
        return false;
    }


    @Override
    public Parcelable onSaveInstanceState() {
        return null;
    }

    @Override
    public void onRestoreInstanceState(Parcelable state) {
    }

    @Override
    public void setAnchorView(View anchor) {
        mAnchorView = anchor;
    }

    @Override
    public boolean onKey(View v, int keyCode, KeyEvent event) {
        if (event.getAction() == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_MENU) {
            dismiss();
            return true;
        }
        return false;
    }

    @Override
    public void setOnDismissListener(OnDismissListener listener) {
        mOnDismissListener = listener;
    }

    @Override
    public ListView getListView() {
        return mPopup.getListView();
    }
}
 No newline at end of file
+4 −0
Original line number Diff line number Diff line
@@ -2283,4 +2283,8 @@
    <!-- The OEM specified sensor string type for the gesture to launch camera app, this value
         must match the value of config_cameraLaunchGestureSensorType in OEM's HAL -->
    <string translatable="false" name="config_cameraLaunchGestureSensorStringType"></string>

    <!-- Whether to open UI submenus side by side with the top menu (as opposed to
         replacing the top menu). -->
    <bool name="config_enableCascadingSubmenus">false</bool>
</resources>
Loading