Статьи

Android: исследование класса меню

Android предоставляет приличную функциональность для создания собственного запуска обычного меню в стандартной структуре. У них даже есть приличное руководство по их использованию в ваших приложениях. Большинство пользователей будут хорошо знакомы со стандартным меню, видя, как их используют Google Карты, GMail, список контактов и даже фоновое окно по умолчанию.

Как бы утешительно они ни были для ваших пользователей (все, что вам знакомо), для вас и вашей эстетики дизайна — они скучные, скучные и несвежие. Но они должны быть? Это вопрос, который я хотел бы решить. Давайте начнем с изучения того, где, почему и как в меню Android. В частности, давайте выясним, где в коде живет логика для меню, а затем давайте выясним, как мы можем ее изменить.

Стандартная тема

Что является компонентом Android без Темы для его резервного копирования? Конечно, для разных типов меню существуют разные типы тем. Вот два стандартных (фактически три), используемых для меню, взятых из репозитория XML в репозитории git :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
<style name="Theme.IconMenu">
 <!-- Menu/item attributes -->
 <item name="android:itemTextAppearance">@android:style/TextAppearance.Widget.IconMenu.Item</item>
    <item name="android:itemBackground">@android:drawable/menu_selector</item>
    <item name="android:itemIconDisabledAlpha">?android:attr/disabledAlpha</item>
    <item name="android:horizontalDivider">@android:drawable/divider_horizontal_bright</item>
    <item name="android:verticalDivider">@android:drawable/divider_vertical_bright</item>
    <item name="android:windowAnimationStyle">@android:style/Animation.OptionsPanel</item>
    <item name="android:moreIcon">@android:drawable/ic_menu_more</item>
    <item name="android:background">@null</item>
</style>
 
<style name="Theme.ExpandedMenu">
 <!-- Menu/item attributes -->
 <item name="android:itemTextAppearance">?android:attr/textAppearanceLargeInverse</item>
    <item name="android:listViewStyle">@android:style/Widget.ListView.Menu</item>
    <item name="android:windowAnimationStyle">@android:style/Animation.OptionsPanel</item>
    <item name="android:background">@null</item>
</style>

Первая тема — это та, которую вы видите, когда нажимаете кнопку меню на телефоне, а вторая — когда вы нажимаете кнопку «больше» на первой.
И если вы проверите свой манифест XML-файл:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
<?xml version="1.0" encoding="utf-8"?>
      package="com.owein"
      android:versionCode="1"
      android:versionName="1.0">
 <application android:icon="@drawable/icon" android:label="@string/app_name">
 <activity android:name=".SampleApp" android:label="@string/app_name">
 <intent-filter>
 <action android:name="android.intent.action.MAIN" />
 <category android:name="android.intent.category.LAUNCHER" />
     </intent-filter>
     </activity>
    </application>
    <uses-sdk android:minSdkVersion="8" />
</manifest>

Вы видите, что эти темы появляются прямо в части манифеста … Хм, прямо в части … Хм … Погоди, я их нигде не вижу! Где они попадают в приложение? Как я должен преодолеть это ограничение?

Хорошо, время немного поболтать. Наденьте свои шляпы Дика Трейси. Есть место, где темы принимаются, и как только мы узнаем, где они находятся, мы можем выяснить, что нужно сделать, чтобы заменить наши собственные темы.

Представляем IconMenuView и ExpandedMenuView

Можно с уверенностью предположить, что меню, которое вы видите, на самом деле является продуктом нескольких классов; а именно слушатель действий и некоторый потомок класса View. Поскольку в руководстве по Android Dev нет упоминаний о том, как персонализировать orstylizeour Menus, мы должны предположить, что для этого есть причина. Эта причина должна скрываться в чашах «внутреннего» кода Android.

Один ключ к тому, чтобы найти, где тема используется, — это когда создаются представления меню, либо в самом коде представления, либо в объекте, который раздувает представление. Обычно тема (и стиль) извлекаются из класса Context и помещаются в массив TypedArray во время создания представления. К счастью, именно так и происходит в классах: IconMenuView и ExpandedMenuView .

