说明:借鉴了以下两篇博文,并在此基础上添加了一些自己的修改。
https://www.cnblogs.com/mjsn/p/6150824.html
https://blog.csdn.net/tangyeegg/article/details/53000121
ActionBar
需要掌握的Menu知识
ActionBar介绍
- ActionBar是一种新增的导航栏功能,在Andorid3.0之后加入到系统的API中,它表示了用户当前操作界面的位置,并提供了额外的用户动作、界面导航等功能。
- 取代了传统的TitleBar和Menu,在程序运行中一直置于顶部位置。
- 使用ActionBar的好处是,它可以提供一种全局统一的UI界面,使得用户在使用任何一款软件时都懂得该如何操作,并且ActionBar还可以自动适应各种不同大小的屏幕。
ActionBar的功能
用图的方式来讲解它的功能
- <1>是导航图标,显示一个左箭头,用户通过这个箭头可以向上导航。
- <2>是ActionBar的图标。
- <3>是ActionBar的标题。
- <4>是两个action按钮,这里放重要的按钮功能,为用户进行某项操作提供直接的访问。
- <5>溢出列表,或者称为overflow按钮(就是最右边的三个点),在menu菜单设计中遇到过,放不下的action按钮会被置于溢出列表中,是以下拉的形式呈现的。
ActionBar详解
添加ActionBar
- ActionBar的添加非常简单,只需要在AndroidManifest.xml中指定Application或Activity的theme是Theme.Holo或其子类就可以了,在Android 3.0及其更高的版本中,Activity中都默认包含有ActionBar组件。
- 这就导致了活动只能继承Activity,代码如下所示:
public class MainActivity extends Activity
android:theme="@android:style/Theme.Holo">
取消ActionBar
- 如果需要隐藏ActionBar可以在AndroidManifest注册文件中,将Activity的属性中设置主题风格为NoTitleBar
<activity
android:theme="@android:style/Theme.NoTitleBar"
- 还有一种做法,在运行时调用 getActionBar().hide() 方法也可以隐藏ActionBar,调用 show() 方法来显示ActionBar()。
ActionBar actionBar = getActionBar();
actionBar.hide();
- 既然是继承Activity,则隐藏标题栏也可以用如下代码:
requestWindowFeature(Window.FEATURE_NO_TITLE);
- 注意:如果使用一个主题(theme)来移除Activity上的ActionBar,那么窗口将不再有ActionBar,因此运行时也就没有办法来添加ActionBar,调用getActionBar()方法会返回null。
修改ActionBar的图标和标题
- 默认情况下,系统会使用<application>或者<activity>中icon属性指定的图片来作为ActionBar的图标,但是我们可以改变这一默认行为。如果我们想要使用另外一张图片来作为ActionBar的图标,可以在<application>或者<activity>中通过 logo 属性来指定,而标题中的内容使用 label 属性来指定。
- 代码如下:
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:logo="@drawable/head"
android:label="召唤ActionBar吧"
<!-- logo中的图标会覆盖icon中的图标 -->
<!-- <application>中的label不仅会成为标题中的内容,还会成为启动器的内容-->
<!-- 如果<activity>标签再次定义logo和label,则会覆盖<application>标签相应的部分-->
- 效果如下:
添加Action按钮
- ActionBar可以根据程序当前的功能来提供与其相关的Action按钮,这些按钮都会以图标或文字的形式直接显示在ActionBar上。如果按钮过多,ActionBar显示不完,多出的一些按钮可以隐藏在overflow里面。
- 当Activity启动的时候,系统回调用Activity的 onCreateOptionsMenu() 方法来取出所有的Action按钮,我们只需要在这个方法中加载一个menu资源,并把所有的Action按钮都定义在资源文件里面就可以了。
- menu资源文件中的代码如下所示:
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:my_app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/user"
android:icon="@drawable/user"
android:title="用户"
my_app:showAsAction="ifroom" />
<item
android:id="@+id/write"
android:icon="@drawable/write"
android:title="发布"
my_app:showAsAction="ifRoom" />
<item
android:id="@+id/favo"
android:icon="@drawable/favo"
android:title="收藏"
my_app:showAsAction="never" />
</menu>
- 可以看到,这里我们通过三个<item>标签定义了三个Action按钮。<item>标签中又有一些属性,其中id是该Action按钮的唯一标识符,icon用于指定该按钮的图标,title用于指定该按钮可能显示的文字(在图标能显示的情况下,通常不会显示文字),showAsAction则指定了改按钮显示的位置,主要有以下几种值可选:
按钮显示的位置 | 含义 |
---|---|
always | 永远显示在ActionBar中,如果屏幕空间不够则无法显示 |
ifroom | 表示屏幕空间够的情况下显示在ActionBar中,不够的话就显示在overflow中 |
never | 永远显示在overflow中 |
withText | ActionBar尽可能显示文字,如果图标有限且受到ActionBar的空间限制,文字可能显示不全。 |
- 官方建议选择ifroom或者never。
- 接着,重写Activity的onCreateOptionsMenu()方法,代码如下所示:
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.main,menu);
// 解决:app:showAsAction="if_room"无效的问题
MenuItemCompat.setShowAsAction(menu.findItem(R.id.user),MenuItem.SHOW_AS_ACTION_IF_ROOM);
MenuItemCompat.setShowAsAction(menu.findItem(R.id.write),MenuItem.SHOW_AS_ACTION_IF_ROOM);
return true;
}
- 调用MenuInflater的inflate()方法来加载menu资源。
- 效果如下所示:
- 可以看到user和write两个按钮已经在ActionBar中显示出来了,而favo这个按钮由于showAsAction属性设置成never,所以被隐藏到overflow当中,只要点击一下overflow按钮就可以看到它了。
- 这里我们注意到,显示在ActionBar上的按钮都只有一个图标而已,我们在title中指定的文字并没有显示出来。没错,title中的内容通常情况下只会在overflow中显示出来,ActionBar中由于屏幕空间有限,默认是不会显示title内容的。但是出于以下几种因素考虑,即使title中的内容无法显示出来,我们也应该给每个item都指定一个title属性:
- 当ActionBar中的剩余空间不足的时候,如果ActionBar按钮指定的showAsAction属性是ifroom的话,该Action按钮就会显示在overflow当中,此时就只有title能够显示了。
- 如果Action按钮在ActionBar中显示,用户可能通过长按该Action按钮的方式来查看到title的内容。
出现问题app:showAsAction="always"无效
- 在Android Studio中,android:showAsAction="always"标红
- 看提示信息:
- 意思是项目用的是appcompat库(可以查看bundle.grandle文件下的dependencies,经验证的确是androidx.appcompat:appcompat:1.2.0),需要添加
xmlns:app="http://schemas.android.com/apk/res-auto"
,而且修改成app:showAsAction="always"
,如下所示:
- 但是依然无效。
- 查看博文,比较多的解决方式是将app换成自己的自定义名字(官方API举例也是这个意思),比如my_app,就可以解决了。如下所示:
- 按道理说应该解决了,但是仍然没能解决(无奈)。
- 最后只能在onCreateOptionsMenu()方法中添加Action属性,代码如下:
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.main,menu);
// 解决:app:showAsAction="always"无效的问题
MenuItemCompat.setShowAsAction(menu.findItem(R.id.user),MenuItem.SHOW_AS_ACTION_IF_ROOM);
MenuItemCompat.setShowAsAction(menu.findItem(R.id.write),MenuItem.SHOW_AS_ACTION_IF_ROOM);
return true;
}
/*
R.id.item的id
MenuItem.SHOW_AS_ACTION_按钮显示位置(ALWAYS/IF_ROOM/NEVER...)
*/
- 最后成功解决,虽然方法很笨拙。
响应Action按钮的点击事件
- 当用户点击Action按钮的时候,系统会调用Activity的 onOptionsItemSelected() 方法,通过方法传入的MenuItem参数,可以调用它的 getItemId() 方法和menu资源中的id进行比较,从而辨别出用户点击的是哪一个Action按钮。
- 代码如下:
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
switch (item.getItemId()) {
case R.id.user:
Toast.makeText(this, "You cliked 用户", Toast.LENGTH_SHORT).show();
break;
case R.id.write:
Toast.makeText(this, "You cliked 发布", Toast.LENGTH_SHORT).show();
break;
case R.id.favo:
Toast.makeText(this, "You cliked 收藏", Toast.LENGTH_SHORT).show();
break;
default:
break;
}
return true;
}
- 可以看到,我们让每个Action按钮被点击的时候都弹出一个Toast,效果如下所示:
通过ActionBar图标进行导航
- 启用ActionBar图标导航的功能,可以允许用户根据当前应用的位置来在不同界面之间切换。比如,A界面展示了一个列表,点击某一项之后进入了B界面,这时B界面就应该启动ActionBar图标导航功能,这样就可以回到A界面。
- 我们可以调用 setDisplayHomeAsUpEnabled() 方法来启用ActionBar图标导航功能
- 代码如下:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 隐藏ActionBar,方法一
/*ActionBar actionBar = getActionBar();
actionBar.hide();*/
// 隐藏ActionBar,方法二
/*requestWindowFeature(Window.FEATURE_NO_TITLE);*/
/*
这里是通过java代码设置ActionBar的标题内容,相当于<application>或
者<activity>标签里设置的label属性
*/
//setTitle("fhy");
ActionBar actionBar = getActionBar();
actionBar.setDisplayHomeAsUpEnabled(true);
setContentView(R.layout.activity_main);
}
- 效果如下:
- 可以看到,在ActionBar的图标左侧出现了一个 向左的箭头,通常情况下这都表示返回的意思,因此最简单的实现就是在它的点击事件里面加入 finish() 方法就可以了。
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:// 图标导航默认的id就是android.R.id.home
finish();
Toast.makeText(this,"检验效果",Toast.LENGTH_SHORT).show();
break;
...
}
return true;
}
- 重点是图标导航默认的id就是 android.R.id.home,不是R.id.home。
- 现在看上去,ActionBar导航和Back键的功能貌似是一样的。没错,如果我们只是简单地finish了一下,ActionBar导航和Back键的功能是完全一样的,但ActionBar的设计初衷并不是这样的,它和Back键的功能还是有一些区别,举个例子吧。
- 第一步我们已经实现了,就是调用setDisplayHomeAsUpEnabled()方法,并传入true。
- 第二步需要在AndroidManifest.xml中配置父Activity,如下所示:
<activity>
android:name=".MainActivity"
<meta-data>
android:name="android.support.PARENT_ACTIVITY"
android:value=".LaunchActivity"/>
</activity>
- 可以看到,这里通过 meta-data 标签指定了MainActivity的父Activity是LaunchActivity,在Android 4.1版本之后,也可以直接使用 android:parentActivityName 这个属性来进行指定,如下所示:
<activity
android:name=".MainActivity"
android:exported="true"
android:parentActivityName=".LaunchActivity">
- 可以看到父Activity --> LaunchActivity是项目中没有的,因此我们需要创建一个新活动LaunchActivity,相关代码如下所示:
- 第三步则需要对 android.R.id.home 这个时间进行一些特殊处理,如下所示:
case android.R.id.home:// 图标导航默认的id就是android.R.id.home
// 按照标准的规范成功实现ActionBar导航的功能了。
Intent upIntent = NavUtils.getParentActivityIntent(this);
if(NavUtils.shouldUpRecreateTask(this,upIntent)){
upIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
NavUtils.navigateUpTo(this,upIntent);
}else{
TaskStackBuilder.create(this)
.addNextIntentWithParentStack(upIntent)
.startActivities();
}
/*Intent intent = new Intent(MainActivity.this,LaunchActivity.class);
startActivity(intent);*/
Toast.makeText(this,"检验效果",Toast.LENGTH_SHORT).show();
break;
- 注意:以上代码中的if语句顺序是和前面两篇博客颠倒的,自己感觉前面两篇博客写错了。
- 其中,调用NavUtils.getParentActivityIntent()方法可以获取跳转至父Activity的Intent,然后如果父Activity和当前的Activity是在同一个Task中的,则直接调用navigateUpTo()方法进行跳转,如果不是在同一个Task中的,则需要创建一个新的Stack。
- 这样,就按照标准的规范成功实现ActionBar导航的功能了。
- 其实,实现的功能效果有些类似于使用Intent实现活动跳转。
添加ActionView
- ActionView是一种可以在ActionBar中替换Action按钮的控件,它可以允许用户在不切换界面的情况下通过ActionBar完成一些较为丰富的操作。比如说,你需要完成一个搜索功能,就可以将SearchView这个控件添加到ActionBar中。
- 为了声明一个ActionView,我们可以在menu资源中通过actionViewClass属性来指定一个控件,例如可以使用如下方式添加SearchView:
<menu xmlns:my_app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/search"
android:icon="@drawable/search"
my_app:actionViewClass="android.widget.SearchView"
my_app:showAsAction="ifRoom|collapseActionView"
android:title="搜索"/>
- 需要注意的是,actionViewClass属性和我们前面提到的showAsAction属性一样,在menu.xml文件中设置不成功,需要在前面onCreate()方法中通过java代码设置,如下:
// SeachView
// 解决my_app:actionViewClass="android.widget.SearchView"无效的问题
SearchView searchView = new SearchView(this);
MenuItemCompat.setActionView(menu.findItem(R.id.search),searchView);
- 另外,还需要注意的是,showAsAction属性中是否添加collapseActionView,前后的效果是不一样的,如下:
- 不添加collapseActionView
- 代码:
- `MenuItemCompat.setShowAsAction(menu.findItem(R.id.search),MenuItem.SHOW_AS_ACTION_IF_ROOM);
- 效果:
- 添加collapseActionView
- 代码:
MenuItemCompat.setShowAsAction(menu.findItem(R.id.search),MenuItem.SHOW_AS_ACTION_IF_ROOM|MenuItem.SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW);
- 如果你还希望在代码中对SearchView的属性进行设置(比如添加监听事件等),完全没有问题,只需要在onCreateOptionsMenu()方法中获取该ActionView的实例就可以了,如下所示:
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.main,menu);
// 解决:app:showAsAction="if_room"无效的问题
MenuItemCompat.setShowAsAction(menu.findItem(R.id.user),MenuItem.SHOW_AS_ACTION_IF_ROOM);
MenuItemCompat.setShowAsAction(menu.findItem(R.id.write),MenuItem.SHOW_AS_ACTION_IF_ROOM);
//MenuItemCompat.setShowAsAction(menu.findItem(R.id.search),MenuItem.SHOW_AS_ACTION_IF_ROOM);
// 添加MenuItem.SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW属性之后,效果不同
MenuItemCompat.setShowAsAction(menu.findItem(R.id.search),MenuItem.SHOW_AS_ACTION_IF_ROOM|MenuItem.SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW);
// SeachView
// 解决my_app:actionViewClass="android.widget.SearchView"无效的问题
SearchView searchView = new SearchView(this);
MenuItemCompat.setActionView(menu.findItem(R.id.search),searchView);
// 获取该ActionView的实例
MenuItem searchItem = menu.findItem(R.id.search);
SearchView searchView1 = (SearchView) searchItem.getActionView();
- 在得到了SearchView的实例之后,就可以任意地配置它的各种属性了。关于SearchView的更多详细用法,可以参考官方文档。
http://developer.android.com/guide/topics/search/search-dialog.html
- 除此之外,有些程序可能还希望在ActionView展开和合并的时候显示不同的界面,其实我们只需要去注册一个ActionView的监听器就能实现这样的功能了,代码如下所示:
- 注意一点的是,要想实现前面的效果,需要在showAsAction属性中添加collapseActionView。
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.main,menu);
...
// 获取该ActionView的实例
MenuItem searchItem = menu.findItem(R.id.search);
// 希望ActionView展开和合并的时候显示不同的界面,只需要去注册一个ActionView的监听器就能实现这样的功能了。
// 要想实现这个效果,前面的setShowAsAction要加上MenuItem.SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW属性
searchItem.setOnActionExpandListener(new MenuItem.OnActionExpandListener() {
@Override
public boolean onMenuItemActionExpand(MenuItem menuItem) {
Log.d("MainActivity","on expand");
Toast.makeText(MainActivity.this,"on expand",Toast.LENGTH_SHORT).show();
return true;
}
@Override
public boolean onMenuItemActionCollapse(MenuItem menuItem) {
Log.d("MainActivity","on collapse");
Toast.makeText(MainActivity.this,"on collapse",Toast.LENGTH_SHORT).show();
return true;
}
});
/*
可以看到,调用MenuItem的setOnActionExpandListener()方法就可以注册一个监听器了,
当SearchView展开的时候就会回调onMenuItemActionExpand()方法,当SearchView合并的时候就会调用
onMenuItemActionCollapse()方法,我们在这两个方法中进行相应的操作就可以了。
*/
Overflow按钮不显示的情况
- 虽然现在我们已经掌握了不少ActionBar的用法,但是当你真正去使用它的时候还是可能遇到各个各样的问题,比如很多人都遇到过overflow按钮不显示的情况。明明是同样一份代码,overflow按钮在有些手机上显示,而在有些手机上偏偏就不显示,这是为什么呢?
- 这是因为,overflow按钮的显示情况和手机的硬件情况是有关系的,如果手机没有物理Menu键的话,overflow按钮就可以显示,如果有物理Menu键的话,overflow按钮就不会显示出来。比如我们启动一个有Menu键的模拟器,然后将代码运行到该模拟器上,结果如下图所示:
- 可以看到,ActionBar最右边的overflow按钮不见了!那么此时我们如何查看隐藏在overflow中的Action按钮呢?其实非常简单,按一下Menu键,隐藏的内容就会从底部出来了,如下图所示:
- 看到这,大家都会觉得这是个非常烦人的设计,在不同手机上竟然显示了不同的界面,而且操作方法也不完全一样,这样会给用户一种非常不习惯的感觉。话说Goole为什么要把ActionBar的overflow设计成这样我也不太理解,但是我们还是有办法改变这一默认行为的。
- 实际上,在 ViewConfiguration 这个类中有一个叫做 sHasPermanentMenuKey 的静态变量,系统就是根据这个变量的值来判断手机有没有物理Menu键的。当然这是一个内部变量,我们无法直接访问它,但是可以通过反射的方式修改它的值,让它永远为false就可以了,代码如下所示:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
......
setOverflowShowingAlways();
}
private void setOverflowShowingAlways(){
try{
ViewConfiguration config = ViewConfiguration.get(this);
Field menuKeyField = ViewConfiguration.class.getDeclaredField("sHasPermanentMenuKey");
menuKeyField.setAccessible(true);
menuKeyField.setBoolean(config,false);
}catch(Exception e){
System.out.println(e.getMessage());
}
}
- 这里我们在onCreate()方法的最后调用了 setOverflowShowingAlways() 方法,而这个方法的内部就是使用反射的方式将 sHasPermanentMenuKey 的值设置成false,现在重新运行以下代码,结果如下图所示:
- 可以看到,即使是在有Menu键的手机上,也能让overflow按钮显示出来,这样就可以大大增加我们软件界面和操作的统一性。
- 注意:不出意外的话,肯定要出意外,没错,有错误出现,针对代码
Field menuKeyField = ViewConfiguration.class.getDeclaredField("sHasPermanentMenuKey");
的报错信息是Reflective access to sHasPermanentMenuKey will throw an exception when targeting API 32 and above
- 可以看出Target API 32不匹配,需要更改API值(在build.gradle文件中修改)
- 这里由于我对这个需求没有,因此不再修改API值,否则其他导入的模块还得报错,需要一步一步去寻找最佳API值。当时测的API值为19可以,但其他模块又报错了,需要再增加API值。
让Overflow中的按钮显示图标
- 如果你点击一下overflow按钮去查看隐藏的Action按钮,你会发现这部分Aciton按钮都是只显示文字不显示图标的,如下图所示:
- 这是官方的默认效果,Google认为隐藏在overflow中的Action按钮都应该只显示文字。当然,如果你认为这样不够美观,希望在overflow中的Action按钮也可以显示图标,我们仍然可以想办法来改变这一默认行为。
- 其实,overflow中的Action按钮应不应该显示图标,是由 MenuBuilder 这个类的 setOptionallconsVisible 方法来决定的,如果我们在overflow被展开的时候给这个方法传入true,那么里面的每一个Action按钮对应的图就都会显示出来了。调用的方法当然仍然是用反射了,代码如下所示:
@Override
public boolean onMenuOpened(int featureId, @NonNull Menu menu) {
if(featureId == Window.FEATURE_ACTION_BAR && menu != null){
if(menu.getClass().getSimpleName().equals("MenuBuilder")){
try{
Method m = menu.getClass().getDeclaredMethod("setOptionalIconsVisible",Boolean.TYPE);
m.setAccessible(true);
m.invoke(menu,true);
}catch(Exception e){
System.out.println(e.getMessage());
}
}
}
return super.onMenuOpened(featureId, menu);
}
/*
可以看到,这里我们重写了一个onMenuOpened()方法,当overflow被展开的时候就会回调这个方法,
接着在这个方法的内部通过返回反射的方法将MenuBuilder的setOptionallconsVisbible变量设置成true就可以了。
*/
- 效果如下所示: