一.前言 值得一提的是,FA2.0 版本相较于之前的 1.1.3 版本,在本地交互和兼容性 方面实现了极为显著的提升。在旧版本中,交互时存在一个明显的问题,即需要借助网页加载过渡。这一情况导致我们无法将 H5 加载页面跳转设置在本地。虽然 H5 布局能够放在本地,但在交互跳转过程中,页面会出现闪烁现象。为了应对这个问题,开发人员只能无奈地新建多个相同的 H5 加载动画,以此来监听跳转。
好在2.0版本成功解决了这一问题,极大地降低了混合开发的成本,无需服务器部署 ,仅在本地环境 下就能实现流畅丝滑的交互体验。不仅如此,FA2.0还内置网页夜间渲染 功能,不用耗费精力去适配网页夜间模式。
一.加载本地H5 1.单个浏览页加载 1 2 3 local uiManager=this.uiManageractivity.uiManager.currentPage.webView.loadUrl("file://" ..activity.getLuaDir().."/index.html" )
2.多个浏览页加载 如果只有一个页面,可以用currentPage ,也可以用getFragment(0) 。多个浏览页都加载本地网页,就是下面这种:
1 2 3 4 5 6 7 8 activity.uiManager.getFragment(0 ).webView.loadUrl("file://" ..activity.getLuaDir().."/index2.html" ) activity.uiManager.getFragment(1 ).webView.loadUrl("file://" ..activity.getLuaDir().."/index2.html" ) activity.uiManager.getFragment(2 ).webView.loadUrl("file://" ..activity.getLuaDir().."/index3.html" ) activity.uiManager.getFragment(3 ).webView.loadUrl("file://" ..activity.getLuaDir().."/index4.html" )
这里0 就是第一个浏览页,以此类推即可,需要全部开启离屏预加载 ,关于路径问题,默认是页面文件夹下的html文件,这里写了个表格,便于理解:
路径描述
路径表示
页面文件夹下的html文件(默认情况)
直接文件名(如index.html)
上一级文件夹下的文件
../文件名(如../index.html)
上两级文件夹下的文件
../../文件名(如../../index.html)
上一级的另一个文件夹下的文件
../other/index.html(other为文件夹名,index.html为文件名)
3.local解析加载 这个方式可以实现不需要引入html文件,将html,css甚至js全部封装进lua进行解析,适用于代码简短、结构简单 的html
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 local html = [[ <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <style> .highlight-text { color: #C0341D; background-color: #FBE5E1; padding: 4px; border-radius: 4px; } </style> <title>高亮文本示例</title> </head> <body> <p><br>这是一个示例文本,<code class="highlight-text">高亮的文字在这里</code></p> </body> </html> ]] local webView = LuaWebView(this)webView.getSettings().setLoadWithOverviewMode(true ) webView.getSettings().setUseWideViewPort(true ) local FrameLayout = luajava.bindClass "android.widget.FrameLayout" local Gravity = luajava.bindClass "android.view.Gravity" local lp = FrameLayout.LayoutParams(FrameLayout.LayoutParams.FILL_PARENT, FrameLayout.LayoutParams.FILL_PARENT)activity.addContentView(webView,lp) webView.loadDataWithBaseURL(nil , html, nil , nil ,nil )
4.LuaWebView加载 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 layout= { LinearLayout; orientation='vertical' ; layout_width='fill' ; layout_height='fill' ; { LuaWebView; layout_marginTop=状态栏高度(), layout_width='fill' ; layout_height='fill' ; id='webView' ; }; } activity.setContentView(loadlayout(layout)) webView.loadUrl("file://" ..activity.getLuaDir().."/index2.html" )
二.核心监听拦截 往往来讲,我们既然用H5,基本都是当布局使用的,所以,点击H5布局能实现与原生的交互,这是最核心的问题,我的思路就是通过网页监听事件,拦截H5的超链接,实现跳转子页面以及与其他原生功能交互。
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 56 57 58 59 60 import "net.fusionapp.core.ui.fragment.WebInterface" import "androidx.viewpager.widget.ViewPager$OnPageChangeListener" local uiManager=this.uiManagerlocal viewPager=uiManager.viewPagerlocal pagerAdapter=uiManager.pagerAdapterlocal pagerCount=pagerAdapter.getCount()function webInterface () for i = 0 ,pagerCount-1 ,1 do local fragment=uiManager.getFragment(i) if fragment then fragment.setWebInterface(WebInterface{onPageFinished=function (view,url) 网页后退() end , onPageStarted=function (view,url,favicon) if url:find ("这里填写你的h5布局元素的超链接" ) then 网页后退() 进入子页面("start" ) return false end if url:find ("这里填写超链接" ) then 网页后退() 进入子页面("login" ) return false end end , onReceivedTitle=function (view,title) end , onLoadResource=function (view,url) end , onUrlLoad=function (view,url) return false end , onReceivedSslError=function (view, sslErrorHandler, sslError) return false end }) end end end webInterface() viewPager.setOnPageChangeListener(OnPageChangeListener{ onPageSelected=function (n) webInterface() end })
注意,超链接拦截方法是目前来讲最简单且最实用的方法,超链接要满足以下需求:
链接可以打开
打开速度得慢一点,前期加载空白
链接地址前缀统一,方便管理
那满足以上需求的最完美的地址是什么呢?我苦思冥想,百般尝试,终于找到了一个完美方案:直接使用 https://github用户名.github.io/后边你随便写.html
这样写的好处是GitHub本就是国外网站,加载自然慢一些,而且我们的超链接要求一定是能打开的 ,如果打不开,拦截就会闪烁网页加载失败的页面。虽然GitHub的后边名称不存在,但是Github默认不存在的都会定向到一个提示页面,由此我们可以随便填写以满足不同点击事件的拦截 。效果搭配以上代码就是完美衔接跳转,根本看不到中间加载过渡。
这里是监听所有浏览页的,不论你用了几个本地H5浏览页,都可以在上边这些代码中监听,**网页后退()**不要随意删除。通过上述方法我们已经实现了网页点击与原生层的交互功能。
三.模拟用户点击 上面讲了H5去定向到原生,也就是说我们已经实现了以H5当布局时,点击布局元素实现原生的功能。
那么原生如何定向到H5呢?在一些引入外部网站时可能会用到,假设我们引入了一个我们的博客网站,但是我想让顶栏用原生框架,那么我们可以删除掉网站的原有头部顶栏元素(或者利用JS屏蔽元素)这里屏蔽要屏蔽最大的父元素。然后我们找到网站的侧滑栏按钮元素,用JS的模拟点击 该元素。
可通过 document.querySelector 获取网页元素,再调用 click 方法来模拟点击。在原生Lua布局框架中调用网页,可将JavaScript代码注入到网页中执行。假设侧滑栏元素的CSS选择器为 .sidebar-toggle
:
1 2 3 4 5 6 7 8 var element = document .querySelector ('.sidebar-toggle' );if (element) { element.click (); } else { console .log ('未找到对应的元素' ); }
再举个简单的例子,比如APP我们设置了一个悬浮球,让他实现返回网页顶部 。Fa中文函数模块配置了返回顶部功能,直接使用即可。
1 2 3 4 5 6 7 8 9 10 11 12 function onFloatingActionButtonClick (v) 返回网页顶部() end
不过这个中文函数自带的返回顶部是直接跳到顶部,并不是平滑动画返回顶部 ,要实现平滑返回,可以这样写JS:
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 window .onload = function ( ) { const duration = 500 ; const start = window .pageYOffset ; const startTime = performance.now (); function easeInOutQuad (t ) { return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t; } function animateScroll ( ) { const currentTime = performance.now (); const timeElapsed = currentTime - startTime; const scrollAmount = start * easeInOutQuad (timeElapsed / duration); window .scrollBy (0 , -scrollAmount); if (timeElapsed < duration) { requestAnimationFrame (animateScroll); } else { window .scrollTo (0 , 0 ); } } requestAnimationFrame (animateScroll); };
总之,就是利用JS模拟用户点击 ,点击html中指定元素,并将这个加载JS代码写进你的原生点击事件 即可。
四.伪原生的实现 1.隐藏网页滑动条 我们还可以把本地网页的上下滑动条给隐藏起来,把下面CSS代码放入本地html文件的head标签内
1 2 3 4 5 6 7 8 9 <style > html ,body { overflow-y : hidden; } html { scroll-behavior : smooth; } </style >
如果上述CSS不生效,那就需要webView 进行控制了:
1 2 3 4 webView.getSettings().setSupportZoom(false ); webView.getSettings().setBuiltInZoomControls(false ); webView.getSettings().setDisplayZoomControls(false ); webView.setVerticalScrollBarEnabled(false )
注意,上述代码如果是在自带的浏览页 进行,直接添加会报错,需要先获取:
1 2 3 4 local WebChromeClient = luajava.bindClass "android.webkit.WebChromeClient" local WebViewClient = luajava.bindClass "android.webkit.WebViewClient" local uiManager=activity.getUiManager()local webView=uiManager.getCurrentFragment().getWebView()
2.禁用浏览页长按菜单 我们可以禁止本地浏览页的长按 功能,无法复制和出现菜单,从而实现伪原生,你可以用lua,也可以用js实现。先来说lua代码:
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 local fragment=uiManager.currentFragmentfragment.setMenuInterface(luajava.bindClass"net.fusionapp.core.ui.fragment.WebViewMenuSupport" .Interface{ onMenuItemCreate=function (list) list.add("白衣" ) end , }) local fragment=uiManager.getFragment(1 )fragment.setMenuInterface(luajava.bindClass"net.fusionapp.core.ui.fragment.WebViewMenuSupport" .Interface{ onMenuItemCreate=function (list) list.add("白衣" ) end , })
另一种方法是使用CSS禁止长按选中文本,使用JavaScript禁止长按弹出菜单:
1 2 3 4 5 6 7 8 * { -webkit-touch-callout: none; -webkit-user-select : none; -khtml-user-select : none; -moz-user-select : none; -ms-user-select : none; user-select : none; }
1 2 3 4 5 6 7 document .addEventListener ('contextmenu' , function (e ) { e.preventDefault (); }, false ); document .addEventListener ('touchstart' , function (e ) { e.preventDefault (); }, { passive : false });
3.隐藏加载进度条 隐藏加载进度条,你可以将进度条颜色设置为透明色,也可以直接用下面代码移除,这种仅适用于FA2
1 2 3 activity.uiManager.getFragment(0 ).webView.parent.removeViewAt(2 )
自定义浏览器布局的需要使用以下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 local webSettings = mLuaWebView.getSettings();webSettings.setUseWideViewPort(true ); webSettings.setLoadWithOverviewMode(true ); webSettings.setJavaScriptEnabled(true ); local WebChromeClient = luajava.bindClass "android.webkit.WebChromeClient" mLuaWebView.setWebChromeClient(luajava.override(WebChromeClient, { onProgressChanged = function (view, newProgress) end }))
4.移除默认点击框 有些网页会有默认的点击反馈色块,我们需要删除它:
1 2 3 4 5 6 7 8 9 10 11 12 *:focus { outline : none; } button :focus { background-color : #e0e0e0 ; box-shadow : 0 0 8px rgba (0 , 0 , 0 , 0.3 ); } body { -webkit-tap-highlight-color : transparent; }
5.强制删除状态栏 这个是强制删除状态栏,有些沉浸和隐藏仍然还是会有小黑条的,如果需要删除,我们使用以下代码:
1 2 3 4 5 6 7 8 local window=activity.getWindow() window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS).addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION) window.getAttributes().layoutInDisplayCutoutMode=1 window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
6.遮罩动画函数 我们可以插入一个遮罩动画,再配合离屏预加载,就可以完美无延迟载入
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 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 require "import" import "android.os.*" import "android.widget.*" import "android.view.*" import "android.graphics.Color" local uiManager=this.uiManagerlocal appBar=uiManager.appBarLayoutlocal appfab=uiManager.FloatingActionButtonlocal appdrv=uiManager.getDrawerRecyclerView()local viewPager=uiManager.viewPagerlocal toolbar=uiManager.toolbarimport "android.graphics.drawable.GradientDrawable" function 渐变(left_jb,right_jb,id) drawable = GradientDrawable(GradientDrawable.Orientation.TR_BL,{ right_jb, left_jb, }); id.setBackgroundDrawable(drawable) end 渐变(0xFFFF6845 ,0xFFFF2317 ,appBar) function 遮罩函数() require "import" import "net.fusionapp.core.ui.fragment.WebInterface" local uimanager=activity.uiManager local fragment=uimanager.currentFragment layout={ FrameLayout, layout_width="fill" , layout_height="fill" , { LinearLayout, orientation="vertical" , layout_width="fill" , layout_height="fill" , background="#ff1d8ae7" ; id="qdy" ; }, { LinearLayout, orientation="vertical" , layout_width="fill" , layout_height="fill" , background="#ff1d8ae7" , gravity="center" , id="qdt" , { LinearLayout; orientation='vertical' ; layout_width='fill' ; layout_height='fill' ; gravity='center' ; id="进度条布局" ; { ProgressBar; id="进度条" ; }; { TextView; layout_width='fill' ; layout_height='50dp' ; id="qdwb" ; gravity='center' ; textColor='#ffffff' ; text="JsHD调试器" ; textSize='18sp' ; }; { TextView; layout_width='fill' ; id="qdwb_1" ; gravity='center' ; textColor='#ffffff' ; text="JavaScript HTML DOM手机可视化调试器" ; textSize='9sp' ; }; }; }, { FrameLayout, layout_width="fill" , layout_height="fill" , { TextView; layout_width='fill' ; layout_height='fill' ; layout_marginBottom="15dp" ; gravity="center|bottom" ; textColor='#ffffff' ; id="版权" ; text='Copyright©Yyge.AllRights Reserved' ; textSize='10sp' ; }; }; } viewPager.getParent().getParent().addView(loadlayout(layout)) function CircleButton (view,InsideColor,radiu) drawable = GradientDrawable() drawable.setShape(GradientDrawable.RECTANGLE) drawable.setColor(InsideColor) drawable.setCornerRadii({radiu,radiu,radiu,radiu,radiu,radiu,radiu,radiu}); view.setBackgroundDrawable(drawable) end activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS).setStatusBarColor(0xff1d8ae7 ); activity.getWindow().setNavigationBarColor(Color.parseColor("#ff1d8ae7" )); import "android.graphics.PorterDuffColorFilter" import "android.graphics.PorterDuff" 进度条.IndeterminateDrawable.setColorFilter(PorterDuffColorFilter(0xffffffff ,PorterDuff.Mode.SRC_ATOP)) function 退出动画() import "android.view.animation.*" import "android.view.animation.AccelerateInterpolator" import "android.view.animation.RotateAnimation" import "android.view.animation.Animation" import "android.view.animation.Animation$AnimationListener" task(1000 ,function () 揭露动画一 = ViewAnimationUtils.createCircularReveal(qdt,activity.getWidth(),0 ,Math.hypot(activity.getWidth(),activity.getHeight())/2 ,0 ); 揭露动画一.setDuration(1000 ) 揭露动画一.start() 揭露动画二 = ViewAnimationUtils.createCircularReveal(qdy,0 ,activity.getHeight(),Math.hypot(activity.getWidth(),activity.getHeight())/2 ,0 ); 揭露动画二.setDuration(1000 ) 揭露动画二.start() 版权.setVisibility(0 ) animationSet = AnimationSet(true ) leave_dh1= AlphaAnimation(1 ,0 ); leave_dh1.setDuration(1000 ); leave_dh1.setFillAfter(true ); animationSet.addAnimation(leave_dh1); 版权.clearAnimation(); 版权.setAnimation(animationSet); animationSet.setAnimationListener(AnimationListener{ onAnimationStart=function () end , onAnimationEnd=function () qdt.setVisibility(8 ) qdy.setVisibility(8 ) 版权.setVisibility(8 ) end }) end ) end 渐变(0xFFFF6845 ,0xFFFF2317 ,qdy) 渐变(0xFFFF6845 ,0xFFFF2317 ,qdt) 进度条布局.setVisibility(View.VISIBLE) function 强制输入框(标题,消息,积极按钮名称) 对话框() .设置标题(标题) .设置消息(消息) .设置积极按钮(积极按钮名称,function () loadstring ([[return (canoffline or 退出程序())]] )() end ) .setCancelable(false ) .显示() end 退出动画() end 遮罩函数()
五.本地更改网页字体 更改网页字体的目的是如果你的APP设置了自定义字体,那么你的网页尽可能的也需要使用相应的字体。字体不需要外链,直接读取APP内本地的字体文件 即可,如果你的H5页面比较多,如果按照传统路径会增加大量的空间和重复字体文件,通过相对路径 ,实现所有本地网页加载同一个目录下的文件。
路径写法
含义说明
test.html
此页面在当前页面所在目录下
./test.html
此页面在当前页面所在目录下,与直接写文件名含义相同
/test.html
此页面在网站根目录下
../test.html
此页面在当前页面的上一级目录下
../../test.html
此页面在当前页面的上一级的上一级目录下(即上两级目录下),每增加一级上级目录,就增加一个../
../web/test.html
此页面在当前页面上一级目录的web子目录下
下面css代码放进html即可,这个实例的路径说明一下,字体font.ttf放在了工程根目录 ,这个css代码对应的html路径在子页面目录 ,即需要往上两级 去加载文件,也就是../../
1 2 3 4 5 6 7 8 9 10 @font-face { font-family : 'ziti' ; src : url ('../../font.ttf' ) format ('truetype' ); font-weight : normal; font-style : normal; } body { font-family : 'ziti' , sans-serif; }
六.浏览页自定义布局 1.第一种 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 function setContentView (h) local args={h} local list=table .clone(args) table .foreach (list,function (k,v) list[k]=luajava.instanceof(v,View)and v or loadlayout(v) local webView=this.uiManager.getFragment(k-1 ).webView webView.setVisibility(8 ) webView.parent.addView(list[k]) end ) end layout={ LinearLayout, layout_width=-1 , layout_height=-1 , gravity="center" , { Button, }, } setContentView(layout)
2.第二种 1 2 3 local layout=activity.uiManager.getFragment(0 ).view.removeAllViews().addView(loadlayout(layout))
3.第三种 1 2 3 4 local layout=this.uiManager.getFragment(0 ).view.addView(loadlayout(layout))
补充这个自定义布局是为了应对有多个浏览页时,例如第一个浏览页用原生布局,第二个浏览页用HTML布局等等