草庐IT

模仿Android微信小程序,实现小程序独立任务视图的效果

guolin 2023-11-06 原文

本文同步发表于我的微信公众号,扫一扫文章底部的二维码或在微信搜索 郭霖 即可关注,每个工作日都有文章更新。

大家好,久违的原创又来了。

今天跟大家分享一个非常有趣的技术,如何在我们的App中实现类似于微信小程序的功能。

哈哈开个玩笑,如果我能徒手实现一套微信小程序系统的话,早就被腾讯挖过去当架构师了。

小程序相信现在所有人都使用过的对吧,很多人甚至天天都在使用。小程序特别的方便,无需下载,无需安装,在微信当中打开就能立刻使用。随取随用,随用随走,也不占用任何手机的存储空间。

而Android上的微信小程序做得格外的像一个真正的应用程序。为什么这么说呢?因为Android上的每个微信小程序甚至还能拥有自己的任务视图,就像是一个真正的独立应用程序一样。点击手机任务栏键可以看到如下界面:

上图中美团外卖、微博热搜、星巴克都是小程序。

拥有独立的任务视图的话,就可以更加方便地在多个小程序或微信本体之间进行快速切换,在这点上Android的体验要比iOS更好。

那么问题来了,这种依附于其他程序的小程序是如何做到拥有一个独立的任务视图的呢?

本篇文章我们就来一探究竟。

事实上,这是一个很基础的功能。有多基础呢?任何一位Android开发者在入门时都一定学过这个知识:Launch Mode。

因此,我就不在这里对Launch Mode进行展开讲解了。如果你真的从来没有听说过Launch Mode,建议参考《第一行代码 第3版》第3章的内容。

我们都知道,Android中Activity的启动模式一共有4种:standdard、singleTop、singleTask和singleInstance。

从字面意思上来看,singleTask表示的就是要启用一个单独的任务来存放当前Activity。但假如你把一个Activity声明成了singleTask,你会发现并不能得到我们想要的效果,所有的Activity仍然是放在同一个任务当中的。

这是因为,singleTask还会关联一个叫taskAffinity的属性,只有被声明成singleTask的Activity,且它的taskAffinity值也是独立的,那么这个Activity才会被放在一个单独的任务当中。

而默认情况下,每个Activity的taskAffinity属性值都是当前应用程序的包名,也就是说它们的值都是相同的,所以才不能得到我们想要的效果。

那么解决方法也很简单,给每一个要启用独立任务视图的Activity都赋值一个不同的taskAffinity值即可。

接下来我们就开始动手实践一下吧。

首先创建一个叫MiniProgramTest的项目。

接下来创建3个空的Activity,分别给它们起名为FirstActivity、SecondActivity和ThirdActivity。

然后编辑项目的activity_main.xml布局文件,在里面加入3个按钮,分别用于启动FirstActivity、SecondActivity和ThirdActivity:

<androidx.constraintlayout.widget.ConstraintLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/first_btn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="启动第一行代码"
        app:layout_constraintVertical_chainStyle="packed"
        app:layout_constraintBottom_toTopOf="@+id/second_btn"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/second_btn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="启动第二行代码"
        app:layout_constraintVertical_chainStyle="packed"
        app:layout_constraintBottom_toTopOf="@+id/third_btn"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/first_btn" />

    <Button
        android:id="@+id/third_btn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="启动第三行代码"
        app:layout_constraintVertical_chainStyle="packed"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/second_btn" />

</androidx.constraintlayout.widget.ConstraintLayout>

布局文件定义好了之后,接下来修改MainActivity的代码,加入启动逻辑:

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val firstBtn = findViewById<Button>(R.id.first_btn)
        val secondBtn = findViewById<Button>(R.id.second_btn)
        val thirdBtn = findViewById<Button>(R.id.third_btn)

        firstBtn.setOnClickListener {
            val intent = Intent(this, FirstActivity::class.java)
            startActivity(intent)
        }
        secondBtn.setOnClickListener {
            val intent = Intent(this, SecondActivity::class.java)
            startActivity(intent)
        }
        thirdBtn.setOnClickListener {
            val intent = Intent(this, ThirdActivity::class.java)
            startActivity(intent)
        }
    }
}

