四、连接屏幕流

各位读者好!我们已经到了应用开发的一个重要阶段——连接屏幕。如您所知,我们在上一章中创建了屏幕,在这一章中,我们将使用安卓强大的框架来连接它们。我们将继续我们的工作,并且,使用安卓,我们将对我们的用户界面做更严肃的事情。做好准备,专注于本章的每一个方面。会很有意思的!我们保证!

在本章中,我们将涵盖以下主题:

  • 创建应用栏
  • 使用抽屉导航
  • 安卓意图
  • 在活动和片段之间传递信息

创建应用栏

我们正在通过安卓应用开发继续我们的旅程。到目前为止,我们已经为我们的应用创建了一个基础,定义了用户界面的基础,并创建了主屏幕;但是,这些屏幕没有连接。在这一章中,我们将把它们联系起来,进行奇妙的互动。

因为一切都是从我们的MainActivity类开始的,所以在我们设置一些动作来触发其他屏幕之前,我们将应用一些改进。我们必须用一个应用栏来包装。什么是应用栏?它是用户界面的一部分,用于访问应用的其他部分,并提供具有交互元素的视觉结构。我们已经有了一个,但它不是通常的安卓应用栏。在这一点上,我们的应用是有一个修改的应用栏,我们希望它有一个标准的安卓应用栏。

在这里,我们将向您展示如何创建一个。

