Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android之三種Menu的使用與分析

Android之三種Menu的使用與分析

編輯:關於Android編程

以下說明全部針對Android3.0(Api-11)。本指南將介紹三種基本菜單分別是PartA:操作欄(選項菜單OptionMenu)、PartB:上下文操作模式(ActionMode)、PartC:彈出菜單(PopupMenu)。  

PartA:操作欄(選項菜單)——onCreateOptionsMenu()創建的

以屏幕操作項和溢出選項的組合形式呈現選項菜單中的各項。 \ 一、創建 為 Activity 指定選項菜單,重寫 onCreateOptionsMenu()(Fragment 對應 onCreateOptionsMenu() 回調)。啟動 Activity 時會調用 onCreateOptionsMenu()方法,因此可以在該方法中將菜單資源(使用 XML 定義)注入到回調方法的Menu 中。   二、處理響應事件 重寫 onOptionsItemSelected() 方法,方法將傳遞所選中的 MenuItem。您可以通過調用 getItemId() 方法來識別對應item,該方法將返回菜單項的唯一 ID(由菜單資源中的 android:id 屬性定義)。   補充:動態內容菜單內容 當菜單項顯示在操作欄中時,選項菜單被視為始終處於打開狀態。發生事件時,如果您要執行菜單更新,則必須調用 invalidateOptionsMenu() 來請求系統調用 onPrepareOptionsMenu()。在onPrepareOptionsMenu()方法中去通過 menu.add() 等操作修改菜單項。    

PartB:上下文操作模式(ActionMode)

用戶長按某一元素時出現的浮動菜單,此模式在屏幕頂部欄顯示影響所選內容的操作項目,並允許用戶選擇多項,會直接影響對應的內容。上下文操作模式是 ActionMode 的一種系統實現,它將用戶交互的重點轉到執行上下文操作上。 \ 一、為單個視圖創建上下文操作模式 實現 ActionMode.Callback 接口:
回調方法中,您既可以為上下文操作欄指定操作選項(顯示內容),又可以響應操作項目的點擊事件,還可以處理操作模式的其他生命周期事件。
private ActionMode.Callback mActionModeCallback = new ActionMode.Callback() {
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
        MenuInflater inflater = mode.getMenuInflater();
        inflater.inflate(R.menu.context_menu, menu);
        return true;
}
//該方法用於創建Menu視圖
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
        switch (item.getItemId()) {
            case R.id.menu_share:
                shareCurrentItem();
                mode.finish(); 
                return true;
            default:
                return false;
        }
}
//該方法用於對用戶的操作做出相應的反饋
public void onDestroyActionMode(ActionMode mode) {
        mActionMode = null;
}
//及時清除mActionMode引用,一者為了垃圾回收,二者為了後面再次進入上下文操作模式考慮
}



在View的LongClickListener中調用 startActionMode() 啟用上下文操作模式
private ActionMode mActionMode;
someView.setOnLongClickListener(new View.OnLongClickListener() { //someView是一個普通的View控件
    public boolean onLongClick(View view) {
        if (mActionMode == null) { mActionMode = getActivity().startActionMode(mActionModeCallback)};
        //根據情況如果消耗事件則返回true,沒有消耗事件則返回false。
        view.setSelected(true);
        ..............
        return true;
    }
});
  • 在當前Activity或者Application的樣式中對ActionMode的樣式進行設置,一般設置如下
  二、為listView等復雜視圖創建上下文操作模式 實現 AbsListView.MultiChoiceModeListener 接口,並使用 setMultiChoiceModeListener() 為視圖組設置該接口。