代码非常简单,点击哪个按钮就去启动相应的Activity就可以了。

但如果仅仅是这样,FirstActivity、SecondActivity和ThirdActivity一定与MainActivity是存放在同一个任务当中的。

因此下面我们就要去编写最核心的代码了,修改AndroidManifest.xml文件,如下所示:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.example.miniprogramtest">

    <application
        ...>

        <activity
            android:name=".FirstActivity"
            android:exported="false"
            android:label="第一行代码"
            android:launchMode="singleTask"
            android:taskAffinity="com.example.miniprogramtest.first"
            />

        <activity
            android:name=".SecondActivity"
            android:exported="false"
            android:label="第二行代码"
            android:launchMode="singleTask"
            android:taskAffinity="com.example.miniprogramtest.second" />

        <activity
            android:name=".ThirdActivity"
            android:exported="false"
            android:label="第三行代码"
            android:launchMode="singleTask"
            android:taskAffinity="com.example.miniprogramtest.third"
            />
        ...
    </application>

</manifest>

可以看到,这里我们将FirstActivity、SecondActivity和ThirdActivity的launchMode都设置成了singleTask,并且给它们都指定了一个不同的taskAffinity。

现在运行一下程序,并分别点击界面上的3个按钮,然后按下手机任务栏键,我们就能看到如下效果了:

有没有觉得很神奇?明明都是同一个App中的3个Activity,现在我们竟然可以让它们在3个独立的任务视图中显示,是不是感觉就好像是微信小程序一样?

不过,虽然FirstActivity、SecondActivity和ThirdActivity都拥有独立的任务视图了,它们和微信小程序还有一个非常明显的差距。

因为每个程序都有自己专属的应用Logo,小程序也不例外。就像我们在最开始的图片中看到的一样,美团小程序有美团的Logo,微博小程序有微博的Logo,星巴克小程序有星巴克的Logo。

而目前,FirstActivity、SecondActivity和ThirdActivity显示的都是MiniProgramTest这个项目的Logo,这使得它们看上去仍然不像是一个独立的应用程序。

下面我们就开始着手优化这部分问题。

首先,这里我准备了3张图片first_line.png、second_line.png、third_line.png,分别用于作为FirstActivity、SecondActivity和ThirdActivity的Logo:

接下来,编辑FirstActivity、SecondActivity和ThirdActivity的代码,在里面加入如下逻辑:

class FirstActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_first)
        setCustomTaskDescription()
    }

    private fun setCustomTaskDescription() {
        val taskDescription = ActivityManager.TaskDescription(
            "FirstActivity",
            BitmapFactory.decodeResource(resources, R.drawable.first_line)
        )
        setTaskDescription(taskDescription)
    }
}

class SecondActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_second)
        setCustomTaskDescription()
    }

    private fun setCustomTaskDescription() {
        val taskDescription = ActivityManager.TaskDescription(
            "SecondActivity",
            BitmapFactory.decodeResource(resources, R.drawable.second_line)
        )
        setTaskDescription(taskDescription)
    }
}

class ThirdActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_third)
        setCustomTaskDescription()
    }

    private fun setCustomTaskDescription() {
        val taskDescription = ActivityManager.TaskDescription(
            "ThirdActivity",
            BitmapFactory.decodeResource(resources, R.drawable.third_line)
        )
        setTaskDescription(taskDescription)
    }
}

这3段代码的逻辑基本都是相同的。

核心部分就是调用了setCustomTaskDescription()方法来给当前Activity设置一个自定义的TaskDescription。

所谓TaskDescription就是给当前的任务设置一个描述,描述中可以包含任务的名称和图标。

那么这里我们给FirstActivity、SecondActivity和ThirdActivity分别设置了不同的TaskDescription,这样在任务视图当中,就可以看到各不相同的应用Logo了,如下图所示:

