您的位置:首页 > 其它

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

2013-08-25 17:42 218 查看
上一篇博文介绍了ActionBar的风格自定义的相关内容,在小结里引出了关于AB选项菜单焦点控制的话题,目前市场主流的Android手机大多都是触屏了,但是也有一些逆历史潮流而动的手机型号会强势出现,比如某双屏翻盖的“W系列神器”;对于一些有实体方向导航键的设备,AB的焦点控制十分重要了。从Google官方文档一坨坨的API里没有找到让菜单项获取焦点的方法,木有关系,研究下源码看看肿么实现。

ActionBar中焦点控制分析

比如,我要完成这样一个目标,在app启动时,让Actionbar上的某个指定选项菜单键获得焦点,默认启动时,焦点框是落在home区域,而我想要如下显示:



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



每个矩形框表示一个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上的菜单创建流程。
[align=center] [/align]



[align=center] [/align]
[align=left] 上图是我整理的AB菜单项创建序列图,因为不影响阅读水印懒得去了,AB菜单项的构建类似于MVC的控制方式,数据源是用户在onCreateOptionMenu中添加的菜单项,V是在ActionBarView中的addView显示,而C则是适配器MenuPresenter,他在这个过程中类似于ListAdapter在列表构建中的作用,创建ActionMenuItem的视图并绑定数据。下面简要介绍下上述序列图的主要内容。[/align]
[align=left] [/align]
[align=left] 第一阶段:Window初始化阶段 ( 1.1~1.4)[/align]
[align=left] [/align]
[align=left] 在窗口被创建并实例化后,Activity会调用窗口的setContentView方法,布局decorView并且为Menu创建坐准备,decorView是整个Window界面最顶层的View,也就是根视图,窗口中包括AB和Content内容,第一阶段任务就是创建根视图及创建菜单实例的过程。DecorView布局文件时screen_action_bar.xml中: [/align]

<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分支,不同版本略有不同。
[align=left] [/align]
[align=left] 第二阶段:AB菜单项布局显示(适配器设置)阶段 1.5.1 ~ 1.5.3[/align]
[align=left] [/align]
[align=left] ActionBarView会为MenuBuilder对象配置MenuPresenter,MenuBuilder是个实现了Menu接口的构建器,主要作用就是把包含于其中的菜单数据展示出来,听起来类似MenuPresenter,其实有区别,MenuBuilder中存有菜单的内容数据,包括普通菜单项内容,ActionItem菜单项内容、可视的、可扩展的等,以列表的形式贯穿起来;每个菜单项以MenuItemImpl的形式存储于该列表中,MenuBuilder中也同时包括MenuPresenter对象,Presenter的侧重点是以何种形式将菜单项内容展现出来。[/align]
[align=left] 序列图中1.5.1 即时为MenuBuilder添加MenuPresenter对象,第二阶段是从PhoneWindow调用ActionBarView的setMenu函数开始的。[/align]

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规范问题,领会精神。



[align=left] 该类图中,左边MenuItemImpl表示每个具体的菜单项数据,包括id、分组、排序、图标等信息,MenuBuilder中的mActionItems就表示一系列被设置成AB菜单项的内容列表。中间的MenuBuilder,是PhoneWindow在setMenu是传进来的构造器参数,该对象左有数据源(mActionItems),右有显示适配器(mPresenters),负责为AB菜单项显示的适配器实际上是个ActionMenuPresenter类型,通过调用该适配器的getMenuView方法获取菜单布局(mMenuItemLayoutRes)和每个菜单项的视图(mItemLayoutRes),适配器通过调用bindItemView来为每个视图布局绑定数据。 [/align]

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等,会在以后的博文中陆续分享。

~~~版权所有,转载请声明~~~
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