ActionBar最近学习整理之三:焦点控制及菜单项构建

       上一篇博文介绍了ActionBar的风格自定义的相关内容,在小结里引出了关于AB选项菜单焦点控制的话题,目前市场主流的Android手机大多都是触屏了,但是也有一些逆历史潮流而动的手机型号会强势出现,比如某双屏翻盖的“W系列神器”;对于一些有实体方向导航键的设备,AB的焦点控制十分重要了。从Google官方文档一坨坨的API里没有找到让菜单项获取焦点的方法,木有关系,研究下源码看看肿么实现。
 
ActionBar中焦点控制分析
 
      比如,我要完成这样一个目标,在app启动时,让Actionbar上的某个指定选项菜单键获得焦点,默认启动时,焦点框是落在home区域,而我想要如下显示:

ActionBar最近学习整理之三:焦点控制及菜单项构建_第1张图片

                                                       
      让Done菜单项获取焦点,想实现这个目的,先看下Android是怎么实现这个AB布局的,打开HierarchyView工具,关于AB部分的布局如下图所示,   

ActionBar最近学习整理之三:焦点控制及菜单项构建_第2张图片

       每个矩形框表示一个View,上面展示了View的类型、索引号和id,三个颜色球表示的分别是该view的测量、布局和绘画的时间性能,红的最慢,绿的最快,如果你把鼠标放到矩形框上可以看到具体时间,右下角的数字表示该view在父视图的位置,以0开始,这张截图是hierarchyView给出截图的一部分,由于我们只关注AB的布局,content和其它内容先不看。

      在hierarchyView工具界面中点击每个矩形框,会显示与之对应的布局视图,结合第一张图片显示的布局样式,对号入座,不难发现不同的矩形框对应哪个控件,得到了如下结论
     a) ActionBar的基本布局被放置在一个ActionBarContainer中,该Container包括ActionBarView和ActionBarContextView,前者是AB默认显示的基本布局,后者是和ActionMode相关。
     b) ActoinBarView是个水平方向的LinearLayout,从左至右依次ActionBarView$HomeView、标题LinearLayout和ActionMenuView,HomeView在第一篇AB博文中已经提到,就是up箭头和应用图标组合,布局源码是action_bar_home.xml,标题LinearLayout包含主标题和子标题,眼尖的同学发现在标题栏布局中还有个up箭头图标,确实是,第一篇AB博文中提到不同的选项开关控制不同的组件显示,那个up箭头不是仅仅在HomeView区里有,在标题区也有,只是源码控制不让他们同时出现,主要是便于视图控制。
     c) ActionMenuView显示是存储菜单项的地方,每个菜单项都是一个ActionMenuItemView布局,最右边的溢出菜单,是一个OverFlowMenuButton类型按钮。从ActionMenuItemView上的id可以看出,这就是要获取焦点的控件,源码里显示该类型本质上又是一个TextView,而TextView的requestFocus仿佛在向我们招手~
	mHandler.postDelayed(new Runnable() { 
		@Override
		public void run() {
			// TODO Auto-generated method stub
			handle_focus();
		}
	}, 300); 	

	private void handle_focus(){
		Log.i(TAG, "handle_focus begin!");
		int actionMenuViewIndex=-1;
		int actionBarViewIndex=-1;
		int actionBarContainerIndex=-1;

		ViewGroup rView = (ViewGroup) getWindow().getDecorView();
		ViewGroup ll = (ViewGroup)rView.getChildAt(0);	 

		for( int i=0;i<ll.getChildCount();i++){
			if(ll.getChildAt(i) instanceof ActionBarContainer){
				actionBarContainerIndex = i;
				break;
			}	
		}

		ActionBarContainer action_bar = (ActionBarContainer)ll.getChildAt(actionBarContainerIndex);

		if(action_bar!=null){
			for( int i=0;i<action_bar.getChildCount();i++){
				if(action_bar.getChildAt(i) instanceof ActionBarView){
					actionBarViewIndex =i;
					break;
				}
			}

   
			ActionBarView ABview =(ActionBarView) action_bar.getChildAt(actionBarViewIndex);
		
			if(ABview!=null){
				if (DEBUG) Log.d(TAG, "Aview != null + count = " + ABview.getChildCount());
				for( int i=0;i<ABview.getChildCount();i++){
					if (DEBUG) Log.d(TAG, "i = " + i + " " + ABview.getChildAt(i));
					if(ABview.getChildAt(i) instanceof ActionMenuView){
						if (DEBUG) Log.d(TAG, i + "th Aview's child is instanceof ActionMenuView");
						actionMenuViewIndex =i;
						break;
					}
				}
			}
   

			if (DEBUG) Log.d(TAG, "indexOf_ActionBarContainer = " + actionBarContainerIndex +
			"indexOf_ActionBarView = " + actionBarViewIndex +
			"indexOf_ActionmenuView = " + actionMenuViewIndex);   

			if(actionMenuViewIndex!=-1){
				ActionMenuView AmView =(ActionMenuView)ABview.getChildAt(actionMenuViewIndex);
				view_menu_cancel=(ActionMenuItemView)AmView.getChildAt(0);
				view_menu_down = (ActionMenuItemView)AmView.getChildAt(1);
				if (view_menu_down != null) {
					Log.i(TAG, "view_pictrue request focus!");
					view_menu_down.requestFocus();
				}
				if (view_menu_cancel != null) {
					view_menu_cancel.setNextFocusUpId(view_menu_down.getId());			
				}
			}
	 	}
	}
       按照上述思路,根据布局位置进行遍历,得到了最终的view_menu_done,获取焦点,上述代码实现了该过程,有兴趣的同学可以查看log输出,进一步验证HierarchyView中的结论。本文举例是cancel按钮获取焦点方法,AB上其它控件的焦点属性控制,同理可证。
 
ActionBar中的菜单创建流程
 
     上述似乎是从结论得到的方法,先看到了布局,然后得到了方法,那么源码中AB上的视图到底是如何构建的呢,仍然以菜单项为例,简要整理下AB上的菜单创建流程。
                     

 
      上图是我整理的AB菜单项创建序列图,因为不影响阅读水印懒得去了,AB菜单项的构建类似于MVC的控制方式,数据源是用户在onCreateOptionMenu中添加的菜单项,V是在ActionBarView中的addView显示,而C则是适配器MenuPresenter,他在这个过程中类似于ListAdapter在列表构建中的作用,创建ActionMenuItem的视图并绑定数据。下面简要介绍下上述序列图的主要内容。
 
      第一阶段:Window初始化阶段 ( 1.1~1.4)
 
      在窗口被创建并实例化后,Activity会调用窗口的setContentView方法,布局decorView并且为Menu创建坐准备,decorView是整个Window界面最顶层的View,也就是根视图,窗口中包括AB和Content内容,第一阶段任务就是创建根视图及创建菜单实例的过程。DecorView布局文件时screen_action_bar.xml中:  
<com.android.internal.widget.ActionBarOverlayLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/action_bar_overlay_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:splitMotionEvents="false">
    <FrameLayout android:id="@android:id/content"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
    <LinearLayout android:id="@+id/top_action_bar"
                  android:layout_width="match_parent"
                  android:layout_height="wrap_content">
        <com.android.internal.widget.ActionBarContainer android:id="@+id/action_bar_container"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_alignParentTop="true"
            style="?android:attr/actionBarStyle"
            android:gravity="top">
            <com.android.internal.widget.ActionBarView
                android:id="@+id/action_bar"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                style="?android:attr/actionBarStyle" />
            <com.android.internal.widget.ActionBarContextView
                android:id="@+id/action_context_bar"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:visibility="gone"
                style="?android:attr/actionModeStyle" />
        </com.android.internal.widget.ActionBarContainer>
        <ImageView android:src="?android:attr/windowContentOverlay"
                   android:scaleType="fitXY"
                   android:layout_width="match_parent"
                   android:layout_height="wrap_content" />
    </LinearLayout>
    <com.android.internal.widget.ActionBarContainer android:id="@+id/split_action_bar"
                  android:layout_width="match_parent"
                  android:layout_height="wrap_content"
                  style="?android:attr/actionBarSplitStyle"
                  android:visibility="gone"
                  android:gravity="center"/>
</com.android.internal.widget.ActionBarOverlayLayout>
      以上代码取自Master分支,不同版本略有不同。
 
      第二阶段:AB菜单项布局显示(适配器设置)阶段 1.5.1 ~ 1.5.3
 
      ActionBarView会为MenuBuilder对象配置MenuPresenter,MenuBuilder是个实现了Menu接口的构建器,主要作用就是把包含于其中的菜单数据展示出来,听起来类似MenuPresenter,其实有区别,MenuBuilder中存有菜单的内容数据,包括普通菜单项内容,ActionItem菜单项内容、可视的、可扩展的等,以列表的形式贯穿起来;每个菜单项以MenuItemImpl的形式存储于该列表中,MenuBuilder中也同时包括MenuPresenter对象,Presenter的侧重点是以何种形式将菜单项内容展现出来。
     序列图中1.5.1 即时为MenuBuilder添加MenuPresenter对象,第二阶段是从PhoneWindow调用ActionBarView的setMenu函数开始的。
    public void setMenu(Menu menu, MenuPresenter.Callback cb) {
        if (menu == mOptionsMenu) return;

        if (mOptionsMenu != null) {
            mOptionsMenu.removeMenuPresenter(mActionMenuPresenter);
            mOptionsMenu.removeMenuPresenter(mExpandedMenuPresenter);
        }

        MenuBuilder builder = (MenuBuilder) menu;
        mOptionsMenu = builder;
        if (mMenuView != null) {
            final ViewGroup oldParent = (ViewGroup) mMenuView.getParent();
            if (oldParent != null) {
                oldParent.removeView(mMenuView);
            }
        }
        if (mActionMenuPresenter == null) {
            mActionMenuPresenter = new ActionMenuPresenter(mContext);
            mActionMenuPresenter.setCallback(cb);
            mActionMenuPresenter.setId(com.android.internal.R.id.action_menu_presenter);
            mExpandedMenuPresenter = new ExpandedActionViewMenuPresenter();
        }

        ActionMenuView menuView;
        final LayoutParams layoutParams = new LayoutParams(LayoutParams.WRAP_CONTENT,
                LayoutParams.MATCH_PARENT);
        if (!mSplitActionBar) {
            mActionMenuPresenter.setExpandedActionViewsExclusive(
                    getResources().getBoolean(
                    com.android.internal.R.bool.action_bar_expanded_action_views_exclusive));
            configPresenters(builder);
            menuView = (ActionMenuView) mActionMenuPresenter.getMenuView(this);
            final ViewGroup oldParent = (ViewGroup) menuView.getParent();
            if (oldParent != null && oldParent != this) {
                oldParent.removeView(menuView);
            }
            addView(menuView, layoutParams);
        } else {
            mActionMenuPresenter.setExpandedActionViewsExclusive(false);
            // Allow full screen width in split mode.
            mActionMenuPresenter.setWidthLimit(
                    getContext().getResources().getDisplayMetrics().widthPixels, true);
            // No limit to the item count; use whatever will fit.
            mActionMenuPresenter.setItemLimit(Integer.MAX_VALUE);
            // Span the whole width
            layoutParams.width = LayoutParams.MATCH_PARENT;
            configPresenters(builder);
            menuView = (ActionMenuView) mActionMenuPresenter.getMenuView(this);
            if (mSplitView != null) {
                final ViewGroup oldParent = (ViewGroup) menuView.getParent();
                if (oldParent != null && oldParent != mSplitView) {
                    oldParent.removeView(menuView);
                }
                menuView.setVisibility(getAnimatedVisibility());
                mSplitView.addView(menuView, layoutParams);
            } else {
                // We'll add this later if we missed it this time.
                menuView.setLayoutParams(layoutParams);
            }
        }
        mMenuView = menuView;
    } 
       该阶段完成了对菜单视图的配置,ActionMenuPresenter.getMenuView完成具体的菜单项视图创建工作,源代码不难理解但较为繁杂,我画了一张类图来大致表示在该视图配置创阶段的类关系,类图可能会有些UML规范问题,领会精神。
 
        该类图中,左边MenuItemImpl表示每个具体的菜单项数据,包括id、分组、排序、图标等信息,MenuBuilder中的mActionItems就表示一系列被设置成AB菜单项的内容列表。中间的MenuBuilder,是PhoneWindow在setMenu是传进来的构造器参数,该对象左有数据源(mActionItems),右有显示适配器(mPresenters),负责为AB菜单项显示的适配器实际上是个ActionMenuPresenter类型,通过调用该适配器的getMenuView方法获取菜单布局(mMenuItemLayoutRes)和每个菜单项的视图(mItemLayoutRes),适配器通过调用bindItemView来为每个视图布局绑定数据。 
   public void bindItemView(MenuItemImpl item, MenuView.ItemView itemView) {
       itemView.initialize(item, 0);                           //此itemVIew的运行时类型时ActionMenuItemView
       final ActionMenuView menuView = (ActionMenuView) mMenuView;
       ActionMenuItemView actionItemView = (ActionMenuItemView) itemView;
       actionItemView.setItemInvoker(menuView);
   }
   
 
   public void initialize(MenuItemImpl itemData, int menuType) {
       mItemData = itemData;
       setIcon(itemData.getIcon());
       setTitle(itemData.getTitleForItemView(this)); // Title only takes effect if there is no icon
       setId(itemData.getItemId());
 
       setVisibility(itemData.isVisible() ? View.VISIBLE : View.GONE);
       setEnabled(itemData.isEnabled());
   }
       每个AB菜单项都会把itemData绑定到菜单视图中,而itemData的数据,则早在为1.5.1.1 initForMenu时,Presenter就获得了含有itemData的MenuBuilder。
 
       第三阶段 菜单项内容数据的导入阶段 1.6
 
       这个阶段其实就是PhoneWindow通过回调Activity的onCreateOptionMenu函数,将开发者的菜单内容导入到MenuBuilder中,该阶段发生在setMenu之后,源代码如下:  
               if ((cb == null) || !cb.onCreatePanelMenu(st.featureId, st.menu)) {
                   // Ditch the menu created above
                   st.setMenu(null);
                   if (isActionBarMenu && mActionBar != null) {
                       // Don't show it in the action bar either
                       mActionBar.setMenu(null, mActionMenuPresenterCallback);
                   }
                   return false;
               }
       如果在Activity中没有创建AB选项菜单内容,则setMenu的MenuBuilder为空,AB上也就不会显示任何内容。反之,对于在onCreateOptionMenu中使用MenuInflater导入菜单布局文件的情况(通常大家设置菜单的方式),如果菜单布局文件非空,MenuInflater调用inflater函数向目标menu对象填充菜单内容,
 
   public void inflate(int menuRes, Menu menu) {
       XmlResourceParser parser = null;
       try {
           parser = mContext.getResources().getLayout(menuRes);
           AttributeSet attrs = Xml.asAttributeSet(parser);
            
           parseMenu(parser, attrs, menu);  //  xml菜单文件的具体解析过程
       } catch (XmlPullParserException e) {
           throw new InflateException("Error inflating menu XML", e);
       } catch (IOException e) {
           throw new InflateException("Error inflating menu XML", e);
       } finally {
           if (parser != null) parser.close();
       }
   }
       填充内容的menu对象也就是作为MenuBuilder传递给ActionBarView来进行菜单设置(setMenu函数)的MenuBuilder。MenuBuilder拥有了这些菜单项内容后,才能供Presenter调用显示。
 
小结
 
      本文对AB上的基本布局及焦点控制做了分析,以源码为基础,梳理了AB上Menu的构建过程,由于用的是Master分支源码,可能会和已经发布的其它版本有所出入。前三篇关于AB的博文都是关于AB显示选项内容,由于项目原因,AB的整理暂且告一段落,关于AB的其它内容,如Tab、导航模式、ActionMode、ActionProvider等,会在以后的博文中陆续分享。
 
    ~~~版权所有,转载请声明~~~

你可能感兴趣的:(Actionbar,焦点控制,ActionItem,标题栏布局)