偵聽器的回調方法中,您既可以為上下文操作欄指定操作,也可以響應操作項目的點擊事件,還可以處理從 ActionMode.Callback 接口繼承的其他回調。
listView.setMultiChoiceModeListener(new MultiChoiceModeListener() { ......}
使用 CHOICE_MODE_MULTIPLE_MODAL 參數調用 setChoiceMode()。
listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE_MODAL); 注意:RecyclerView並沒有提供setChoiceMode這樣的一個方法。但是要實現上述功能也不難,大體思路可以如下:(Adapter中需要聲明SparseArray markPosition集合; ActionMode actionMode浮動上下文操作欄引用;)
在Adapter的onCreateViewHolder方法中給view注冊一個 View.OnLongClickListener()監聽器,該監聽器的內容會首先檢測當前 mActionMode 值是否為空,即浮動上下文操作欄顯示與否。如果為空則調用mActionMode = getActivity().startActionMode(mActionModeCallback)顯示浮動上下文操作欄。最後不管 mActionMode 值是否為空,都會將當前view對應在Adapter中的position記錄進markPosition集合中,同時調用view.setSelected(true){如果期望View在選中時有特別的顯示效果可以將view的background設置為一個State List類型的Drawable}。
在Adapter的onBindViewHolder方法中首先檢測當前position是否屬於前面的集合中的值,如果不屬於則調用view.setSelected(false),屬於則調用調用view.setSelected(true)。
最後點擊浮動上下文菜單欄的某個按鈕時,將之前的集合元素取出,處理完後清空集合。
補充:如果為獲得更好的用戶體驗,可以在view的onClickListener中檢測actionMode,如果該引用不為空則記錄當前位置Postion進入集合;否則進行跳轉、刪除等操作。  

ActionMode底層分析(分析目的是修改上下文浮動操作欄的返回圖標)

getActivity().startActionMode() @Activity.class
public ActionMode startActionMode(ActionMode.Callback callback) {
        return mWindow.getDecorView().startActionMode(callback);
}
其中mWindow = new PhoneWindow(this);因此我們往下看PhoneWindow的startActionMode方法。 @PhoneWindow.class
public ActionMode startActionMode(ActionMode.Callback callback) {
            if (mActionMode != null) { mActionMode.finish(); }
            final ActionMode.Callback wrappedCallback = new ActionModeCallbackWrapper(callback);
            ActionMode mode = null;
            ...........
            if (mode != null) {
                mActionMode = mode;
            } else {
                if (mActionModeView == null) {//創建ActionModeView
                    if (isFloating()) {
                        mActionModeView = new ActionBarContextView(mContext);//note1
                        mActionModePopup = new PopupWindow(mContext, null,
                                com.android.internal.R.attr.actionModePopupWindowStyle); 
                        mActionModePopup.setWindowLayoutType(
                                WindowManager.LayoutParams.TYPE_APPLICATION);
                        mActionModePopup.setContentView(mActionModeView); //mActionModeView這裡是准備被顯示的View
                        mActionModePopup.setWidth(MATCH_PARENT);

                        TypedValue heightValue = new TypedValue();
                        mContext.getTheme().resolveAttribute(
                                com.android.internal.R.attr.actionBarSize, heightValue, true);
                        final int height = TypedValue.complexToDimensionPixelSize(heightValue.data,
                                mContext.getResources().getDisplayMetrics());
                        mActionModeView.setContentHeight(height);
                        mActionModePopup.setHeight(WRAP_CONTENT);
                        mShowActionModePopup = new Runnable() {
                            public void run() {
                                mActionModePopup.showAtLocation( //note2
                                        mActionModeView.getApplicationWindowToken(),
                                        Gravity.TOP | Gravity.FILL_HORIZONTAL, 0, 0);
                            }
                        };
                    } else {
                        ViewStub stub = (ViewStub) findViewById(
                                com.android.internal.R.id.action_mode_bar_stub);
                        if (stub != null) {
                            mActionModeView = (ActionBarContextView) stub.inflate();
                        }
                    }
                }

                if (mActionModeView != null) { //顯示ActionModeView
                    mActionModeView.killMode();
                    mode = new StandaloneActionMode(getContext(), mActionModeView, wrappedCallback,
                            mActionModePopup == null);
                    if (callback.onCreateActionMode(mode, mode.getMenu())) {//創建菜單到ActionMode中
                        mode.invalidate();
                        mActionModeView.initForMode(mode);//note3
                        mActionModeView.setVisibility(View.VISIBLE);
                        mActionMode = mode;
                        if (mActionModePopup != null) {
                            post(mShowActionModePopup); //交給Handler去執行前面的Runnable異步方法
                        }
                        mActionModeView.sendAccessibilityEvent(
                                AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
                    } else {
                        mActionMode = null;
                    }
                }
            }
            ....
            return mActionMode;
}
------------------------------------------------------------- note1: ActionBarContextView()@ActionBarContextView.class ActionBarContextView(mContext)-->最終調用構造器為:public ActionBarContextView( Context context, null,com.android.internal.R.attr.actionModeStyle, 0) 構造器內部會調用final TypedArray a = context.obtainStyledAttributes( null,R.styleable.ActionMode,com.android.internal.R.attr.actionModeStyle, 0);即從主題中定義的actionModeStyle樣式文件中和主題直接定義的屬性中獲取到如下屬性:   下面這一行是獲取返回按鍵布局的非常關鍵的一行代碼!!!也可以說closeItemLayout屬性定義了整個ActionMode最左邊的布局視圖信息,注意如果要自定義返回按鈕其id必須為@+id/action_mode_close_button。 mCloseItemLayout = a.getResourceId(com.android.internal.R.styleable.ActionMode_closeItemLayout, R.layout.action_mode_close_item);   note2 [email protected]
public void showAtLocation(IBinder token, int gravity, int x, int y) {
        .........
        final WindowManager.LayoutParams p = createPopupLayoutParams(token);
        preparePopup(p);
        .....
        invokePopup(p);
}
將視圖顯示到手機界面上。具體內容講完note3後就會詳細分析。   note3 @ActionBarContextView.class
public void initForMode(final ActionMode mode) {
        if (mClose == null) {
            LayoutInflater inflater = LayoutInflater.from(mContext);
            mClose = inflater.inflate(mCloseItemLayout, this, false);  
            //看到這裡都想哭了,,,,,,,找了半天就是想搞明白那個返回鍵究竟在哪設置的!!!!
            //這裡終於找到了,mCloseItemLayout就是定義了返回鍵的布局文件,它的定義看note1,即ActionBarContextView的構造器。
            addView(mClose);
        } else if (mClose.getParent() == null) {
            addView(mClose);
        }
        View closeButton = mClose.findViewById(R.id.action_mode_close_button);
        closeButton.setOnClickListener(new OnClickListener() {
            public void onClick(View v) {
                mode.finish(); //點擊返回按鈕則銷毀當前ActionBarContextView視圖
            }
        });
        ....
}
------------------------------------------------------------- 最後我們可以對PopupWindow做一個總結:PopupWindow.setContentView(View v); 方法參數是PopupWindow將要具體顯示的內容,而PopupWindow的任務就是在屏幕中合適的位置將該View顯示出來。但是該方法並不會將View顯示出來,需要調用如下兩個方法才能最終顯示出來:showAtLocation(View parent, int gravity, int x, int y)、showAsDropDown(View anchor, int xoff, int yoff)。showAtLocation是在一個特定的位置中顯示視圖,而showAsDropDown則會首先選取指定視圖的左下方或者左上方顯示視圖。showAtLocation()和showAsDropDown()兩者底層顯示過程基本一致,先後調用preparePopup()和 invokePopup()方法,前者對即將顯示的視圖進行初始化操作,後者調用mWindowManager.addView(decorView, p);將視圖顯示出來。 PopupWindow的構造器中有如下的方法:final TypedArray a = context.obtainStyledAttributes( null, R.styleable.PopupWindow, com.android.internal.R.attr.popupWindowStyle,0);因此它從主題中定義的popupWindowStyle樣式文件中和主題直接定義的屬性中獲取到如下屬性:      

PartC:彈出菜單(PopupMenu)

PopupMenu 是錨定到 View 的模態菜單。如果空間足夠,它將顯示在定位視圖左下方,否則顯示在其左上方。適用於提供與特定內容相關的大量操作,或者為命令的另一部分提供選項。不會直接影響對應的內容。 \ 一、實例化PopupMenu及其構造器函數 該函數將提取當前應用的 Context 以及菜單應錨定到的 View。
PopupMenu popup = new PopupMenu(this, v); 二、使用 MenuInflater 將菜單資源擴充到 PopupMenu.getMenu() 返回的 Menu 對象中 MenuInflater inflater = popup.getMenuInflater();
inflater.inflate(R.menu.actions, popup.getMenu());
API 級別 14 及更高版本中,您可以改為使用 PopupMenu.inflate() 三、調用 PopupMenu.show() PopupMenu.show();
這裡就會顯示上圖的菜單選項了 四、處理點擊事件 實現 PopupMenu.OnMenuItemClickListener 接口,並通過調用 setOnMenuItemclickListener() 將其注冊到 PopupMenu 補充1:監聽PopupMenu銷毀 當用戶選擇項目或觸摸菜單以外的區域時,系統即會清除此菜單。 您可使用 PopupMenu.OnDismissListener 偵聽清除事件。 補充2:顯示圖標 PopupMenu默認是不顯示圖標的,而且對外也不提供相應的修改方法,通過反射進行如下修改。
//使用反射,強制顯示菜單圖標
 try {  
            Field field = popupMenu.getClass().getDeclaredField("mPopup");  
            field.setAccessible(true);  
            MenuPopupHelper mHelper = (MenuPopupHelper) field.get(popupMenu);  
            mHelper.setForceShowIcon(true);  
} catch (IllegalAccessException | NoSuchFieldException e) {      e.printStackTrace();   } 

  補充3:下一個完整的用例:
private void showPopupMenu(View v){
        PopupMenu popup = new PopupMenu(this, v);
        MenuInflater inflater = popup.getMenuInflater();
        inflater.inflate(R.menu.popmenu, popup.getMenu());
        try {
            Field field = popup.getClass().getDeclaredField("mPopup");
            field.setAccessible(true);
            MenuPopupHelper mHelper = (MenuPopupHelper) field.get(popup);
            mHelper.setForceShowIcon(true);
        } catch (IllegalAccessException | NoSuchFieldException e) {
            e.printStackTrace();   }
        popup.setOnMenuItemClickListener(new OnPopupMenuItemClickListener(this));
        popup.setGravity(Gravity.RIGHT);
        popup.show();
}
補充4:主題設置(PopupMenu的字體背景等) 在xml文件中 標簽中我們是無法設置背景和字體顏色的,通常情況是通過修改Theame屬性來實現的,具體如下:


注意:上面的ProfileTheme在manifest.xml文件中可以加給某個Activity如:

 

PopupMenu底層分析(繪制流程探究)

顯示流程

android.support.v7.widget.PopupMenu中有一個域——private MenuPopupHelper mPopup;大部分操作都是委托它去執行的。 android.support.v7.view.menu.MenuPopupHelper,中有一個tryShow()方法,該方法負責具體繪制,完成工作有: 該部分完成的工作有對監聽器初始化設置
計算彈出菜單寬度高度——正方形
調用ListPopupWindow域的show()方法 android.support.v7.widget.ListPopupWindow的show方法,完成工作有: 調用int height = buildDropDown()方法——創建android.support.v7.widget.ListPopupWindow.DropDownListViewmDropDownList對象並設置Adapter。
android.support.v7.widget.ListPopupWindow.DropDownListView extends ListViewCompat負責具體視圖的顯示。(ListViewCompat extends ListView,那麼具體的itemView自然是交給對應的Adapter來提供,對於Adapter查看後面的內容)
方法內部調用android.widget.PopupWindow.setContentView(dropDownView);方法將當前View交給PopupWindow顯示到手機上。
設置屏幕外觸摸監聽器
調用android.widget.PopupWindow.showAsDropDown(anchor, xoff, yoff)方法。該方法內部完成工作有:
  • final WindowManager.LayoutParams p = createPopupLayoutParams(anchor.getWindowToken());
  • preparePopup(p);
  • 布局信息初始化
  •  
  • final boolean aboveAnchor = findDropDownPosition(anchor, p, xoff, yoff, gravity);
  • updateAboveAnchor(aboveAnchor);
  • 計算當前彈出菜單應該顯示在當前View的上面還是下面,對背景色進行設置
  •  
  • invokePopup(WindowManager.LayoutParams p)
  • 進行視圖具體的顯示,使用了mWindowManager.addView(PopupDecorView decorView , ViewGroup.LayoutParams params);等方法。PopupDecorView decorView對象的構造利用了傳入的android.support.v7.widget.ListPopupWindow.DropDownListView對象。最後這樣就顯示了。
  MenuPopupHelper中的MenuAdapter的定義 android.support.v7.view.menu.MenuPopupHelper中有一個適配器private class MenuAdapter extends BaseAdapter提供PopupMenu所要顯示的視圖。 其中一個很重要的getView方法如下:
public View getView(int position, View convertView, ViewGroup parent) {
            if (convertView == null) {
                convertView = mInflater.inflate(ITEM_LAYOUT, parent, false); //note1
            }
            MenuView.ItemView itemView = (MenuView.ItemView) convertView;
            if (mForceShowIcon) {
                ((ListMenuItemView) convertView).setForceShowIcon(true); //note2
            }
            itemView.initialize(getItem(position), 0); //note3
            return convertView;
}
1、static final int ITEM_LAYOUT = R.layout.abc_popup_menu_item_layout;該布局文件只有一個Title和SubTitle的布局信息。外面包裹的是一個android.support.v7.view.menu.ListMenuItemView類型(是一個繼承自LinearLayout的類) 2、設置ListMenuItemView的標志位 mPreserveIconSpacing = mForceShowIcon = true。mForceShowIcon域可以通過MenuPopupHelper的setForceShowIcon方法進行設置,默認是false。 3、調用android.support.v7.view.menu.ListMenuItemView的initialize方法對該行視圖進行初始化設置:設置title、icon等。最後返回當前的convertView對象。   MenuPopupHelper中的MenuAdapter的傳遞 android.support.v7.view.menu.MenuPopupHelper在創建android.support.v7.widget.ListPopupWindow對象的時候會將MenuAdapter傳過去。android.support.v7.widget.ListPopupWindow在創建android.support.v7.widget.ListPopupWindow.DropDownListView的時候也會將MenuAdapter傳過去。DropDownListView是一個繼承自ListView的控件,之後就是ListView和Adapter的情況了,該部分可以參考ListView知識。  

關於Style的設置

ListPopupWindow的構造器中有如下方法:
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ListPopupWindow,  R.attr.popupMenuStyle, 0);即從主題中定義的popupMenuStyle樣式文件中和主題直接定義的屬性中獲取到如下屬性:

        
        
        
        
DropDownListView的構造器中有如下方法: final TypedArray a =context.obtainStyledAttributes( attrs, R.styleable.ListView, R.attr.dropDownListViewStyle, 0);即從主題中定義的dropDownListViewStyle樣式文件中和主題直接定義的屬性中獲取到如下屬性:

         
        
        
        
         
        
         
        
         
        
        
        
        
        
    

  PopupWindow的構造器中有如下方法: final TypedArray a = context.obtainStyledAttributes( null, R.styleable.PopupWindow, com.android.internal.R.attr.popupWindowStyle,0);即從主題中定義的popupWindowStyle樣式文件中和主題直接定義的屬性中獲取到如下屬性:  

        
        
        
        
        
        
        
        
        
        
        
        

   
  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved