《Android编程权威指南》之深入学习intent和任务

《Android编程权威指南》第 23 章,本章将创建新应用啦,叫 NerdLauncher,然后技术点是关于隐式 intent 和 intent 过滤器。本章应用呢,将会展示设备上的其他应用,还可以启动其他应用。

一、创建 NerdLauncher 项目

创建项目,添加 RecyclerView 用于显示 App 列表。



class MainActivity : AppCompatActivity() {

    private lateinit var mBinding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mBinding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(mBinding.root)

        mBinding.recyclerView.layoutManager = LinearLayoutManager(this)
    }
}

二、解析隐式 intent

PackageManager 可用来获取所有可启动主 activity。可启动主 activity 都带有包含 MAIN 操作和 LAUNCHER 类别的 intent 过滤器。如下所示:

            
                
                
            

新增 setupAdapter() 函数,并在 onCreate() 中调用:

   private fun setupAdapter(){
       val startupIntent = Intent(Intent.ACTION_MAIN).apply {
           addCategory(Intent.CATEGORY_LAUNCHER)
       }
       val activities = packageManager.queryIntentActivities(startupIntent,0)
       Log.i(TAG, "Found ${activities.size} activities")
   }

上述代码就有创建操作设为 ACTION_MAIN、类别为 CATEGORY_LAUNCHER 的隐式 intent。

PackageManager.queryIntentActivities(Intent, Int) 会返回包含所有activity(有匹配目标 intent 的过滤器)的 ResolveInfo 信息。这里参数 0 表示不打算修改查询结果。

activities

接下来需要展示列表,使用 activity 标签名「应用名」。首先,使用ResolveInfo.loadLabel(PackageManager) 函数,对 ResolveInfo 对象中的 activity 标签按首字母排序。

在上述方法下面加入代码:

activities.sortWith(Comparator { a, b -> 
            String.CASE_INSENSITIVE_ORDER.compare(
                a.loadLabel(packageManager).toString(),
                b.loadLabel(packageManager).toString()
            )
        })

然后,定义一个 ViewHolder 用来显示 activity 标签名。使用成员变量存储 ResolveInfo 。

    private class ActivityHolder(itemView: View):RecyclerView.ViewHolder(itemView){
        
        private val tvName = itemView as TextView
        private lateinit var resolveInfo:ResolveInfo
        
        fun bindActivity(resolveInfo: ResolveInfo){
            this.resolveInfo = resolveInfo
            val packageManager = itemView.context.packageManager
            val appName = resolveInfo.loadLabel(packageManager).toString()
            tvName.text = appName
        }
    }

接下来实现 RecyclerView.Adapter:

    private class ActivityAdapter(val activities:List):RecyclerView.Adapter(){
        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ActivityHolder {
                val layoutInflater = LayoutInflater.from(parent.context)
                val view =layoutInflater.inflate(android.R.layout.simple_list_item_1,parent,false)
                 return ActivityHolder(view)
            }

        override fun onBindViewHolder(holder: ActivityHolder, position: Int) {
            val resolveInfo = activities[position]
            holder.bindActivity(resolveInfo)
        }

        override fun getItemCount(): Int {
            return activities.size
        }
    }

最后,把 adapter 实例配置给 RecyclerView ,在 setupAdapter 末尾处添加:

 mBinding.recyclerView.adapter = ActivityAdapter(activities)

运行结果:

result

三、在运行时创建显式intent

接下来做点击列表,用显示 intent 启动对应的 activity 啦。

要创建启动 activity 的显式 intent,就需要从 ResolveInfo 对象中获取 activity 的包名与类名。

更新 ActivityHolder 类实现一个点击监听器,并从 activityInfo 中获取必要信息,创建一个显示 intent 去启动目标 activity。

    private class ActivityHolder(itemView: View) : RecyclerView.ViewHolder(itemView),
        View.OnClickListener {
       ...
        init {
            tvName.setOnClickListener(this)
        }
       ...
        override fun onClick(v: View) {
            val activityInfo = resolveInfo.activityInfo
            val intent = Intent(Intent.ACTION_MAIN).apply {
                setClassName(activityInfo.applicationInfo.packageName, activityInfo.name)
            }
            val context = v.context
            context.startActivity(intent)
        }
    }

然后运行项目,点击列表 item,就可以跳转到相应的 App 了。

四、任务与回退栈

Android 使用任务来跟踪应用运行的状态。

任务是一个 activity 栈。栈底部的 activity 通常称为基 activity。栈顶的 activity 用户能看得到。按回退键,栈顶 activity 会弹出栈外。如果用户看到的是基 activity,按回退键,系统就会回到主屏幕。

