Android H5混合開發(5):封裝Cordova View, 讓Fragment、彈框、Activity自由使用Cordova


近期,有同事咨詢如何在Fragment中使用Cordova,看了下Cordova源碼,官方並沒有提供包含Cordova Webview的Fragment,以供我們繼承。

上網查詢了一下,也有幾篇文章講解Fragment中如何使用Cordova,不過Cordova邏輯與Fragment邏輯耦合太深,不太適用於常規項目開發。

通過分析CordovaActivity的源碼實現,我們只需要將Cordova封裝成自定義View就可以了。后面的演示,咱們還是基於之前的工程吧,代碼會在后面分享給大家的。

CordovaView實現的目標:

應像系統Webview一樣,與頁面邏輯解耦,且方便使用

1、CordovaView的邏輯應獨立;
2、能在Fragment中使用;
3、能在Activity中使用;
4、能在彈框中使用;

CordovaView封裝

因我這邊時間比較緊張,所以不帶領大家去分析CordovaActivity的實現原理了,此處直接貼出自定義CordovaView的源碼吧:

自定義控件內容,主要包含:
1、CordovaWebView的初始化、UI、URL加載、配合Activity、Fragment生命周期等;
2、讀取Cordova配置文件,設置頁面相關屬性;
3、Crodova接口實現(無需自己寫,直接使用Cordova自帶的CordovaInterfaceImpl)
4、異常消息回調,以便於使用方自己控制錯誤提示等;
5、返回真實Webview控件,以便於使用方自行控制goback、reload、websetting等;
6、其他...
CordovaView.java實現 (重要的注釋都已加上了):
package com.ccc.ddd;

import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.Color;
import android.os.Bundle;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.webkit.WebView;
import android.widget.FrameLayout;
import android.widget.RelativeLayout;

import org.apache.cordova.Config;
import org.apache.cordova.ConfigXmlParser;
import org.apache.cordova.CordovaInterfaceImpl;
import org.apache.cordova.CordovaPlugin;
import org.apache.cordova.CordovaPreferences;
import org.apache.cordova.CordovaWebView;
import org.apache.cordova.CordovaWebViewEngine;
import org.apache.cordova.CordovaWebViewImpl;
import org.apache.cordova.PluginEntry;
import org.apache.cordova.PluginManager;
import org.apache.cordova.engine.SystemWebView;
import org.apache.cordova.engine.SystemWebViewEngine;
import org.json.JSONException;
import org.json.JSONObject;

import java.lang.reflect.Field;
import java.util.ArrayList;

/**
 * 自定義Cordova控件
 * 1、可用於Activity、Fragment集成
 * 2、可在布局xml文件中引入
 * 3、可在代碼中使用new關鍵字創建實例
 *
 * <p>
 * 使用示例:
 * String launchUrl = "file:///android_asset/www/index.html";
 * CordovaView cordovaView = view.findViewById(R.id.cv);
 * cordovaView.initCordova(getActivity());
 * cordovaView.loadUrl(launchUrl);
 * <p>
 * 
 * 作者:齊xc
 * 日期:2019.09.15
 */
public class CordovaView extends RelativeLayout {
    //頁面對象
    private Activity activity;
    //Cordova瀏覽器對象: 初始化、UI布局控制、url加載、生命周期(開始、暫停、銷毀...)
    protected CordovaWebView appView;
    //Cordova配置對象: 各類配置信息讀取、設置、使用
    protected CordovaPreferences preferences;
    //Cordova接口實現對象: 消息處理(頁面跳轉、頁面數據存取、權限申請...)
    protected CordovaInterfaceImpl cordovaInterface;
    //是否保持運行
    protected boolean keepRunning = true;
    //是否沉浸式
    protected boolean immersiveMode;
    //默認啟動url
    protected String launchUrl;
    //插件實體類集合
    protected ArrayList<PluginEntry> pluginEntries;
    //接收錯誤的監聽器(用於回調頁面加載錯誤,如:頁面未找到等等。使用方需先調用方法:setOnReceivedErrorListener())
    private OnReceivedErrorListener errorListener;

    /**
     * 構造函數
     *
     * @param context 上下文
     */
    public CordovaView(Context context) {
        super(context);
    }

    /**
     * 構造函數
     *
     * @param context 上下文
     * @param attrs   屬性
     */
    public CordovaView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    /**
     * 初始化Cordova
     *
     * @param activity 頁面
     */
    public void initCordova(Activity activity) {
        this.activity = activity;

        //加載配置信息
        loadConfig();

        //設置頁面是否全屏
        if (preferences.getBoolean("SetFullscreen", false)) {
            preferences.set("Fullscreen", true);
        }
        if (preferences.getBoolean("Fullscreen", false)) {
            if (!preferences.getBoolean("FullscreenNotImmersive", false)) {
                immersiveMode = true;
            } else {
                activity.getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
                        WindowManager.LayoutParams.FLAG_FULLSCREEN);
            }
        } else {
            activity.getWindow().setFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN,
                    WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
        }

        //實例化接口實現
        cordovaInterface = makeCordovaInterface();

        //設置背景為白色
        activity.getWindow().getDecorView().setBackgroundColor(Color.WHITE);

        //初始化
        initCordova();
    }

    /**
     * 加載配置信息
     * 1.讀取默認啟動的url  file:///android_asset/www/index.html
     * 2.讀取res/xml/config.xml文件,獲得插件集合 pluginEntries
     */
    private void loadConfig() {
        ConfigXmlParser parser = new ConfigXmlParser();
        parser.parse(activity);
        preferences = parser.getPreferences();
        preferences.setPreferencesBundle(activity.getIntent().getExtras());
        launchUrl = parser.getLaunchUrl();
        pluginEntries = parser.getPluginEntries();
        try {
            //通過反射處理
            Field field = Config.class.getDeclaredField("parser");
            field.setAccessible(true);
            field.set(null, parser);
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

    /**
     * 初始化
     */
    private void initCordova() {
        //實例化webview對象
        appView = makeWebView();
        //將webview加載到頁面中,並根據參數配置其屬性
        createViews();
        //如果"實例化接口"為空
        if (!appView.isInitialized()) {
            //appview初始化
            //初始化插件管理
            //初始化消息隊列
            //初始化橋模塊
            //......
            appView.init(cordovaInterface, pluginEntries, preferences);
        }
        //設置插件管理器
        //設置onActivityResult消息回調
        //設置Activity銷毀處理
        cordovaInterface.onCordovaInit(appView.getPluginManager());
    }


    /**
     * 創建views
     */
    @SuppressWarnings({"deprecation", "ResourceType"})
    private void createViews() {
        //appView.getView()指SystemWebViewEngine的SystemWebView,繼承自android.webkit.WebView
        appView.getView().setId(100);
        appView.getView().setLayoutParams(new FrameLayout.LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.MATCH_PARENT));

        //設置當前視圖為SystemWebViewEngine的SystemWebView
        //setContentView(appView.getView());
        this.removeAllViews();
        this.addView(appView.getView(), new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));

        //如果preferences有配置背景色,則設置webview背景色
        if (preferences.contains("BackgroundColor")) {
            try {
                int backgroundColor = preferences.getInteger("BackgroundColor", Color.BLACK);
                // Background of activity:
                appView.getView().setBackgroundColor(backgroundColor);
            } catch (NumberFormatException e) {
                e.printStackTrace();
            }
        }

        //webview獲得焦點(不受touch限制)
        appView.getView().requestFocusFromTouch();
    }

    /**
     * 構建CordovaWebView
     *
     * @return CordovaWebView
     */
    private CordovaWebView makeWebView() {
        //1.通過preferences配置信息構建CordovaWebViewEngine
        //2.通過CordovaWebViewEngine構建CordovaWebView
        return new CordovaWebViewImpl(makeWebViewEngine());
    }

    /**
     * 通過preferences配置信息構建CordovaWebViewEngine
     *
     * @return CordovaWebViewEngine
     */
    private CordovaWebViewEngine makeWebViewEngine() {
        return CordovaWebViewImpl.createEngine(activity, preferences);
    }

    /**
     * 構建接口實現類,接收消息
     *
     * @return CordovaInterfaceImpl
     */
    private CordovaInterfaceImpl makeCordovaInterface() {
        return new CordovaInterfaceImpl(activity) {
            @Override
            public Object onMessage(String id, Object data) {
                return CordovaView.this.onMessage(id, data);
            }
        };
    }

    /**
     * 處理消息
     *
     * @param id   消息id
     * @param data 消息數據
     * @return 處理結果
     */
    public Object onMessage(String id, Object data) {
        try {
            if ("onReceivedError".equals(id)) {
                JSONObject d = (JSONObject) data;
                try {
                    //將消息透傳給客戶端
                    if (errorListener != null) {
                        int errorCode = d.getInt("errorCode");
                        String description = d.getString("description");
                        String failingUrl = d.getString("url");
                        errorListener.onReceivedError(errorCode, description, failingUrl);
                    }
                } catch (JSONException e) {
                    e.printStackTrace();
                }
            }
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return null;
    }


    /**
     * 獲得Webview組件,供客戶端使用
     *
     * @return Webview組件
     */
    public SystemWebView getWebview() {
        if (appView != null && appView.getView() instanceof WebView) {
            SystemWebView webView = (SystemWebView) appView.getView();
            return webView;
        }
        return null;
    }

    /**
     * 獲得系統webview引擎
     * @return 系統webview引擎
     */
    public SystemWebViewEngine getSystemWebViewEngine(){
        return (SystemWebViewEngine) appView.getEngine();
    }

    /**
     * 加載url
     *
     * @param url 地址,默認應是:file:///android_asset/www/index.html
     */
    public void loadUrl(String url) {
        if (appView == null) {
            initCordova();
        }

        // If keepRunning
        // 如果preferences配置了KeepRunning,則頁面置於后台時,仍可見
        this.keepRunning = preferences.getBoolean("KeepRunning", true);

        //加載url
        //第2個參數表示重新初始化插件管理器、插件集合等
        appView.loadUrlIntoView(url, true);
    }

    /**
     * 當頁面執行onPause方法時,可調用
     */
    public void onPause() {
        if (this.appView != null) {
            CordovaPlugin activityResultCallback = null;
            try {
                Field field = CordovaInterfaceImpl.class.getDeclaredField("activityResultCallback");
                field.setAccessible(true);
                activityResultCallback = (CordovaPlugin) field.get(this.cordovaInterface);
            } catch (Exception e) {
                e.printStackTrace();
            }
            boolean keepRunning = this.keepRunning || activityResultCallback != null;
            this.appView.handlePause(keepRunning);
        }
    }

    /**
     * 當頁面執行onNewIntent方法時,可調用
     */
    public void onNewIntent(Intent intent) {
        if (this.appView != null)
            this.appView.onNewIntent(intent);
    }

    /**
     * 當頁面執行onResume方法時,可調用
     */
    public void onResume() {
        if (this.appView == null) {
            return;
        }
        activity.getWindow().getDecorView().requestFocus();
        this.appView.handleResume(this.keepRunning);
    }

    /**
     * 當頁面執行onStop方法時,可調用
     */
    public void onStop() {
        if (this.appView == null) {
            return;
        }
        this.appView.handleStop();
    }

    /**
     * 當頁面執行onStart方法時,可調用
     */
    public void onStart() {
        if (this.appView == null) {
            return;
        }
        this.appView.handleStart();
    }

    /**
     * 當頁面執行onDestroy方法時,可調用
     */
    public void onDestroy() {
        if (this.appView != null) {
            appView.handleDestroy();
        }
    }

    /**
     * 當頁面執行onWindowFocusChanged方法時,可調用
     */
    @SuppressLint("InlinedApi")
    @Override
    public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);
        //設置沉浸式與全屏
        if (hasFocus && immersiveMode) {
            final int uiOptions = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
                    | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
                    | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
                    | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
                    | View.SYSTEM_UI_FLAG_FULLSCREEN
                    | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
            activity.getWindow().getDecorView().setSystemUiVisibility(uiOptions);
        }
    }

    /**
     * 當頁面執行startActivityForResult方法時,可調用
     */
    @SuppressLint({"NewApi", "RestrictedApi"})
    public void startActivityForResult(Intent intent, int requestCode, Bundle options) {
        // Capture requestCode here so that it is captured in the setActivityResultCallback() case.
        cordovaInterface.setActivityResultRequestCode(requestCode);
        //super.startActivityForResult(intent, requestCode, options);
    }

    /**
     * 當頁面執行onActivityResult方法時,可調用
     */
    public void onActivityResult(int requestCode, int resultCode, Intent intent) {
        //super.onActivityResult(requestCode, resultCode, intent);
        cordovaInterface.onActivityResult(requestCode, resultCode, intent);
    }

    /**
     * 當頁面執行onSaveInstanceState方法時,可調用
     */
    public void onSaveInstanceState(Bundle outState) {
        cordovaInterface.onSaveInstanceState(outState);
    }

    /**
     * 當頁面執行onConfigurationChanged方法時,可調用
     */
    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        if (this.appView == null) {
            return;
        }
        PluginManager pm = this.appView.getPluginManager();
        if (pm != null) {
            pm.onConfigurationChanged(newConfig);
        }
    }

    /**
     * 接口:接收到錯誤時的回調
     */
    public interface OnReceivedErrorListener {
        /**
         * 接收到錯誤
         *
         * @param errorCode   錯誤碼(請參考Cordova官方定義)
         * @param description 錯誤描述
         * @param failingUrl  發生異常的url
         */
        void onReceivedError(int errorCode, String description, String failingUrl);
    }

    /**
     * 設置監聽
     *
     * @param listener 監聽器
     */
    public void setOnReceivedErrorListener(OnReceivedErrorListener listener) {
        this.errorListener = listener;
    }
}

