Android拓展知识 - ActionBar

说明:借鉴了以下两篇博文,并在此基础上添加了一些自己的修改。

https://www.cnblogs.com/mjsn/p/6150824.html
https://blog.csdn.net/tangyeegg/article/details/53000121

需要掌握的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中
withTextActionBar尽可能显示文字,如果图标有限且受到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就可以了。
*/
  • 效果如下所示:
    在这里插入图片描述
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值