1. 程式人生 > >解決安卓4.4webview的相容性問題

解決安卓4.4webview的相容性問題

放棄WebView,使用Crosswalk做富文字編輯器

原文連結:http://segmentfault.com/a/1190000002919135

為什麼放棄WebView

Android WebView做普通瀏覽還好,做富文字編輯器,常常會遇到各種奇葩的bug,而且很難修復。儘管Google在版本迭代中不斷修復bug,但依舊沒法用它來做富文字編輯。

Kitcat的改變

Google為了加強WebView的功能,在Kitcat引入了Chromium核心。但還是存在著編輯的bug。

我所知道的一個bug是:

Kitcat版WebView在刪除Html標籤時處理不好,例如 <img>標籤,就無法刪除。點選刪除時直接越過此元素,將游標定位在圖片前方,對圖片不做處理。

當然,這個bug在Android 5.0 修復了。

Lollipop新策略

Although WebView has been based on Chromium since Android 4.4, the Chromium layer is now updatable from Google Play.As new versions of Chromium become available, users can update from Google Play to ensure they get the latest enhancements and bug fixes for WebView, providing the latest web APIs and bug fixes for apps using WebView on Android 5.0 and higher.

可見在Lollipop裡,可以通過GooglePlay來更新Chromium核心。

但是問題來了:

  • 國內容易更新麼?
  • 如果不是自動更新,使用者會手動更新麼?當然GooglePlay是自動更新,那國內手機沒有自己市場的廠商呢?
  • Lollipop以前的版本怎麼辦? Lollipop目前只有很少使用者可以更新。

探索新的富文字編輯方案

顯然,即便是有了Lollipop的解決方案,但問題依然很多。我們還是需要一個替代方案,來保證我們在所有的Android手機上表現一致。

這個方案就是在應用中整合Chromium。

由於自己編譯Chromium的難度較大,於是轉而尋找編譯好的Chromium庫來使用。

需要宣告的是:Chromium核心只能在Android 4.0以上才能使用,之後提到的所有Chromium庫都只能在4.0以上平臺使用。

過渡方案

最初在尋找替代方案的時候,應該是2013年10月左右,找到了兩個Chromium庫:

  1. 這個庫封裝的較好,但是有一個致命的bug是不能滾動。

    README中宣告:

    Attempting to scroll the view (by swiping a finger across the screen) does not update the displayed image.

    However, internally, the view is scrolled.

    This can be seen by displaying a stack of buttons and trying to click on the topmost one.

    This issue makes ChromeView mostly unusable in production.

    注:這個庫的README最新聲明裡面推薦了Crosswalk,作者還是很用心的。

  2. 這個庫整體穩定,不存在上面的bug。用它作為編輯器差不多一年,沒有出現什麼問題。

    但在今年6、7月的時候,突然間發現在三星新出的幾款平板上(搭載了Kitcat)表現為花屏,螢幕上出現了各種顏色的橫條,無法進行編輯。其他搭載了Kitcat的手機當時沒有發現過什麼問題。

    這裡說一下這個庫,自從作者看到Kitcat使用Chromium後,作者就宣告不再更新了,其實差不多一年前就已經不更新了。

    這個庫使用起來比較麻煩,需要自己再進行封裝,甚至連onPageFinished都需要自己來做。

可以看到,上面的替代方案,到今年6、7月,實際上已經無法使用。

而且非組織維護的程式碼,通常都有些不可靠的意味。

於是不得不繼續尋找替代方案。終於在Google I/O上看到了希望 —— Crosswalk

Crosswalk入門

上面的連結可以看到Crosswalk的介紹,Crosswalk種種吹牛逼的描述我就不寫了。寫一下我的使用感受:

  1. 不用費力搞什麼自己封裝了,直接像用WebView一樣使用。
    在使用android-chromium這個庫時,不僅要自己封裝API來方便使用,還要操心Chromium的初始化,甚至還需要在清單檔案裡寫一堆關於Chromium的東西,用來幫助Chromium建立單獨的程序(Crosswalk只會建立Chromium的執行緒,不需要獨立程序)。
  2. Crosswalk由組織維護,比個人維護強多了。
  3. 跟隨最新的Chromium不斷更新,js等不用擔心有函式沒法使用。而且不斷更新過程中,肯定也會修復以前存在的bug,穩定性也是不用擔心的。

最新穩定版Crosswalk基於Chromium38編譯。

注:此庫也可以配合Cordova(PhoneGap)使用。

OK,感受說完,上教程。

整合到應用中

  1. 下載zip包,解壓後匯入。
  2. 關聯此Library。
  3. 在清單檔案中寫入下列許可權

    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
    <uses-permission android:name="android.permission.CAMERA" />
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
    <uses-permission android:name="android.permission.RECORD_AUDIO" />
    <uses-permission android:name="android.permission.WAKE_LOCK" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

    注:使用過程中,觀察Logcat可以看到報需要藍芽許可權,可以不用管它,不新增藍芽許可權可以正常使用。此外,使用XWalkView必須開啟硬體加速。

    XWalkView needs hardware acceleration to render web pages. As a result, the AndroidManifest.xml of the caller's app must be appended with the attribute "android:hardwareAccelerated" and its value must be set as "true".

    android:hardwareAccelerated : The default value is "true" if you've set either minSdkVersion or targetSdkVersion to "14" or higher; otherwise, it's "false".

    在清單檔案Application中宣告即可。

    <application android:name="android.app.Application" android:label="XWalkUsers"
     android:hardwareAccelerated="true">

基本使用

Crosswalk中用來替代WebView的控制元件叫XWalkView。

layout檔案寫法

和其他自定義控制元件一樣。

<org.xwalk.core.XWalkView android:id="@+id/activity_main"
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="fill_parent"
  android:layout_height="fill_parent">
</org.xwalk.core.XWalkView>

程式碼中使用

重中之重:防止記憶體洩漏

和其他Android的控制元件不同,這個類需要監聽系統事件。例如:生命週期、intent、Activity result。

控制元件內建的Web引擎需要獲取並處理這些資訊。並且當XWalkView 不再需要使用的時候,在onDestroy方法中XWalkView必須顯式的呼叫destroy方法,否則容易造成Web引擎的記憶體洩漏。

原文如下:

Unlike other Android views, this class has to listen to system events like application life cycle, intents, and activity result. The web engine inside this view need to get and handle them. And the onDestroy() method of XWalkView MUST be called explicitly when an XWalkView won't be used anymore, otherwise it will cause the memory leak from the native side of the web engine. It's similar to the destroy() method of Android WebView.

這段文字來自XWalkView官方API文件。奇怪的是官方的範例中並沒有在意這些事情,直接像WebView一樣使用,更沒有使用destroy方法。考慮到之前使用android-chromium庫也是需要顯式呼叫。這裡還是加上,避免記憶體洩漏。

   import android.app.Activity;
   import android.os.Bundle;

   import org.xwalk.core.XWalkView;

   public class MyActivity extends Activity {
   private XWalkView mXWalkView;
   @Override
   protected void onCreate(Bundle savedInstanceState) {
   		super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mXWalkView = (XWalkView) findViewById(R.id.activity_main);
mXWalkView.load("http://crosswalk-project.org/", null);
   }

   @Override
   protected void onPause() {
   super.onPause();
   if (mXWalkView != null) {
   mXWalkView.pauseTimers();
   mXWalkView.onHide();
   }
   }

   @Override
   protected void onResume() {
   super.onResume();
   if (mXWalkView != null) {
   mXWalkView.resumeTimers();
   mXWalkView.onShow();
   }
   }

   @Override
   protected void onDestroy() {
   super.onDestroy();
   if (mXWalkView != null) {
   mXWalkView.onDestroy();
   }
   }

   @Override
   protected void onActivityResult(int requestCode, int resultCode, Intent data) {
   if (mXWalkView != null) {
   mXWalkView.onActivityResult(requestCode, resultCode, data);
   }
   }

   @Override
   protected void onNewIntent(Intent intent) {
   if (mXWalkView != null) {
   mXWalkView.onNewIntent(intent);
   }
   }
   }

loadUrl去哪了?

上面的程式碼中其實已經劇透了,使用load方法即可。

// url
mXWalkView.load("http://crosswalk-project.org/", null);

// this loads a file from the assets/ directory
mXWalkView.load("file:///android_asset/index.html", null);

public void load (String url, String content)

Load a web page/app from a given base URL or a content. If url is null or empty and content is null or empty, then this function will do nothing. If content is not null, load the web page/app from the content. If content is not null and the url is not set, return "about:blank" ifi calling getUrl(). If content is null, try to load the content from the url. It supports URL schemes like 'http:', 'https:' and 'file:'. It can also load files from Android assets, e.g. 'file:///android_asset/'.

Parameters

url the url for web page/app.

content the content for the web page/app. Could be empty.

WebViewClient?

對應WebView的WebViewClient,XWalkView中有XWalkResourceClient。

mXWalkView.setResourceClient(new XWalkResourceClient(mXWalkView){
@Override
public void onLoadFinished(XWalkView view, String url) {
super.onLoadFinished(view, url);
}
@Override
public void onLoadStarted(XWalkView view, String url) {
super.onLoadStarted(view, url);
}
});

呼叫JavaScript

mXWalkView = (XWalkView) findViewById(R.id.activity_main);
XWalkSettings webSettings = mXWalkView.getSettings();
//Tells the WebView to enable JavaScript execution. 
webSettings.setJavaScriptEnabled(true);

執行js

mXWalkView.load("javascript:document.body.contentEditable=true;", null);

當然,按照Kitcat引入的方式,使用evaluateJavascript方法也是可以的。(大神們推薦)

JavaScript回撥Java

  1. 定義js回撥介面

    public class JsInterface {
    public JsInterface() {
    }
    @JavascriptInterface
    public String sayHello() {
    return "Hello World!";
    }
    }

    Caution: If you've set your targetSdkVersion to 17 or higher, you must add the @JavascriptInterface annotation to any method that you want available to your JavaScript (the method must also be public). If you do not provide the annotation, the method is not accessible by your web page when running on Android 4.2 or higher.From developer.android.com

    備註:這裡的 @JavaScriptInterface所在的包是 import org.xwalk.core.JavascriptInterface;

  2. XWalkView設定JavaScript可用且繫結物件

    mXWalkView = (XWalkView) findViewById(R.id.activity_main);
    XWalkSettings webSettings = mXWalkView.getSettings();
    //Tells the WebView to enable JavaScript execution. 
    webSettings.setJavaScriptEnabled(true);
    //繫結
    mXWalkView.addJavascriptInterface(new JsInterface(), "NativeInterface");
  3. 呼叫html執行JavaScript或直接執行Javascript呼叫Java

    mXWalkView.load("file:///android_asset/index.html", null);

    index.html原始碼:

    <a href="#" onclick="clicked()">Say Hello</a>
    <script>
    function clicked() {
    alert(NativeInterface.sayHello());
    }
    </script>

高階使用

除錯

Kitcat開始,Android提供了和Chrome聯調功能。可以很方便的在Chrome中除錯WebView中的程式碼。

Crosswalk使用Chromium核心當然也具備這個功能。

開啟除錯的語句如下:

  // turn on debugging
    XWalkPreferences.setValue(XWalkPreferences.REMOTE_DEBUGGING, true);

對於Crosswalk來說,這個設定是全域性的。

使用動畫或者設定隱藏可見注意

預設XWalkView不能使用動畫,甚至setVisibility也不行。