Вот конструктор из конструктора IconMenuView:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
public IconMenuView(Context context, AttributeSet attrs) {
    super(context, attrs);
 
    TypedArray a =
        context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.IconMenuView, 0, 0);
     
   mRowHeight =
   a.getDimensionPixelSize(com.android.internal.R.styleable
      .IconMenuView_rowHeight, 64);
 
    mMaxRows = a.getInt(com.android.internal.R.styleable.IconMenuView_maxRows, 2);
    mMaxItems = a.getInt(com.android.internal.R.styleable.IconMenuView_maxItems, 6);
     
    mMaxItemsPerRow = 
      a.getInt(com.android.internal.R.styleable.IconMenuView_maxItemsPerRow, 3);
 
    mMoreIcon =
      a.getDrawable(com.android.internal.R.styleable.IconMenuView_moreIcon);
    a.recycle();
 
    a = context.obtainStyledAttributes(attrs,
       com.android.internal.R.styleable.MenuView, 0, 0);
     
    mItemBackground =
       a.getDrawable(com.android.internal.R.styleable.MenuView_itemBackground);
    
    mHorizontalDivider =
       a.getDrawable(com.android.internal.R.styleable.MenuView_horizontalDivider);
     
    mHorizontalDividerRects = new ArrayList<Rect>();
    mVerticalDivider = 
       a.getDrawable(com.android.internal.R.styleable.MenuView_verticalDivider);
     
    mVerticalDividerRects = new ArrayList<Rect>();
     
    mAnimations =
       a.getResourceId(com.android.internal.R.styleable.
          MenuView_windowAnimationStyle, 0);
    a.recycle();
 
    if (mHorizontalDivider != null) {
        mHorizontalDividerHeight = mHorizontalDivider.getIntrinsicHeight();
    }
 
    // Make sure to have some height for the divider
    if (mHorizontalDividerHeight == -1){
        mHorizontalDividerHeight = 1;
    }
 
    if (mVerticalDivider != null) {
        mVerticalDividerWidth = mVerticalDivider.getIntrinsicWidth();
 
        // Make sure to have some width for the divider
        if (mVerticalDividerWidth == -1){
            mVerticalDividerWidth = 1;
        }
    }
 
    mLayout = new int[mMaxRows];
 
    // This view will be drawing the dividers
    setWillNotDraw(false);
 
    // This is so we'll receive the MENU key in touch mode
    setFocusableInTouchMode(true);
    // This is so our children can still be arrow-key focused
    setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
}

Как ясно видно, все эти значения там жестко запрограммированы. Там действительно нет способа заменить свой собственный. Вы даже не можете создать IconMenuView, который имеет 4, 5 или 6 строк. Для этого вам нужно создать свой собственный класс, производный от MenuView .

Но представление — это только один из компонентов меню. Каковы другие компоненты и как они работают вместе?

Создание меню

Во внутреннем каталоге меню git-репозитория Android есть ряд классов, связанных с меню, как вы уже догадались. Те из них, которые нас больше всего интересуют, реализуют либо Menu , ContextMenu, либо SubMenu . Почему? Если вы оглянетесь назад на руководство по Меню, под onCreateOptionsMenu Activity они рекомендуют следующий сегмент кода:

1
2
3
4
5
6
@Override
public boolean onCreateOptionsMenu(Menu menu) {
    MenuInflater inflater = getMenuInflater();
    inflater.inflate(R.menu.game_menu, menu);
    return true;
}

Что говорит нам о том, что Activity жестко запрограммирован на подкласс Menu . Если мы хотим даже начать думать о создании нашего собственного меню, нам придется работать с этим интерфейсом.
Этот раздел поста блога называется «Создание меню» по определенной причине. Классы, которые реализуют Menu, это MenuBuilder , ContextMenuBuilder и SubMenuBuilder . MenuBuilder создает представление меню с помощью вызова метода getMenuView :

1
2
3
4
5
6
7
public View getMenuView(int menuType, ViewGroup parent) {
    if (menuType == TYPE_EXPANDED
            && (mMenuTypes[TYPE_ICON] == null || !mMenuTypes[TYPE_ICON].hasMenuView())) {
        getMenuType(TYPE_ICON).getMenuView(parent);
    }
    return (View) getMenuType(menuType).getMenuView(parent);
}

который делегирует метод внутреннего класса ( MenuType :: getMenuView ):

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
MenuView getMenuView(ViewGroup parent) {
    if (LAYOUT_RES_FOR_TYPE[mMenuType] == 0) {
        return null;
    }
 
    synchronized (this) {
        MenuView menuView = mMenuView != null ? mMenuView.get() : null;
        if (menuView == null) {
            menuView = (MenuView) getInflater().inflate(
                    LAYOUT_RES_FOR_TYPE[mMenuType], parent, false);
            menuView.initialize(MenuBuilder.this, mMenuType);
            mMenuView = new WeakReference<MenuView>(menuView);
            if (mFrozenViewStates != null) {
                View view = (View) menuView;
                view.restoreHierarchyState(mFrozenViewStates);
                mFrozenViewStates.remove(view.getId());
            }
        }
        return menuView;
    }
}

который, в свою очередь, делегирует здание View для LayoutInflater . Именно здесь, на этом этапе, жестко запрограммированный файл ресурсов макета передается в MenuView.

LayoutInflator извлекается из вызова функции getInflater () :

01
02
03
04
05
06
07
08
09
10
11
LayoutInflater getInflater() {
    // Create an inflater that uses the given theme for the Views it inflates
    if (mInflater == null) {
        Context wrappedContext =
     new ContextThemeWrapper(mContext, THEME_RES_FOR_TYPE[mMenuType]);
        mInflater = (LayoutInflater) wrappedContext
                         .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    }
 
    return mInflater;
}