如何使用

1、Fragment中(灰常簡單)

布局文件:fragment_test.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#ffff00"
    tools:context=".TestFragment">

    <com.ccc.ddd.CordovaView
        android:id="@+id/cv"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

    </com.ccc.ddd.CordovaView>

</FrameLayout>

代碼:TestFragment.java

package com.ccc.ddd;

import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.webkit.WebChromeClient;
import android.webkit.WebView;
import android.webkit.WebViewClient;

public class TestFragment extends Fragment {
    public TestFragment() {
    }

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

    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_test, container, false);
        initView(view);
        return view;
    }

    private void initView(View view) {
        String launchUrl = "file:///android_asset/www/index1.html";
        final CordovaView cordovaView = view.findViewById(R.id.cv);
        cordovaView.initCordova(getActivity());
        cordovaView.loadUrl(launchUrl);
        //如果需要處理異常,設置此回調即可;
        cordovaView.setOnReceivedErrorListener(new CordovaView.OnReceivedErrorListener() {
            @Override
            public void onReceivedError(int errorCode, String description, String failingUrl) {
                Log.i("onReceivedError", "errorCode:" + errorCode + "     description:" + description + "     failingUrl:" + failingUrl);
            }
        });
        //獲得Cordova的Webview控件,執行操作,如:reload、goback、設置緩存、獲得進度條等等
        //cordovaView.getWebview().reload();
        //cordovaView.getWebview().goBack();
        /*cordovaView.getWebview().setWebChromeClient(new SystemWebChromeClient(cordovaView.getSystemWebViewEngine()) {
            //監聽進度
            @Override
            public void onProgressChanged(WebView view, int newProgress) {
                super.onProgressChanged(view, newProgress);
                //設置頁面加載進度
                Log.i("newProgress","newProgress: "+newProgress);
            }

            @Override
            public void onReceivedTitle(WebView view, String title) {
                super.onReceivedTitle(view, title);
                //設置標題
            }
        });*/
    }
}