XWalkView represents an Android view for web apps/pages. Thus most of attributes for Android view are valid for this class. Since it internally uses android.view.SurfaceView for rendering web pages by default, it can't be resized, rotated, transformed and animated due to the limitations of SurfaceView. Alternatively, if the preference key ANIMATABLE_XWALK_VIEW is set to True, XWalkView can be transformed and animated because TextureView is intentionally used to render web pages for animation support. Besides, XWalkView won't be rendered if it's invisible.

開啟動畫模式:

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

// ANIMATABLE_XWALK_VIEW preference key MUST be set before XWalkView creation.
XWalkPreferences.setValue(XWalkPreferences.ANIMATABLE_XWALK_VIEW, true);

setContentView(R.layout.animatable_xwview_layout);
}
@Override
public void onDestroy() {
super.onDestroy();

// Reset the preference for animatable XWalkView.
XWalkPreferences.setValue(XWalkPreferences.ANIMATABLE_XWALK_VIEW, false);
}

由於設定也像除錯一樣是全域性的,在onDestroy時記得關閉。

暫停JS timer

html程式碼

<!DOCTYPE html>
<html>
<body>

<p>A script on this page starts this clock:</p>
<p id="demo"></p>

<script>
  var myVar = setInterval(function(){ myTimer(); }, 1000);

  function myTimer()
  {
    var d = new Date();
    var t = d.toLocaleTimeString();
    document.getElementById("demo").innerHTML = t;
  }
</script>

</body>
</html>

XWalkView對應方法:

mButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (mXWalkView != null) {
if (!isPaused) {
// Pause JS timer
mXWalkView.pauseTimers();
isPaused = true;
mButton.setImageResource(android.R.drawable.ic_media_play);
} else {
// Resume JS timer
mXWalkView.resumeTimers();
isPaused = false;
mButton.setImageResource(android.R.drawable.ic_media_pause);
}
}
}
});

這也在防止記憶體洩漏,監聽系統事件示例程式碼中提到過:

@Override
protected void onPause() {
   super.onPause();
   if (mXWalkView != null) {
   mXWalkView.pauseTimers();
   mXWalkView.onHide();
   }
}

@Override
protected void onResume() {
   super.onResume();
   if (mXWalkView != null) {
   mXWalkView.resumeTimers();
   mXWalkView.onShow();
   }
}

歷史記錄

mPrevButton.setOnClickListener(new OnClickListener() {
    @Override
    public void onClick(View v) {
// Go backward
if (mXWalkView != null &&
mXWalkView.getNavigationHistory().canGoBack()) {
    mXWalkView.getNavigationHistory().navigate(
    XWalkNavigationHistory.Direction.BACKWARD, 1);
}
XWalkNavigationItem navigationItem = mXWalkView.getNavigationHistory().getCurrentItem();
showNavigationItemInfo(navigationItem);
    }
});

mNextButton.setOnClickListener(new OnClickListener() {
    @Override
    public void onClick(View v) {
// Go forward
if (mXWalkView != null &&
mXWalkView.getNavigationHistory().canGoForward()) {
    mXWalkView.getNavigationHistory().navigate(
    XWalkNavigationHistory.Direction.FORWARD, 1);
}
XWalkNavigationItem navigationItem = mXWalkView.getNavigationHistory().getCurrentItem();
showNavigationItemInfo(navigationItem);
    }
});



private void showNavigationItemInfo(XWalkNavigationItem navigationItem){
    url = navigationItem.getUrl();// Get the url of current navigation item.
    originalUrl = navigationItem.getOriginalUrl();// Get the original url of current navigation item
    title = navigationItem.getTitle();

    text1.setText(title);
    text2.setText(url);
    text3.setText(originalUrl);
}

自動視訊暫停

// The web page below will display a video.
// When home button is pressed, the activity will be in background, and the video will be paused.
mXWalkView.load("http://www.w3.org/2010/05/video/mediaevents.html", null);

loadAppFromManifest

mXWalkView.loadAppFromManifest("file:///android_asset/manifest.json", null);

manifest.json

{
  "name": "ManifestTest",
  "start_url": "index.html",
  "description": "Manifest test",
  "version": "1.0.0"
}