如何优雅地从浏览器打开本地应用

来源:互联网 发布:淘宝远程控制电脑 编辑:程序博客网 时间:2024/05/16 13:50

某一天从微信打开知乎链接的时候,发现竟然可以从浏览器直接打开知乎应用并且进入到特定的页面,觉得十分神奇,于是就研究了一下怎么来实现这个功能。在实现的过程中,发现要完成知乎那样的体验,还是需要下很大功夫,下面我将详细介绍如何实现到知乎那样的完美体验。

准备知识

相信Android开发者都会知道,在应用内跳转页面,我们肯定会用到Intent。使用Intent跳转页面有显式跳转和隐式跳转两种方式。

Intent intent = new Intent();intent.setClass(this, MainActivity.class);startActivity(intent);

典型的显式跳转就如同上面的代码一样,如果需要携带数据的话,我们也有两种方式。一方面我们可以调用Intent的putExtra系列方法,通过键值对的方式把数据传递过去。Intent支持传递的数据包括基本类型数据、字符串、序列化以及各自的数组,有意思的是Intent支持Parcelable数组但不支持Serializable数组。然后在跳转的页面通过相应的get方法把数据拿出来即可。

另一方面,我们也可以用Intent的setData方法将统一资源标识符Uri传递过去,然后在接收端解析Uri来拿出数据。对于一个Uri,主要由以下几部分组成:

scheme://host:port/path?query

相应地,我们便能从Uri中把各个部分的信息拿出来。

而隐式跳转则是通过Intent的action来实现。典型的便是我们在桌面点击了应用的图标,然后应用启动进入我们的第一个页面。相信大家都知道对于应用自动的第一个页面,我们在AndroidManifest.xml文件中对Activity注册时,会有这么一段代码。

<intent-filter>       <action android:name="android.intent.action.MAIN"/>       <category android:name="android.intent.category.LAUNCHER"/></intent-filter>

实际上应用从点击图标到启动第一个页面的过程相当复杂,如果大家有兴趣的话可以去看老罗的博客
Android应用程序启动过程源代码分析。
http://blog.csdn.net/luoshengyang/article/details/6689748

从源代码的分析来看,桌面图标保存了相应应用的启动intent,该intent保存了AndroidManifest.xml的信息。因此我们自定义隐式启动页面时,只需要在AndroidManifest.xml中相应的Activity配置相应的action信息,然后调用的时候设置相应的action便可启动相应Activity。

比如我们之前开发应用时,想要调用系统拨打电话应用,我们并不知道拨打应用Activity的具体信息,所以只有通过隐式方式来调用。

Intent intent = new Intent();intent.setAction(Intent.ACTION_DIAL);intent.setData(Uri.parse("tel:" + 233333));intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);startActivity(intent);

嗯,有了这些基础知识,我们便可以来实现浏览器打开本地App了。

浏览器打开本地App

常规下,浏览器打开一个url要么是http要么是https,那么,我们要怎么让浏览器能识别到一个url是指向我们的应用的呢?那我们来看看Url吧,仔细看看一个url链接其实就是一个Uri,有scheme,有host,有query等等。scheme实际上相当于一种协议,比如http,系统识别到这个协议便会交由网络部分去处理这个请求。那么系统能否识别自定义的scheme呢?答案是肯定的。我们可以在自己的应用内自定义一种scheme,系统安装我们的应用后便会在某个地方进行scheme注册,这样当有相应的scheme请求的时候,便能将请求导向到我们的应用。有意思的是如果在多个应用中配置了相同的scheme的话,从浏览器请求时便会提示用户选择某一个应用来打开。

那么,我们来看看基本做法怎么做吧。

  • 配置要打开的页面,比如我们在应用的启动页面配置相应的scheme
<intent-filter>    <action android:name="android.intent.action.MAIN"/>    <category android:name="android.intent.category.LAUNCHER"/></intent-filter><intent-filter>    <action android:name="android.intent.action.VIEW" />    <category android:name="android.intent.category.DEFAULT" />    <category android:name="android.intent.category.BROWSABLE" />    <data android:scheme="myapp" /></intent-filter>
  • 配置浏览器访问的url
myapp://?id=1

为了简单,就没有配置host信息,读者可以自己将host加上即可。这样,当浏览器访问这个url的时候便会打开我们的App。那么怎么拿传递的数据呢,不要方,看代码。

Intent intent = getIntent();if (Intent.ACTION_VIEW.equals(intent.getAction())) {  Uri data = intent.getData();  String id = data.getQueryParameter("id");}

之所以要作一个判断,是为了区别该界面是否是由浏览器打开的,然后我们便可以通过Uri的getQueryParameter来获取传递的信息了,当然,你还可以拿到Uri的其他信息,这里就不说明了。

至此,通过浏览器打开本地App功能算是完成了。相信读者看到这里也会说,这么简单,我分分钟便能搞定。没错,做到这个地步,相信只要有点Android开发知识的同学都能分分钟搞定,那么我在这里提几个问题:

  • 假如我们从浏览器打开了应用,按下HOME键,然后又从桌面点击了应用图标,这时候会发生什么?
  • 假如应用之前已经打开了某个页面,然后我们又从浏览器重新打开了应用,这时候按返回键,我们还能回到之前打开的页面么?
  • 我们通过浏览器打开页面一般都会是二级甚至三级页面,如果之前没有打开过应用,那么直接按返回键就会退出应用,这似乎用户体验不太友好,怎么解决?

如果你能把这三个问题解决了,那么从浏览器打开本地应用就算掌握了。哈哈,没那么简单吧,要解答上面三个问题,必须熟练掌握Activity的启动方式并灵活应用,下面我来介绍我的方案吧。

仿知乎浏览器打开本地应用

其实我上面抛出的三个问题就是从知乎的体验来提出的,通过浏览器打开知乎的某个问题的回答页面,假如之前知乎是打开的,则按返回键能直接回到之前的页面,如果之前知乎没有打开过,则按返回键会回到主页面。当通过浏览器打开页面后,按HOME键之后再重新点击图标,知乎会直接打开到浏览器打开的那个页面。所以知乎是完美解决了以上三个问题的。那么,我们来看如果不做任何处理,会是什么效果。

由于手机界面截图太大了,就不贴图了,我们从打印日志来看界面的启动情况吧。分别在onCreate方法和onResume方法中放入日志打印代码。

Log.d("Activity start onCreate", getIntent().getAction());Log.d("Activity start onResume", getIntent().getAction());

首先完全退出应用,然后通过浏览器调用url唤起界面,然后按下HOME键,点击图标重新进入,我们可以得到如下所示的日志
这里写图片描述

可以看出点击图标后会重新创建应用,之前在浏览器打开的界面找不到了。同样对于第二个问题,如果我们不做任何处理,浏览器唤起的页面按返回键是回不到之前打开的界面的。那么,这是为什么呢?

篇幅所限,就不介绍Activity的四种启动方式了。首先我们来了解一下任务栈这个概念吧。默认情况下,如果没有对Activity设置TaskAffinity属性,一个应用的所有Activity都是运行在同一个任务栈的,任务栈的名称为应用的PackageName。如果从应用A启动应用B的某个Activity C,则C会运行在A的任务栈中。说到这里,相信大家应该明白为啥了吧。从桌面启动的应用运行在应用本身的任务栈中,而从浏览器打开的界面则运行在浏览器的任务栈中,两个任务栈是分开的,所以在情景一,会重新创建出新的任务栈来打开应用,而在情景二中,由于浏览器的后台任务栈是桌面,在浏览器的任务栈中按返回键当然不能回到本地应用的任务栈咯而是回到桌面。

那么,知道了问题的原因,怎么来解决这个问题呢?由于从桌面点击应用会创建自己的应用栈,那么如果我们可以把浏览器任务栈中的界面移动到应用本身的任务栈中,则不就解决第一个问题了么。那么怎么将Activity从其他任务栈中移到自己的任务栈中呢?方法很简单,只需要在相应的Activity中配置allowTaskReparenting属性为true即可。

android:allowTaskReparenting="true"

设置了该属性的Activity在应用真正启动时,会将在其他任务栈中移动到自己的任务栈中来,由于移动过来的界面处与栈顶,所以会直接显示之前在浏览器中打开的界面,是不是很厉害。

对于场景二,应用自己打开的Activity在自己的任务栈中,由于我们没办法把已启动的Activity移动到浏览器的任务栈中,所以只有另辟蹊径。我们知道浏览器唤起的那个界面如果不做任何处置,则会在浏览器的任务栈中新建Activity,那么我们是不是可以指定唤起的Activity的打开方式为SingleTask呢,因为对于SingleTask类型启动的界面来说,如果在本任务栈中不存在对应的Activity的话,会在新的任务栈中新建Activity。所以当浏览器唤起界面时,由于浏览器任务栈中没有对应Activity,所以会在本地应用所在任务栈去创建Activity,这样就链接到了本地应用的任务栈,并且将本来处于在后台任务栈的应用任务栈移动到了前台任务栈。

由于Android的返回机制是只有在某个任务栈为空时,才会退到上一个任务栈,所以按返回键的效果便是在本地应用栈中退出相应的Activity,直到任务栈为空时,再把浏览器所在的任务栈移动到前台。当然有个小问题是如果我们在配置文件中声名Activity为SingleTask启动方式,如果该任务栈中存在相应的Activity,则会把该Activity之上的所有Activity清除掉。而通过给Intent配置New_Task的flag方式默认不会清除Activity之上的其他Activity,除非添加了Clear_Top的flag。由于我们不能控制浏览器设置的Intent,所以没法添加New_Task的flag,所以直接在配置文件中设置SingleTask并行不通。

考虑到应用一般都会有启动页,并且启动后会自动finish,那么我们可以利用该页面做做文章。我们通过浏览器打开启动页,然后在启动页面通过New_Task的方式去启动主页面,再逐级打开二级、三级页面。这样既不会清楚已经打开的Activity,又可以实现任务栈的移动。法很简单,只需要在启动页面的OnCreate方法中根据Intent的类型做页面跳转即可。

Intent intent = getIntent();if (Intent.ACTION_VIEW.equals(intent.getAction())) {  Intent intent1 = new Intent();  intent1.setClass(this, MainActivity.class);  intent1.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);  startActivity(intent1);  finish();} else if (Intent.ACTION_MAIN.equals(intent.getAction())) {  Intent intent1 = new Intent();  intent1.setClass(this, MainActivity.class);  startActivity(intent1);  finish();}

这样便解决了第二个问题,在浏览器中按返回键能回到之前打开的页面。对于第三个问题,就更简单了,我们只需要有个AppManager类来管理已经打开的界面,然后在启动页面判断应用是否有打开过的页面,如果有,则直接跳转到需要唤起的界面,如果之前没有打开界面,则先跳转到主界面,再跳转到需要唤起的界面,这样按返回键还能回到主界面不至于直接退出应用。这里值得注意的是跳转到主界面不要新建Intent,直接沿用获得的Intent重新设置跳转信息即可。思路比较简单,就不再赘述了。

if (intent != null && Intent.ACTION_VIEW.equals(intent.getAction())) {  int activitySize = AppManager.getAppManager().getSize();  if (activitySize > 1) {    jumpFromBrowser(intent.getData());  } else {    intent.setClass(this, MainActivity.class);    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);    startActivity(intent);  }  finish();  return;}

这样三个问题都解决了,我们来简单回味一下整个优化过程:

  • 通过设置android:allowTaskReparenting=”true”属性将其他任务栈中存在的Activity在应用启动时移动到自己的任务栈中,实现按HOME键启动应用能回到浏览器唤起的页面。
  • 通过Splash页面做过渡,通过New_Task的方式启动浏览器唤起的页面,使得在浏览器中按返回键能接着本地应用已打开的页面。
  • 通过判断Activity的数量决定是否是直接唤起页面还是先唤起主界面再打开需要打开的界面使得按返回键不至于直接从二级或者三级界面退出应用,提高用户体验。

文字写得有点啰嗦,那来一张简单明了的图吧。按照这个设计流程,并且在AndroidManifest.xml中将MainActivity以及浏览器需要唤起的Activity设置android:allowTaskReparenting=”true”属性,你也可以实现知乎那样的浏览器唤起应用。

流程图
这里写图片描述

能看到这里的都是真爱啊,希望大家在Android的道路上越走越远,越走越远,越走越远!

文章转自 zhuimengfb 的博客
查看原文