有关 Activity 任务栈,可以看看扔物线的视频,讲的很详细易懂,一看就懂,不过还是需要多看几遍,思考一下,最好再实践一下理解更深刻:

https://www.bilibili.com/video/BV1CA41177Se?spm_id_from=333.999.0.0

当前应用去打开其他的 App,其他 App 的启动 Activity 都是运行在自身的任务栈中的,为了在启动新 activity 时启动新任务,需要为 intent 添加一个标志:

addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)

FLAG_ACTIVITY_NEW_TASK 标志控制每个 activity 仅创建一个任务。如果那个被打开的 activity 没有任务栈就会创建一个新的任务栈,如果已经有了一个运行的任务,Android 就会自动切回到那个任务,就不再创建新的任务了。

五、用 NerdLauncher 当主屏幕

这里实践把 NerdLauncer 应用成 Android 主界面(home screen),「我们的桌面实际上也是 Android 系统中的一个应用,显示着我们安装的各个 App,给我们启动 App 的入口」。

只需要修改 NerdLauncherActivity 的类别即可,打开 manifests/AndroidManifest.xml:

        
            
                

                
                
                
            
        

不过我这里实践按主屏幕键没有看到书中的那个框,可能跟 Android 版本有关。下次再研究一下。

六、深入学习:进程与任务

『进程』 是操作系统创建的、供应用对象生存以及应用运行的地方。通常会拥有由操作系统管理着的一些系统资源,比如内存、网络端口以及打开的文件等。拥有至少一个执行线程,Android 系统中,每个进程都需要一个 虚拟机 来运行。

Android 4.4(KitKat)之前,Dalvik 是 Android 操作系统使用的进程虚拟机。进程只要一启动,就会有一个 Dalvik 虚拟机新实例跳出来收留它。不过,自Android 5.0(Lollipop)开始,Android运行时(ART)取代了 Dalvik,已成为公认的进程虚拟机。

之前的应用 CriminalIntent,联系人应用虽然是 CriminalIntent 打开的,联系人列表 activity 会被加入到 CriminalIntent 应用任务中,可是联系人 activity 实例实际是在联系人应用进程的内存空间创建的,而且也是在该应用进程里的虚拟机上运行的。如图所示:

任务与进程一对多的关系

接下来再去试验一个过程,在 CriminalIntent 中进入联系人列表,按 Home 回到桌面,从桌面去启动联系人应用,从联系人列表中选取联系人或添加联系人。这个过程,系统会在联系人应用进程中创建新的联系人列表 activity 和联系人明细界面实例。也会创建联系人应用新任务。这个新任务会引用联系人列表和联系人明细界面 activity 实例,如图所示:

进程对多个任务

理解完本章,我们应该知道,Google Play 商店中一些自称为任务终止器的应用,实际上都是进程终止器。这些应用会“杀掉”某个进程,这表明,它们可能正在销毁其他应用任务引用的 activity。

七、深入学习:并发文档

并发文档(concurrent document):在Android Lollipop(API 级别 21)上引入,可以为运行的应用动态创建任意数目的任务。

Google Drive 是并发文档概念应用的最好实例。用户可以用它打开并编辑多份文档。从概览屏可以看到,这些文档编辑activity都处在独立的任务中。

多个 Google Drive 任务

如果需要应用启动多个任务,可给 intent 打上 Intent.FLAG_ACTIVITY_NEW_DOCUMENT 标签,再调用startActivity(...)函数;或在 manifest 中,为 activity 设置如下 documentLaunchMode:

android:documentLaunchMode="intoExisting"

这样,一份文档只会对应一个任务。(如果发送带有和已存在任务相同数据的intent,系统就不会再创建新任务。)如果无论如何都想创建新任务,那就给intent 同时打上 Intent.FLAG_ACTIVITY_NEW_DOCUMENT 和 Intent.FLAG_ACTIVITY_MULTIPLE_TASK 标签,或把 manifest 中的 documentLaunchMode 属性值改为 always。

自行实践~ O(∩_∩)O哈哈~

八、挑战练习:应用图标

给 NerdLauncher 应用中显示的所有应用添加图标。

简单的呢,就是在给列表设置文字内容的下面添加代码:

            val appIcon = resolveInfo.loadIcon(packageManager)
            appIcon.setBounds(0, 0, appIcon.minimumWidth, appIcon.minimumHeight)
            tvName.setCompoundDrawables(appIcon, null, null, null)

运行效果「丑丑的」:

result

其他

ResolveInfo 还可以获取其他信息,详情介绍请参考:

https://developer.android.com/reference/android/content/pm/ResolveInfo

NerdLauncher 项目 Demo 地址:

https://github.com/visiongem/AndroidGuideApp/tree/master/NerdLauncher

你可能感兴趣的:(《Android编程权威指南》之深入学习intent和任务)