2、Activity中(同樣灰常簡單)

布局文件:activity_test_cordova_view.xml

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".TestCordovaViewActivity">

    <com.ccc.ddd.CordovaView
        android:id="@+id/cv"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    </com.ccc.ddd.CordovaView>

</android.support.constraint.ConstraintLayout>

代碼實現:TestCordovaViewActivity.java

package com.ccc.ddd;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;

public class TestCordovaViewActivity extends AppCompatActivity {

    private CordovaView cordovaView;
    private String launchUrl = "file:///android_asset/www/index.html";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test_cordova_view);

        cordovaView = findViewById(R.id.cv);
        cordovaView.initCordova(this);
        cordovaView.loadUrl(launchUrl);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        cordovaView.onDestroy();
    }
}

其他用法

在彈框中實現,也是類似的,具體不再演示了。

1)生命周期

如果想關聯Activity或Frament的生命周期,CordovaView中都已經預留了生命周期方法,只需在頁面生命周期方法中,關聯對應的方法即可,如:

@Override
    protected void onDestroy() {
        super.onDestroy();
        cordovaView.onDestroy();
    }
...其他生命周期函數寫法類似...
2)異常消息處理

CordovaWebview加載過程中如果遇到問題,會將錯誤信息回傳,如果開發者需要處理其錯誤信息,只需設置監聽即可:

cordovaView.setOnReceivedErrorListener(new CordovaView.OnReceivedErrorListener() {
            @Override
            public void onReceivedError(int errorCode, String description, String failingUrl) {
                Log.i("onReceivedError", "errorCode:" + errorCode + "     description:" + description + "     failingUrl:" + failingUrl);
            }
        });
3)Webview控件

通過cordovaView.getWebview()方法獲得Webview控件,可用於配置WebSetting、設置goback、設置reload、監聽加載進度等。

cordovaView.getWebview().reload();
cordovaView.getWebview().goBack();
cordovaView.getWebview().setWebChromeClient(new SystemWebChromeClient(cordovaView.getSystemWebViewEngine()) {
            //監聽進度
            @Override
            public void onProgressChanged(WebView view, int newProgress) {
                super.onProgressChanged(view, newProgress);
                //設置頁面加載進度
                Log.i("newProgress","newProgress: "+newProgress);
            }

            @Override
            public void onReceivedTitle(WebView view, String title) {
                super.onReceivedTitle(view, title);
                //設置標題
            }
        });

Fragment中運行效果

Demo源碼下載

https://pan.baidu.com/s/1_6Ms-K_Wj5EjCp01LVZHhg
https://github.com/qxcwanxss/CordovaDemo.git

如有不明白的地方,建議多看下CordovaActivity的源碼實現 ,也歡迎留言,就是不一定有時間解答,哈哈。。。


Android H5混合開發(1):構建Cordova 項目
https://www.cnblogs.com/qixingchao/p/11654454.html

Android H5混合開發(2):自定義Cordova插件
https://www.cnblogs.com/qixingchao/p/11652418.html

Android H5混合開發(3):原生Android項目里嵌入Cordova
https://www.cnblogs.com/qixingchao/p/11652424.html

Android H5混合開發(4):構建Cordova Jar包
https://www.cnblogs.com/qixingchao/p/11652431.html

Android H5混合開發(5):封裝Cordova View, 讓Fragment、彈框、Activity自由使用Cordova
https://www.cnblogs.com/qixingchao/p/11652438.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM