我们制作了一个当前使用 WebView 全屏显示 Web 内容的 Android 应用程序。
这可行,但性能很大程度上取决于 WebVeiw 组件的版本,并且在 Chrome 浏览器更新时并不总是更新(在不同的 Android 版本中,WebView 组件和 Chrome 浏览器之间存在相对复杂的关系)。从谷歌关于该主题的演示中我们得出结论,使用 TWA 我们可能会获得更好和更一致的性能,因为 TWA 功能与 Chrome 浏览器一起更新。因此,我们希望在 TWA 不存在时使用 TWA 并回退到 WebView(我们的应用程序在 Android 4.4 和更高版本上运行)。
该应用程序需要执行更多的逻辑,而不仅仅是显示 Web 内容,因此我们无法仅在 Manifest 中定义 TWA/WebView。检查是否能够使用 TWA 并在 MainActivity.java 中启动 TWA 或回退到 WebView。但是,使用 TWA 时,URL/地址栏和底部导航栏仍然可见。
URL/地址栏:据我们所知,要使 TWA 不显示 URL/地址栏,TWA 中显示的域必须有一个与证书“匹配”的 /.well-known/assetlinks.json 文件安卓应用。包含有关此信息和有用链接的两个页面是https://developers.google.com/web/android/trusted-web-activity/integration-guide和https://developer.android.com/training/app-links/验证站点关联。assetlinks.json 是使用https://developers.google.com/digital-asset-links/tools/generator创建的,并通过 https://digitalassetlinks.googleapis.com/v1/statements:list?source.web 成功检查。站点=website_url&relation=delegate_permission/common.handle_all_urls
在虚拟设备(Android API 级别 29,Chrome v83)上测试期间,我们启用了 Chrome 的“在非 root 设备上启用命令行”标志,并在终端中执行了
$ adb shell "echo '_ --disable-digital-asset-link-verification-for-url=\"website_url"' > /data/local/tmp/chrome-command-line"
之后,Chrome 会显示一条警告消息,但 URL/地址栏仍然存在。
底部导航栏:Chrome v80 和更新版本应支持使用沉浸式选项删除底部导航栏:https ://bugs.chromium.org/p/chromium/issues/detail?id=965329#c18 尽管使用描述的创建选项全屏应用程序(https://developer.android.com/training/system-ui/immersive#java)底部导航栏仍在显示。
我们如何删除 URL/地址栏和底部导航栏,基本上我们如何使 TWA 中的网页内容全屏显示?
我们查看了以下示例应用程序,以了解我们需要做什么才能使 TWA 正常工作,但没有发现任何工作(尽管我们错过了一些重要的东西并非不可想象):
- https://github.com/GoogleChromeLabs/svgomg-twa
- https://github.com/tsuyosh/TrustedWebActivitiesDemo
- https://github.com/GoogleChrome/android-browser-helper
- https://github.com/elliatab/TwaDemo
- https://github.com/thanhtungka91/TwaDemoJava
我们项目文件的相关内容:
清单.json
<application
android:name="main_application"
android:hardwareAccelerated="true"
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:banner="@drawable/banner"
android:label="@string/app_name"
android:usesCleartextTraffic="true"
android:networkSecurityConfig="@xml/network_security_config"
android:theme="@style/AppTheme" >
<activity
android:name="main_activity"
android:hardwareAccelerated="true"
android:label="@string/app_name"
android:launchMode = "singleInstance"
android:keepScreenOn="true" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
</application>
MainActivity.java
public class MainActivity extends Activity implements IServiceCallbacks {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Set up looks of the view
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
requestWindowFeature(Window.FEATURE_NO_TITLE);
View decorView = getWindow().getDecorView();
decorView.setOnSystemUiVisibilityChangeListener(new View.OnSystemUiVisibilityChangeListener() {
@Override
public void onSystemUiVisibilityChange(int visibility) {
// Note that system bars will only be "visible" if none of the
// LOW_PROFILE, HIDE_NAVIGATION, or FULLSCREEN flags are set.
if ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) {
// bars are visible => user touched the screen, make the bars disappear again in 2 seconds
Handler handler = new Handler();
handler.postDelayed(new Runnable() {
public void run() {
hideBars();
}
}, 2000);
} else {
// The system bars are NOT visible => do nothing
}
}
});
decorView.setKeepScreenOn(true);
setContentView(R.layout.activity_main);
// create Trusted Web Access or fall back to a WebView
String chromePackage = CustomTabsClient.getPackageName(this, TrustedWebUtils.SUPPORTED_CHROME_PACKAGES, true);
if (chromePackage != null) {
if (!chromeVersionChecked) {
TrustedWebUtils.promptForChromeUpdateIfNeeded(this, chromePackage);
chromeVersionChecked = true;
}
if (savedInstanceState != null && savedInstanceState.getBoolean(MainActivity.TWA_WAS_LAUNCHED_KEY)) {
this.finish();
} else {
this.twaServiceConnection = new MainActivity.TwaCustomTabsServiceConnection();
CustomTabsClient.bindCustomTabsService(this, chromePackage, this.twaServiceConnection);
}
} else {
// set up WebView
}
}
private class TwaCustomTabsServiceConnection extends CustomTabsServiceConnection {
public void onCustomTabsServiceConnected(ComponentName componentName, CustomTabsClient client) {
CustomTabsSession session = MainActivity.this.getSession(client);
CustomTabsIntent intent = MainActivity.this.getCustomTabsIntent(session);
Uri url = Uri.parse("http://our_url");
TrustedWebUtils.launchAsTrustedWebActivity(MainActivity.this, intent, url);
MainActivity.this.twaWasLaunched = true;
}
public void onServiceDisconnected(ComponentName componentName) {
}
}
protected void hideBars() {
if (getWindow() != null) {
View decorView = getWindow().getDecorView();
decorView.setSystemUiVisibility(
// Hide the nav bar and status bar
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_FULLSCREEN
// Set the content to appear under the system bars so that the
// content doesn't resize when the system bars hide and show.
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE
// Enables regular immersive mode
| View.SYSTEM_UI_FLAG_IMMERSIVE
);
}
// Remember that you should never show the action bar if the
// status bar is hidden, so hide that too if necessary.
ActionBar actionBar = getActionBar();
if (actionBar != null) {
actionBar.hide();
}
}
}
构建.gradle
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
jcenter()
google()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.0.0'
}
}
allprojects {
repositories {
jcenter()
google()
maven { url "https://jitpack.io" }
}
}
构建.gradle
apply plugin: 'com.android.application'
android {
compileSdkVersion 29
buildToolsVersion '29.0.2'
defaultConfig {
applicationId application_id
minSdkVersion 19
targetSdkVersion 29
}
buildTypes {
release {
minifyEnabled true
debuggable false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
}
debug {
minifyEnabled false
debuggable true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
jniDebuggable true
renderscriptDebuggable true
renderscriptOptimLevel 3
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.leanback:leanback:1.0.0'
implementation 'androidx.recyclerview:recyclerview:1.1.0'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.webkit:webkit:1.2.0'
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'com.github.GoogleChrome.custom-tabs-client:customtabs:master'
}
/.well-known/assetlinks.json
[
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target" : { "namespace": "android_app", "package_name": "our package name",
"sha256_cert_fingerprints": ["11:22:33:44"]
}
},
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target" : { "namespace": "android_app", "package_name": "our other package name",
"sha256_cert_fingerprints": ["11:22:33:44"]
}
}
]