首先将顶级活动扩展替换为AppCompatActivity。我们需要访问应用栏所需的功能。AppCompatActivity将这些附加功能添加到标准FragmentActivity中。您的BaseActivity定义现在应该是这样的:

    abstract class BaseActivity : AppCompatActivity() {   
    ... 

然后更新使用的主题应用,这样就可以使用应用栏了。打开安卓清单,设置一个新的主题如下:

    ... 
    <application 
      android:name=".Journaler" 
      android:allowBackup="false" 
      android:icon="@mipmap/ic_launcher" 
      android:label="@string/app_name" 
      android:roundIcon="@mipmap/ic_launcher_round" 
      android:supportsRtl="true" 
      android:theme="@style/Theme.AppCompat.Light.NoActionBar"> 
    ... 

现在打开你的activity_main布局。删除标题中包含的指令,并添加Toolbar:

    <?xml version="1.0" encoding="utf-8"?> 
    <LinearLayout xmlns:android=
     "http://schemas.android.com/apk/res/android" 
    android:layout_width="match_parent" 
    android:layout_height="match_parent" 
    android:orientation="vertical"> 

    <android.support.v7.widget.Toolbar 
      android:id="@+id/toolbar" 
      android:layout_width="match_parent" 
      android:layout_height="50dp" 
      android:background="@color/colorPrimary" 
      android:elevation="4dp" /> 

    <android.support.v4.view.ViewPager  
      android:id="@+id/pager" 
      android:layout_width="match_parent" 
      android:layout_height="match_parent" /> 

    </LinearLayout> 

对所有布局应用相同的更改。完成后,更新你的BaseActivity代码,使用新的Toolbar。你的onCreate()方法现在应该是这样的:

    override fun onCreate(savedInstanceState: Bundle?) { 
      super.onCreate(savedInstanceState) 
      setContentView(getLayout()) 
      setSupportActionBar(toolbar)        
    Log.v(tag, "[ ON CREATE ]") 
    } 

我们通过调用setSupportActionBar()方法并从布局中传递工具栏的 ID 来分配应用栏。如果运行该应用,它将如下所示:

我们丢了我们头上的纽扣!别担心,我们会把他们找回来的!我们将创建一个菜单来处理动作,而不是按钮。在安卓系统中,菜单是用来管理项目的界面,你可以定义自己的菜单资源。在/res目录中,创建一个menu文件夹。右键单击menu文件夹,选择新建|新建菜单资源文件。称它为主。将打开一个新的 XML 文件。根据此示例更新其内容:

    <?xml version="1.0" encoding="utf-8"?> 
    <menu xmlns:android="http://schemas.android.com/apk/res/android" 
    xmlns:app="http://schemas.android.com/apk/res-auto"> 

    <item 
      app:showAsAction="ifRoom" 
      android:orderInCategory="1" 
      android:id="@+id/drawing_menu" 
      android:icon="@android:drawable/ic_dialog_dialer" 
      android:title="@string/mnu" /> 

    <item 
      app:showAsAction="ifRoom" 
      android:orderInCategory="2" 
      android:id="@+id/options_menu" 
      android:icon="@android:drawable/arrow_down_float" 
      android:title="@string/mnu" /> 
    </menu>

我们设置公共属性、图标和顺序。要确保您的图标可见,请使用以下方法:

    app:showAsAction="ifRoom" 

这样,如果有任何可用空间,菜单中的项目将被展开;否则,可以通过上下文菜单访问它们。安卓系统中你可以选择的其他间距选项如下:

  • 始终:该按钮始终位于应用栏中
  • 从不:此按钮从不放在应用栏中
  • 折叠视图:该按钮可以显示为一个小部件
  • 带文字:该按钮带文字显示

要将菜单分配给应用栏,请在BaseActivity中添加以下内容:

    override fun onCreateOptionsMenu(menu: Menu): Boolean { 
      menuInflater.inflate(R.menu.main, menu) 

      return true 
    } 

最后,将动作连接到菜单项,并通过添加以下代码扩展MainActivity:

    override fun onOptionsItemSelected(item: MenuItem): Boolean { 
      when (item.itemId) { 
        R.id.drawing_menu -> { 
          Log.v(tag, "Main menu.") 
          return true 
        } 
        R.id.options_menu -> { 
          Log.v(tag, "Options menu.") 
          return true 
        } 
        else -> return super.onOptionsItemSelected(item) 

     } 

    } 

这里,我们覆盖了onOptionsItemSelected()方法,并处理了菜单项标识的情况。在每次选择时,我们都会添加一条日志消息。现在运行您的应用。您应该会看到以下菜单项:

在每个项目上点击几次,观察日志。您应该会看到类似如下的日志:

    V/Main activity: Main menu. 
    V/Main activity: Options menu. 
    V/Main activity: Options menu. 
    V/Main activity: Options menu. 

    V/Main activity: Main menu. 

    V/Main activity: Main menu. 

我们成功地将标题切换到应用栏。它与我们在应用线框中的标题有很大不同。这在目前并不重要,因为我们将在接下来的章节中做一些重要的造型。我们的应用栏看起来会有所不同。

在下一节中,我们将处理导航抽屉,并开始组装应用的导航。

使用导航抽屉

您可能还记得,在我们的模型中,我们已经展示了将会有到过滤数据(Notes 和 Todos)的链接。我们将使用导航抽屉过滤这些内容。每个现代应用都使用一个导航抽屉。它是显示应用导航选项的用户界面的一部分。要定义抽屉,我们必须在布局中放置DrawerLayout视图。打开activity_main并进行以下修改:

    <?xml version="1.0" encoding="utf-8"?> 
    <android.support.v4.widget.DrawerLayout    xmlns:android=
    "http://schemas.android.com/apk/res/android" 
     android:id="@+id/drawer_layout" 
     android:layout_width="match_parent" 
     android:layout_height="match_parent"> 

    <LinearLayout 
      android:layout_width="match_parent" 
      android:layout_height="match_parent" 
      android:orientation="vertical"> 

    <android.support.v7.widget.Toolbar 
      android:id="@+id/toolbar" 
      android:layout_width="match_parent" 
      android:layout_height="50dp" 
      android:background="@color/colorPrimary" 
      android:elevation="4dp" /> 

    <android.support.v4.view.ViewPager xmlns:android=
    "http://schemas.android.com/apk/res/android" 
      android:id="@+id/pager" 
      android:layout_width="match_parent" 
      android:layout_height="match_parent" /> 

    </LinearLayout> 

    <ListView 
       android:id="@+id/left_drawer" 
       android:layout_width="240dp" 
       android:layout_height="match_parent" 
       android:layout_gravity="start" 
       android:background="@android:color/darker_gray" 
       android:choiceMode="singleChoice" 
       android:divider="@android:color/transparent" 
       android:dividerHeight="1dp" /> 
    </android.support.v4.widget.DrawerLayout>  

屏幕的主要内容必须是DrawerLayout的第一个孩子。导航抽屉使用第二胎作为抽屉的内容。在我们的例子中,是ListView。要告诉导航抽屉导航应该位于左侧还是右侧,请使用layout_gravity属性。如果我们计划使用位于右侧的导航抽屉,我们应该将属性值设置为end

现在我们有一个空的导航抽屉,我们必须用一些按钮填充它。为每个导航项目创建一个新的布局文件。称之为adapter_navigation_drawer。将其定义为内部只有一个按钮的简单线性布局:

    <?xml version="1.0" encoding="utf-8"?> 
    <LinearLayout xmlns:android=
    "http://schemas.android.com/apk/res/android" 
      android:layout_width="match_parent" 
      android:layout_height="match_parent" 
      android:orientation="vertical"> 

    <Button 
      android:id="@+id/drawer_item" 
      android:layout_width="match_parent" 
      android:layout_height="wrap_content" /> 

    </LinearLayout> 

然后,创建一个名为navigation的新包。在这个包中,创建一个新的 Kotlin data类,如下所示:

    package com.journaler.navigation 
    data class NavigationDrawerItem( 
      val title: String,        
      val onClick: Runnable 
    ) 

我们定义了一个抽屉项目实体。现在再创建一个类:

    class NavigationDrawerAdapter( 
        val ctx: Context, 
        val items: List<NavigationDrawerItem> 
    ) : BaseAdapter() { 

    override fun getView(position: Int, v: View?, group: ViewGroup?):   
    View { 
      val inflater = LayoutInflater.from(ctx) 
      var view = v 
      if (view == null) { 
        view = inflater.inflate( 
          R.layout.adapter_navigation_drawer, null 
        ) as LinearLayout 
      } 

      val item = items[position] 
      val title = view.findViewById<Button>(R.id.drawer_item) 
      title.text = item.title 
      title.setOnClickListener { 
        item.onClick.run() 
      } 

      return view 
     } 

     override fun getItem(position: Int): Any { 
       return items[position] 
      } 

     override fun getItemId(position: Int): Long { 
       return 0L 
     } 

     override fun getCount(): Int {     
     return items.size 
     } 

    } 

这里显示的这个类扩展了安卓的BaseAdapter并覆盖了适配器提供视图实例所需的方法。适配器创建的所有视图将被分配到我们的导航抽屉中展开ListView

最后,我们将分配这个适配器。为此,我们需要通过执行以下代码来更新我们的MainActivity类:

    class MainActivity : BaseActivity() { 
    ... 
    override fun onCreate(savedInstanceState: Bundle?) { 
      super.onCreate(savedInstanceState) 
      pager.adapter = ViewPagerAdapter(supportFragmentManager) 

      val menuItems = mutableListOf<NavigationDrawerItem>() 
      val today = NavigationDrawerItem( 
        getString(R.string.today), 
          Runnable { 
            pager.setCurrentItem(0, true) 
          } 
        ) 

        val next7Days = NavigationDrawerItem( 
           getString(R.string.next_seven_days), 
             Runnable { 
               pager.setCurrentItem(1, true) 
             } 
         ) 

         val todos = NavigationDrawerItem( 
           getString(R.string.todos), 
             Runnable { 
               pager.setCurrentItem(2, true) 
             } 
         ) 

         val notes = NavigationDrawerItem( 
           getString(R.string.notes), 
             Runnable { 
               pager.setCurrentItem(3, true) 
             } 
        ) 

        menuItems.add(today) 
        menuItems.add(next7Days) 
        menuItems.add(todos) 
        menuItems.add(notes) 

        val navgationDraweAdapter = 
          NavigationDrawerAdapter(this, menuItems) 
        left_drawer.adapter = navgationDraweAdapter 
      } 
      override fun onOptionsItemSelected(item: MenuItem): Boolean { 
        when (item.itemId) { 
          R.id.drawing_menu -> { 
            drawer_layout.openDrawer(GravityCompat.START) 
            return true 
          } 
          R.id.options_menu -> { 
             Log.v(tag, "Options menu.") 
             return true 
          } 
          else -> return super.onOptionsItemSelected(item) 
        }      
      }  
    }  

在这个代码示例中,我们实例化了几个NavigationDrawerItem实例,然后,我们为将要执行的按钮和Runnable动作分配了一个标题。每个Runnable都会跳转到我们视图页面的特定页面。我们将所有实例作为一个单一的可变列表传递给适配器。您可能也注意到了我们更改了drawing_menu项目的线路。点击它,我们将展开我们的导航抽屉。请遵循以下步骤:

  1. 构建并运行您的应用。
  2. 单击主屏幕右上角的菜单按钮,或从屏幕最左侧向右滑动展开导航抽屉。
  3. 点击按钮。
  4. 您会注意到,视图页导航正在为其在导航抽屉下方的页面位置制作动画。

连接活动

大家记得,除了MainActivity,我们还有一些更多的活动。在我们的应用中,我们创建了创建/编辑笔记和待办事项的活动。我们的计划是将它们连接到按钮点击事件,然后,当用户点击按钮时,适当的屏幕将会打开。我们将首先定义一个enum,它代表我们将在一个打开的活动中执行的操作。当我们打开它时,我们可以查看、创建或更新笔记或待办事项。创建一个名为modelenum的名为MODE的新包。确保您有以下enum值:

    enum class MODE(val mode: Int) { 
      CREATE(0), 
      EDIT(1), 
      VIEW(2); 

      companion object { 
        val EXTRAS_KEY = "MODE" 

        fun getByValue(value: Int): MODE { 
          values().forEach { 
            item -> 

            if (item.mode == value) { 
              return item 
            } 
          } 
          return VIEW 
        } 
      }  
    } 

我们在这里添加了一些内容。在enum的伴随对象中,我们定义了额外的键定义。很快,你会需要它,你会明白它的目的。我们还创建了一个方法,根据它的价值给我们一个enum

您可能还记得,使用 Notes 和 Todos 的两个活动共享同一个类。打开ItemActivity并如下展开:

     abstract class ItemActivity : BaseActivity() { 
       protected var mode = MODE.VIEW 
       override fun getActivityTitle() = R.string.app_name 
       override fun onCreate(savedInstanceState: Bundle?) { 
         super.onCreate(savedInstanceState) 
         val modeToSet = intent.getIntExtra(MODE.EXTRAS_KEY, 
         MODE.VIEW.mode) 
         mode = MODE.getByValue(modeToSet) 
         Log.v(tag, "Mode [ $mode ]") 
       } 
     }  

我们引入了我们刚刚定义的类型的字段模式,它将告诉我们是否正在查看、创建或编辑 Note 或 Todo 项。然后,我们否决了onCreate()方法。这很重要!当我们点击按钮并打开活动时,我们会传递一些值给它。这段代码片段检索我们传递的值。为此,我们访问Intent实例(在下一节中,我们将解释intents)和名为MODE(值为MODE.EXTRAS_KEY)的整数字段。给我们这个值的方法叫做getIntExtra()。每种类型都有一个方法版本。如果没有值,则返回MODE.VIEW.mode。最后,我们将模式设置为通过从整数值中获取MODE实例而获得的值。

谜题的最后一部分是触发活动开始。打开ItemsFragment并如下展开:

    class ItemsFragment : BaseFragment() { 
      ... 
      override fun onCreateView( 
        inflater: LayoutInflater?, 
        container: ViewGroup?, 
        savedInstanceState: Bundle? 
      ): View? {         
          val view = inflater?.inflate(getLayout(), container, false) 
          val btn = view?.findViewById<FloatingActionButton>
          (R.id.new_item) 
          btn?.setOnClickListener { 
            val items = arrayOf( 
              getString(R.string.todos), 
              getString(R.string.notes) 
            ) 
            val builder = 
            AlertDialog.Builder(this@ItemsFragment.context) 
            .setTitle(R.string.choose_a_type) 
            .setItems( 
              items, 
              { _, which -> 
               when (which) { 
               0 -> { 
                 openCreateTodo() 
               } 
               1 -> { 
                 openCreateNote() 
               } 
               else -> Log.e(logTag, "Unknown option selected 
               [ $which ]") 
                } 
               } 
             ) 

            builder.show() 
          } 

          return view 
       } 

      private fun openCreateNote() { 
        val intent = Intent(context, NoteActivity::class.java) 
        intent.putExtra(MODE.EXTRAS_KEY, MODE.CREATE.mode) 
        startActivity(intent) 
      } 

      private fun openCreateTodo() { 
        val intent = Intent(context, TodoActivity::class.java) 
        intent.putExtra(MODE.EXTRAS_KEY, MODE.CREATE.mode) 
        startActivity(intent) 

      } 

     } 

我们访问了FloatingActionButton实例,并分配了一个点击监听器。单击后,我们将创建一个包含两个选项的对话框。这些选项中的每一个都会触发一个适当的活动打开方法。两种方法的实现非常相似。举个例子,我们将重点介绍openCreateNote()

我们将创建一个新的Intent实例。在安卓系统中,Intent代表了我们做某件事的意图。要开始一个活动,我们必须传递我们想要开始的活动的上下文和类。我们还必须赋予它一些价值。这些值将被传递给一个活动实例。在我们的例子中,我们正在传递MODE.CREATE. startActivity()方法的整数值,该方法将执行意图,并且将出现一个屏幕。

运行应用,单击屏幕右下角的圆形按钮,并从对话框中选择一个选项,如下图所示:

这将带您进入此屏幕:

这将带您进一步添加您自己的带有日期和时间的数据:

深入了解安卓意图

你计划在安卓系统中执行的大多数操作都是通过Intent类定义的。Intents可以像我们一样启动活动,启动服务(后台运行的进程),或者发送广播消息。

Intent通常接受我们想要传递给某个类的动作和数据。我们可以设置的动作属性有,例如,ACTION_VIEWACTION_EDITACTION_MAIN

除了动作和数据,我们可以为意图设置一个类别。该类别为我们设置的操作提供了附加信息。我们还可以设置意图的类型和代表我们将使用的显式组件类名的组件。

intents有两种类型:

  • 明确的意图
  • 隐含的意图

显式意图具有显式组件集,该组件集提供要运行的显式类。隐式意图没有显式组件,但是系统根据我们分配的数据和属性决定如何处理它。意图解决流程负责处理此类intents

这些参数的组合是无穷无尽的。我们将给出一些例子,以便您更好地理解intents的目的:

  • 打开网页:
         val intent = Intent(Intent.ACTION_VIEW,
         Uri.parse("http://google.com")) 
         startActivity(intent) 
         Sharing: 
         val intent = Intent(Intent.ACTION_SEND) 
         intent.type = "text/plain" 
         intent.putExtra(Intent.EXTRA_TEXT, "Check out this cool app!") 
         startActivity(intent)  
  • 从相机捕捉图像:
        val takePicture = Intent(MediaStore.ACTION_IMAGE_CAPTURE) 
        if (takePicture.resolveActivity(packageManager) != null) { 
         startActivityForResult(takePicture, REQUEST_CAPTURE_PHOTO +
         position) 
        } else { 
          logger.e(tag, "Can't take picture.") 
       }  
  • 从图库中选取图像:
        val pickPhoto = Intent( 
         Intent.ACTION_PICK, 
         MediaStore.Images.Media.EXTERNAL_CONTENT_URI 
        ) 
        startActivityForResult(pickPhoto, REQUEST_PICK_PHOTO + 
       position) 

如您所见,intents是安卓框架的关键部分。在下一节中,我们将扩展我们的代码以更多地使用intents

在活动和片段之间传递信息

为了在我们的活动之间传递信息,我们将使用安卓捆绑包。Bundle 可以包含不同类型的多个值。我们将通过扩展我们的代码来说明 Bundle 的使用。打开ItemsFragemnt,更新如下:

    private fun openCreateNote() { 
      val intent = Intent(context, NoteActivity::class.java) 
      val data = Bundle() 
      data.putInt(MODE.EXTRAS_KEY, MODE.CREATE.mode) 
      intent.putExtras(data) 
      startActivityForResult(intent, NOTE_REQUEST) 
    } 
    private fun openCreateTodo() { 
       val date = Date(System.currentTimeMillis()) 
       val dateFormat = SimpleDateFormat("MMM dd YYYY", Locale.ENGLISH) 
       val timeFormat = SimpleDateFormat("MM:HH", Locale.ENGLISH) 

       val intent = Intent(context, TodoActivity::class.java) 
       val data = Bundle() 
       data.putInt(MODE.EXTRAS_KEY, MODE.CREATE.mode) 
       data.putString(TodoActivity.EXTRA_DATE, dateFormat.format(date)) 
       data.putString(TodoActivity.EXTRA_TIME, 
       timeFormat.format(date)) 
       intent.putExtras(data) 
       startActivityForResult(intent, TODO_REQUEST) 
    } 

    override fun onActivityResult(requestCode: Int, resultCode: Int, 
    data: Intent?) { 
      super.onActivityResult(requestCode, resultCode, data) 
      when (requestCode) { 
         TODO_REQUEST -> { 
           if (resultCode == Activity.RESULT_OK) { 
             Log.i(logTag, "We created new TODO.") 
           } else { 
             Log.w(logTag, "We didn't created new TODO.") 
           } 
          } 
          NOTE_REQUEST -> { 
            if (resultCode == Activity.RESULT_OK) { 
              Log.i(logTag, "We created new note.") 
            } else { 
              Log.w(logTag, "We didn't created new note.") 
              } 
           } 
         } 
      } 

在这里,我们介绍了一些重要的变化。首先,我们开始了笔记和待办事项活动,作为子活动。这意味着我们的MainActivity课取决于那些活动的工作结果。当开始子活动而不是startActivity()方法时,我们使用了startActivityForResult()。我们传递的参数是意图和请求号。为了得到执行的结果,我们重写了onActivityResult()方法。如您所见,我们检查了哪个活动完成了,以及该执行是否产生了成功的结果。

我们也改变了传递信息的方式。我们创建了Bundle实例并分配了多个值,就像 Todo 活动一样。我们添加了模式、日期和时间。使用putExtras()方法将束分配给意图。为了使用这些额外的东西,我们也更新了我们的活动。打开ItemsActivity并应用更改,如下所示:

     abstract class ItemActivity : BaseActivity() { 
       protected var mode = MODE.VIEW 
       protected var success = Activity.RESULT_CANCELED 
       override fun getActivityTitle() = R.string.app_name 

       override fun onCreate(savedInstanceState: Bundle?) { 
         super.onCreate(savedInstanceState) 
         val data = intent.extras 
         data?.let{ 
           val modeToSet = data.getInt(MODE.EXTRAS_KEY, MODE.VIEW.mode) 
           mode = MODE.getByValue(modeToSet) 
         } 
         Log.v(tag, "Mode [ $mode ]") 
       } 

       override fun onDestroy() { 
         super.onDestroy() 
         setResult(success) 
      } 

    } 

这里,我们介绍了活动工作的现场举行结果。我们还更新了处理传递信息的方式。正如您所看到的,如果有任何额外的可用,我们将为模式获取一个整数值。最后,onDestroy()方法设置可用于父活动的工作结果。

打开TodoActivity并应用以下更改:

     class TodoActivity : ItemActivity() { 

     companion object { 
       val EXTRA_DATE = "EXTRA_DATE" 
       val EXTRA_TIME = "EXTRA_TIME" 
     } 

     override val tag = "Todo activity" 

     override fun getLayout() = R.layout.activity_todo 

     override fun onCreate(savedInstanceState: Bundle?) { 
       super.onCreate(savedInstanceState) 
       val data = intent.extras 
       data?.let { 
         val date = data.getString(EXTRA_DATE, "") 
         val time = data.getString(EXTRA_TIME, "") 
         pick_date.text = date 
         pick_time.text = time 
       } 
     } 

    }  

我们已经获得了日期和时间的附加值,并将其设置为日期/时间选择器按钮。运行您的应用并打开 Todo 活动。您的待办事项屏幕应该如下所示:

当您离开待办事项活动并返回主屏幕时,请观察您的日志。将出现一个日志,内容如下:

W/Items 片段-我们没有创建新的待办事项。

因为我们还没有创建任何 Todo 项目,所以我们传递了一个正确的结果。我们通过返回主屏幕取消了创建过程。在后面的章节中,以及接下来的章节中,我们将成功地创建 Notes 和 Todos。

摘要

我们用这一章来连接我们的接口,并建立一个真实的应用流。我们通过为用户界面元素设置适当的动作来建立屏幕之间的连接。我们把数据从一点传到另一点。这一切都用一种非常简单的方式!我们有东西在工作,但它看起来很丑。在下一章,我们将确保它看起来很漂亮!我们将为它设计风格,并添加一些不错的视觉效果。准备好迎接安卓强大的 UI API。