Android 入门宝典 – ActionBar 应用

  • Post author:
  • Post category:其他


在某些情况下,操作栏可能会被另一个使用ActionMode启用上下文操作的栏覆盖。例如,当用户在您的活动中选择一个或多个项目时,您可以启用一种操作模式,该模式提供针对所选项目的特定操作,并使用一个UI临时替换操作栏。尽管UI可能占用相同的空间,但ActionMode API与ActionBar的API是不同的且独立的。

https://developer.android.com/reference/android/support/v7/app/ActionBar



上下文操作模式(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 的样式进行设置,一般设置如下

<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
    <!--该项的设置是保证Activity视图顶部不会出现两个上下文操作栏,当前ActionMode将会覆盖在Toolbar上显示-->
    <item name="windowActionModeOverlay">true</item>
    <!--该项设置ActionMode背景、字体、返回按键样式等内容;可以参考一个Android提供的样式拷贝过来然后进行修改;好像直接继承的话,子类对父类的样式修改是无效的-->
    <!--这里也强烈建议对于自己不懂的样式我们可以借鉴别人的设置,稍加修改部分参数,这也是很鼓励的做法-->
    <item name="actionModeStyle">@style/actionModeStyle</item>
</style>

<style name="actionModeStyle" >
    <item name="background">@color/colorPrimary</item>
    <item name="backgroundSplit">?attr/actionModeSplitBackground</item>
    <item name="height">?attr/actionBarSize</item>
    <item name="titleTextStyle">@style/TextAppearance.AppCompat.Widget.ActionMode.Title</item>
    <item name="subtitleTextStyle">@style/TextAppearance.AppCompat.Widget.ActionMode.Subtitle</item>
    <item name="closeItemLayout">@layout/abc_action_mode_close_item_material</item>
</style>

二、为 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 进入集合;否则进行跳转、删除等操作。

原文链接:https://blog.csdn.net/evan_man/article/details/51685022



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样式文件中和主题直接定义的属性中获取到如下属性:

<declare-styleable name="ActionMode">
	<!-- title的样式. -->
	<attr name="titleTextStyle" />
	<!-- subtitle的样式. -->
	<attr name="subtitleTextStyle" />
	<!-- 背景颜色 -->
	<attr name="background" />
	<!-- 拆分操作模式栏背景. -->
	<attr name="backgroundSplit" />
	<!-- 操作栏高度. -->
	<attr name="height" />
	<!-- 在操作栏开始位置的close视图(返回按键)的布局. -->
	<attr name="closeItemLayout" format="reference" />
</declare-styleable>

下面这一行是获取返回按键布局的非常关键的一行代码!!!也可以说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

showAtLocation@PopupWindow.class

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视图
	    }
	});
	...
}

————————————————

原文链接:https://blog.csdn.net/evan_man/article/details/51685022