• Cordova系列之化繁为简:打造全场景适用的Cordova组件


    前言

    在我之前的文章 Cordova初探 的开篇中说到了Cordova在Android应用开发中的一个显著的局限性就是我们的Activity必须继承其提供的CordovaActivity。这种设计对于那些追求个性化UI设计的项目而言,显得尤为受限。
    其实也可以理解,Cordova主要旨在为前端开发者提供一个方便的框架,让他们可以专注于编写HTML、CSS和JS代码,以满足业务需求,并确保这些代码能够在iOS和Android平台上运行。
    这对于纯H5的开发方式来说是非常合适的,其中Cordova充当的是H5内容的容器。但是,在我遇到的项目中,纯H5开发方式实际上并不常见。
    H5在理论上能够实现优雅的跨平台功能,但在实践中,我们需要考虑到多个关键因素,如兼容性、性能以及与原生应用交互的成熟度。
    所以,本片文章主要是为Android开发的同学们提供一个基于Cordova封装的全场景适用的组件,使得我们在Cordova原有功能的基础上,能够突破必须继承CordovaActivity限制,能够随心所欲的自定义我们的ui,并提供更加丰富的功能以更好的支撑我们的业务需求。

    目标 & 思路

    目标
    • 保持Cordova的原有功能和使用方式。
    • 摆脱继承Activity的限制,使其像Webview控件一样灵活使用。
    • 提供基于Activity和Fragment的封装,简化开发,同时支持UI个性化。
    • 提供关键节点的回调,以满足更广泛的业务需求。
    思路

    以CordovaActivity这个入口源码为切入点,我们会发现,CordovaActivity代码量并不大,其核心功能集中在几个关键节点上,例如处理SplashScreen、加载配置、初始化变量、创建CordovaInterface等。所以,一个思路就是将CordovaActivity中关于CordovaWebView和CordovaWebViewEngine的核心代码提取出来,封装成一个更小粒度的自定义view供外部使用,同时提供Activity和Fragment的模版类,暴露出自定义ui的接口。这样一来,就可以实现核心目标了。

    具体的代码实现已经上传到github上了,欢迎感兴趣的读者前往查看。下面主要详细介绍组件的使用方法。

    组件使用

    添加依赖
    implementation("com.xeonyu:cordova-webcontainer:1.0.5")
    
    • 1
    继承CordovaWebContainerActivity使用

    该方式适用于绝大部分业务场景,集成简单且保持UI灵活。

    布局示例

    
    <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="com.yzq.demo.activity.WebContainerActivity">
    
        
        <androidx.appcompat.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="@color/purple_200"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:titleTextColor="@color/white" />
    
        
        <ProgressBar
            android:id="@+id/progressbar"
            style="@android:style/Widget.ProgressBar.Horizontal"
            android:layout_width="match_parent"
            android:layout_height="5dp"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/toolbar" />
    
        
        <com.yzq.cordova_webcontainer.CordovaWebContainer
            android:id="@+id/web_container"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintTop_toBottomOf="@id/progressbar"
            app:layout_goneMarginTop="5dp" />
    
        
        <com.google.android.material.floatingactionbutton.FloatingActionButton
            android:id="@+id/reload_fab"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="16dp"
            android:contentDescription="刷新"
            android:src="@drawable/refresh"
            android:tint="@color/white"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent" />
    androidx.constraintlayout.widget.ConstraintLayout>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48

    代码示例:

    /**
     * @description 继承自WebcontainerActivity的使用示例
     * @author  yuzhiqiang (zhiqiang.yu.xeon@gmail.com)
     */
    
    class WebContainerActivity : CordovaWebContainerActivity() {
        private lateinit var binding: ActivityWebContainerBinding
        private val TAG = "WebContainerActivity"
    
        /*布局初始化*/
        override fun initContentView() {
            binding = ActivityWebContainerBinding.inflate(layoutInflater)
            setContentView(binding.root)
        }
    
    
        /*初始化Webcontainer控件*/
        override fun initWebContainer(): CordovaWebContainer {
            with(binding) {
                webContainer.run {
                    /**
                     * 初始化webcontainer
                     */
                    init(
                        this@WebContainerActivity,
                        this@WebContainerActivity
                    )
    
                 
            return binding.webContainer
        }
    
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            val url = "https://baidu.com/"
            binding.webContainer.loadUrl(url)
            binding.webContainer.setOnPageScrollChangedListener { xOffset, yOffset, oldxOffset, oldyOffset ->
                Log.i(TAG, "yOffset:$yOffset,oldyOffset:$oldyOffset")
            }
            binding.reloadFab.setOnClickListener {
                binding.webContainer.reload()
    
            }
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    在Fragment中使用

    支持在Fragment中使用,继承CordovaWebContainerFragment即可,api跟CordovaWebContainerActivity
    一致。需要注意的是Fragment的宿主Activity需要处理下页面结果回调的方法。

    宿主的Activity中重写下面两个方法,写法固定

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
            super.onActivityResult(requestCode, resultCode, data)
          
            currentFragment?.onActivityResult(requestCode, resultCode, data)
        }
    
        override fun onRequestPermissionsResult(
            requestCode: Int,
            permissions: Array<out String>,
            grantResults: IntArray,
        ) {
            super.onRequestPermissionsResult(requestCode, permissions, grantResults)
          
            currentFragment?.onRequestPermissionsResult(requestCode, permissions, grantResults)
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    Fragment正常使用即可,示例代码

    package com.yzq.demo.fragment
    
    /**
     * @description 在Fragment中使用示例
     * @author  yuzhiqiang (zhiqiang.yu.xeon@gmail.com)
     */
    
    class WebContainerFragment(val webUrl: String) : CordovaWebContainerFragment() {
        private lateinit var rootView: View
        private lateinit var webContainer: CordovaWebContainer
    
        override fun initContentView(
            inflater: LayoutInflater,
            container: ViewGroup?,
            savedInstanceState: Bundle?,
        ): View {
            rootView = layoutInflater.inflate(R.layout.fragment_web_container, container, false)
            return rootView
        }
    
        override fun initWebContainer(): CordovaWebContainer {
            webContainer = rootView.findViewById(R.id.web_container)
            webContainer.init(requireActivity() as AppCompatActivity, this)
            return webContainer
        }
    
        override fun initWidget() {
            if (webUrl.isNotEmpty()) {
                webContainer.loadUrl(url = webUrl)
            } else {
                webContainer.loadUrl()
            }
        }
    
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    作为自定义view使用

    如果你不希望继承指定的Activity,你可以把CordovaWebContainer作为自定义view使用。

    package com.yzq.demo.activity
    
    
    /**
     * @description 直接使用Webcontainer控件的示例,适用于更加灵活的场景,例如你不想继承指定的Activity
     * @author  yuzhiqiang (zhiqiang.yu.xeon@gmail.com)
     */
    
    class MainActivity : AppCompatActivity() {
        private lateinit var binding: ActivityMainBinding
        private val TAG = "MainActivity"
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            binding = ActivityMainBinding.inflate(layoutInflater)
            setContentView(binding.root)
            binding.run {
                toolbar.title = "基于Cordova的webview使用"
             
                /*初始化*/
                webContainer.init(this@MainActivity, this@MainActivity)
                /*加载url*/
    //            val url = "https://www.baidu.com/"
                webContainer.loadUrl()
    
            }
    
        }
    
    
        //固定写法
        override fun onSaveInstanceState(outState: Bundle, outPersistentState: PersistableBundle) {
            super.onSaveInstanceState(outState, outPersistentState)
            binding.webContainer.onSaveInstanceState(outState)
        }
        //固定写法
        override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
            super.onActivityResult(requestCode, resultCode, data)
            binding.webContainer.onActivityResult(requestCode, resultCode, data)
    
        }
        //固定写法
        override fun startActivityForResult(intent: Intent, requestCode: Int, options: Bundle?) {
            binding.webContainer.startActivityForResult(requestCode)
            super.startActivityForResult(intent, requestCode, options)
        }
    //固定写法
        override fun onRequestPermissionsResult(
            requestCode: Int,
            permissions: Array<out String>,
            grantResults: IntArray,
        ) {
            super.onRequestPermissionsResult(requestCode, permissions, grantResults)
            binding.webContainer.onRequestPermissionsResult(requestCode, permissions, grantResults)
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55

    API

    列一下主要的api,具体的使用可以参考github上的示例代码。

    初始化WebContainer

    传入Activity以及LifecycleOwner

    /*初始化(必须)*/
    webContainer.init(this, this)
    
    • 1
    • 2
    加载url
    /*必须*/
    webContainer.loadUrl(url)
    
    • 1
    • 2
    关键事件回调
    webContainer.addPagePbserver(PageObserver)
    
    • 1

    支持以下事件监听
    在这里插入图片描述

    请求拦截处理
    /*可选拦截请求 等同于shouldInterceptRequest 记得用这个*/
                    webContainer.webviewClient.interceptRequest { view, request, response ->
                        val url = request.url.toString()
                        Log.i(TAG, "interceptRequest:$url")
                        return@interceptRequest response
                    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    loadurl 处理
    /*可选 处理准备load的url 等同于 shouldOverrideUrlLoading*/
                    webContainer.webviewClient.overrideUrlLoading { view, request ->
                        Log.i(TAG, "overrideUrlLoading:${request.url}")
                        request.url.toString().let {
                            if (it.startsWith("baidu://")) {
                                return@overrideUrlLoading true
                            }
                        }
                        return@overrideUrlLoading false
                    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    滚动监听
    webContainer.setOnPageScrollChangedListener { xOffset, yOffset, oldxOffset, oldyOffset ->
                Log.i(TAG, "yOffset:$yOffset,oldyOffset:$oldyOffset")
            }
    
    • 1
    • 2
    • 3

    其他使用跟webview api一样

    混淆

    组件内部已包含必要的混淆规则。

    #Cordova
    -keep class org.apache.cordova.**{*;}
    -keep interface org.apache.cordova.**{*;}
    -keep enum org.apache.cordova.**{*;}
    
    • 1
    • 2
    • 3
    • 4

    这就是基于Cordova封装的全场景组件。如果您觉得本组件对您有所帮助,欢迎在GitHub上点个star,希望能帮到你。

    github地址: cordova-webcontainer


    感谢阅读,觉有有帮助点赞支持,如果有任何疑问或建议,欢迎在评论区留言。如需转载,请注明出处:喻志强的博客 ,谢谢!

  • 相关阅读:
    Qt之进程通信-QProcess(含源码+注释)
    拆解爱美客2022年上半年财报:盈利能力强、业绩增速却下滑,股东人数锐减三成
    基于log4cpp封装日志类
    C语言实现面向对象编程 | 干货
    企业数据流转5个大坑,你最烦哪个?
    C# 定时器定时不准确问题
    C++ 继承原理。
    MySQL 主从复制
    【计算机考研必备常识】24考研你开始准备了吗?
    VLANIF配置
  • 原文地址:https://blog.csdn.net/yuzhiqiang_1993/article/details/134341102