ContextThemeWrapper гарантирует, что стандартная тема меню Android заменяет тему действия. Это снова жестко закодировано.

Удивительно, что ContextMenuBuider и SubMenuBuilder не реализуют свои собственные версии. Они полагаются на ту же функциональность, что и MenuBuilder. Каждому из них передается жестко закодированная тема. Там нет никакого способа передать в вашей собственной теме для меню. Чтобы сделать это, нам нужно создать пользовательский конструктор, который получен из Menu . Это начинает звучать как большая работа.

Так что, если LayoutInflator отвечает за создание представления, которое мы видим на наших телефонах, то что это все о MenuInflater и как мы получаем эти кнопки в самом меню?

Меню Инфляция

Итак, вы прочитали немного о ресурсах Меню и создании меню на страницах разработчиков Android, верно? MenuInflater — это то, что заполняет и размещает все MenuItems или SubMenus, которые занимают MenuView. Его работа состоит в том, чтобы проанализировать файл menu.xml и загрузить содержимое этого файла в представление, которое представляется вашему конечному пользователю. Таким образом, он не создает и не назначает тип меню, с которым вы имеете дело. Это происходит непосредственно от самой Активности.

Так, где рождаются меню?

То, что мы хотим знать, — это как отделить биржевую активность от биржевых меню? Для этого нам нужно знать, где находится фактическая ссылка на меню. Пока мы не увидим, где «new» вызывается по ссылке на ContextMenu или OptionsMenu, мы можем написать столько пользовательских меню, сколько захотим, это не будет иметь значения

Просматривая документацию класса Activity для разработчика Android, можно найти ряд методов, связанных с меню, таких как closeContextMenu , onContextMenuClosed , onContextItemSelected , onMenuOpened , onCreateContextMenu и т. Д. Последний из них — больное место, поскольку мы имеем дело не с методом, который работает с меню (допускает полностью настраиваемое поведение), а с методом, который имеет дело исключительно с ContextMenu (который душит наше творчество). Они действительно не » Я не хочу, чтобы мы контролировали эту кнопку меню, пффф.

Так как руководства разработчика Android по действиям в целом или основам приложений не проливают дополнительный свет на ситуацию, мы снова отправляемся в репозиторий git, чтобы найти одну особенно неприятную находку:

1
2
3
public void openContextMenu(View view) {
    view.showContextMenu();
}

Они прикрепили ContextMenu к каждому существующему представлению! Но я отвлекаюсь. Если у меня так много проблем с поиском ContextMenu, возможно, OptionsMenu предоставляет более прямой маршрут:

1
2
3
public void openOptionsMenu() {
    mWindow.openPanel(Window.FEATURE_OPTIONS_PANEL, null);
}

где переменная mWindow установлена ​​в методе attach так:

1
mWindow = PolicyManager.makeNewWindow(this);

и имеет тип Window , интерфейс. Из описания в окне:

Единственная существующая реализация этого абстрактного класса — android.policy.PhoneWindow, которую вы должны создать при создании окна. Со временем этот класс будет подвергнут рефакторингу и добавлен фабричный метод для создания экземпляров Window без знания конкретной реализации.

Я думаю, что PolicyManager — это тот метод фабрики, и мы уже встречались с PhoneWindow в другом сообщении в блоге. Итак, где рождаются меню? Для догадки взгляните на следующий фрагмент кода из PhoneWindow:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
@Override
public boolean showContextMenuForChild(View originalView) {
    // Reuse the context menu builder
    if (mContextMenu == null) {
        mContextMenu = new ContextMenuBuilder(getContext());
        mContextMenu.setCallback(mContextMenuCallback);
    } else {
        mContextMenu.clearAll();
    }
 
    mContextMenuHelper = mContextMenu.show(originalView,
       originalView.getWindowToken());
    return mContextMenuHelper != null;
}

Вывод

Я озаглавил этот пост в блоге «Как реализовать класс пользовательского меню?» без предварительного знания ответа. Единственная причина, по которой я хотел исследовать это, заключалась в том, чтобы создать библиотеку с открытым исходным кодом для чего-то, что я еще не видел, пользовательских меню. Я нашел очень мало в учебнике / блогосфере по этому поводу.

Чтобы ответить на вопрос, вы можете реализовать их так, как хотите, но вы никогда не сможете их использовать. Мой вопрос к инженерам Google. Почему? Почему это так? Почему я не могу передать пользовательскую тему в меню? Почему я не могу выбрать, как их стилизовать? Да, я понимаю, что вы не хотите, чтобы какой-то коварный разработчик взял на себя управление телефоном, отключив действия всех кнопок телефона, кроме кнопки Меню? Ну что ж. Вперед и вверх к следующему питомцу проекта.

Справка: Android: как реализовать класс пользовательского меню? от нашего партнера JCG в Statically Typed .

Статьи по Теме :