其实到这里为止,我们就把微信小程序的外壳搭建得差不多了。剩下的部分,当然也是最难的部分,就是在这个壳子里面添加小程序的内容了。这部分的技术以前端为主,并不是我擅长的领域,我也讲不了,因此就不再继续向下延伸了。

不过或许还有些朋友会存在这样的疑惑:目前我们的技术实现方案是给每个小程序定义一个单独的Activity(FirstActivity、SecondActivity和ThirdActivity),而微信小程序却可以有无限多个,我们显然不可能在AndroidManifest.xml文件中注册无限个Activity,那么微信又是如何实现的呢?

其实这只是一个美丽的误会,因为微信小程序并不是可以有无限多个,只是你平时没有注意这个小细节而已。

我们通过做个实验来验证一下吧,观察下图中的效果:

可以看到,这里我事先依次按照顺序打开了哔哩哔哩、QQ音乐、微博热搜、京东购物、星巴克,这5个小程序。

这个时候回到微信当中,再打开一个顺丰速运小程序。

再次回到任务视图列表界面,你会发现现在多了一个顺丰速运的小程序,而最早打开的哔哩哔哩小程序却从任务视图列表中消失不见了。

由此可以看出,微信其实在AndroidManifest.xml文件中也只是放置了5个占位的Activity。当你尝试打开第6个小程序时,最先打开的那个小程序就会被回收,将它的容器提供给第6个小程序使用。

好了,本篇文章到这里就结束了。内容其实非常的简单,但是已经把在Android上如何实现小程序外层的架子讲明白了。至于如何实现小程序最核心的内容部分,那就要看各位架构师的水准了。

我们下期再见。

如果想要学习Kotlin和最新的Android知识,可以参考我的新书 《第一行代码 第3版》点击此处查看详情


关注我的技术公众号,每天都有优质技术文章推送。

微信扫一扫下方二维码即可关注:

有关模仿Android微信小程序,实现小程序独立任务视图的效果的更多相关文章

  1. ruby - 其他文件中的 Rake 任务 - 2

    我试图在一个项目中使用rake,如果我把所有东西都放到Rakefile中,它会很大并且很难读取/找到东西,所以我试着将每个命名空间放在lib/rake中它自己的文件中,我添加了这个到我的rake文件的顶部:Dir['#{File.dirname(__FILE__)}/lib/rake/*.rake'].map{|f|requiref}它加载文件没问题,但没有任务。我现在只有一个.rake文件作为测试,名为“servers.rake”,它看起来像这样:namespace:serverdotask:testdoputs"test"endend所以当我运行rakeserver:testid时

  2. ruby - 在 Ruby 程序执行时阻止 Windows 7 PC 进入休眠状态 - 2

    我需要在客户计算机上运行Ruby应用程序。通常需要几天才能完成(复制大备份文件)。问题是如果启用sleep,它会中断应用程序。否则,计算机将持续运行数周,直到我下次访问为止。有什么方法可以防止执行期间休眠并让Windows在执行后休眠吗?欢迎任何疯狂的想法;-) 最佳答案 Here建议使用SetThreadExecutionStateWinAPI函数,使应用程序能够通知系统它正在使用中,从而防止系统在应用程序运行时进入休眠状态或关闭显示。像这样的东西:require'Win32API'ES_AWAYMODE_REQUIRED=0x0

  3. ruby - 如何指定 Rack 处理程序 - 2

    Rackup通过Rack的默认处理程序成功运行任何Rack应用程序。例如:classRackAppdefcall(environment)['200',{'Content-Type'=>'text/html'},["Helloworld"]]endendrunRackApp.new但是当最后一行更改为使用Rack的内置CGI处理程序时,rackup给出“NoMethodErrorat/undefinedmethod`call'fornil:NilClass”:Rack::Handler::CGI.runRackApp.newRack的其他内置处理程序也提出了同样的反对意见。例如Rack

  4. ruby - 在 Ruby 中编写命令行实用程序 - 2

    我想用ruby​​编写一个小的命令行实用程序并将其作为gem分发。我知道安装后,Guard、Sass和Thor等某些gem可以从命令行自行运行。为了让gem像二进制文件一样可用,我需要在我的gemspec中指定什么。 最佳答案 Gem::Specification.newdo|s|...s.executable='name_of_executable'...endhttp://docs.rubygems.org/read/chapter/20 关于ruby-在Ruby中编写命令行实用程序

  5. ruby-on-rails - Rails 应用程序之间的通信 - 2

    我构建了两个需要相互通信和发送文件的Rails应用程序。例如,一个Rails应用程序会发送请求以查看其他应用程序数据库中的表。然后另一个应用程序将呈现该表的json并将其发回。我还希望一个应用程序将存储在其公共(public)目录中的文本文件发送到另一个应用程序的公共(public)目录。我从来没有做过这样的事情,所以我什至不知道从哪里开始。任何帮助,将不胜感激。谢谢! 最佳答案 无论Rails是什么,几乎所有Web应用程序都有您的要求,大多数现代Web应用程序都需要相互通信。但是有一个小小的理解需要你坚持下去,网站不应直接访问彼此

  6. ruby - 无法运行 Rails 2.x 应用程序 - 2

    我尝试运行2.x应用程序。我使用rvm并为此应用程序设置其他版本的ruby​​:$rvmuseree-1.8.7-head我尝试运行服务器,然后出现很多错误:$script/serverNOTE:Gem.source_indexisdeprecated,useSpecification.Itwillberemovedonorafter2011-11-01.Gem.source_indexcalledfrom/Users/serg/rails_projects_terminal/work_proj/spohelp/config/../vendor/rails/railties/lib/r

  7. ruby-on-rails - Rails 应用程序中的 Rails : How are you using application_controller. rb 是新手吗? - 2

    刚入门rails,开始慢慢理解。有人可以解释或给我一些关于在application_controller中编码的好处或时间和原因的想法吗?有哪些用例。您如何为Rails应用程序使用应用程序Controller?我不想在那里放太多代码,因为据我了解,每个请求都会调用此Controller。这是真的? 最佳答案 ApplicationController实际上是您应用程序中的每个其他Controller都将从中继承的类(尽管这不是强制性的)。我同意不要用太多代码弄乱它并保持干净整洁的态度,尽管在某些情况下ApplicationContr

  8. ruby - 如何使用 RSpec::Core::RakeTask 创建 RSpec Rake 任务? - 2

    如何使用RSpec::Core::RakeTask初始化RSpecRake任务?require'rspec/core/rake_task'RSpec::Core::RakeTask.newdo|t|#whatdoIputinhere?endInitialize函数记录在http://rubydoc.info/github/rspec/rspec-core/RSpec/Core/RakeTask#initialize-instance_method没有很好的记录;它只是说:-(RakeTask)initialize(*args,&task_block)AnewinstanceofRake

  9. ruby-on-rails - 如何在我的 Rails 应用程序 View 中打印 ruby​​ 变量的内容? - 2

    我是一个Rails初学者,但我想从我的RailsView(html.haml文件)中查看Ruby变量的内容。我试图在ruby​​中打印出变量(认为它会在终端中出现),但没有得到任何结果。有什么建议吗?我知道Rails调试器,但更喜欢使用inspect来打印我的变量。 最佳答案 您可以在View中使用puts方法将信息输出到服务器控制台。您应该能够在View中的任何位置使用Haml执行以下操作:-puts@my_variable.inspect 关于ruby-on-rails-如何在我的R

  10. ruby - 如何根据特征实现 FactoryGirl 的条件行为 - 2

    我有一个用户工厂。我希望默认情况下确认用户。但是鉴于unconfirmed特征,我不希望它们被确认。虽然我有一个基于实现细节而不是抽象的工作实现,但我想知道如何正确地做到这一点。factory:userdoafter(:create)do|user,evaluator|#unwantedimplementationdetailshereunlessFactoryGirl.factories[:user].defined_traits.map(&:name).include?(:unconfirmed)user.confirm!endendtrait:unconfirmeddoenden

随机推荐