鉴于目前iOS手机性能的提升,使用webview方法开发的app效果已经很理想,团队在项目中使用了hybrid的开发模式,积累了一定的经验,便分享出来给大家参考,并互相学习。
好了,废话不多说,下面从目标、技术原理、代码展示三个部分讲解。
一、目标
使用native与HTML结合的方式,变动小的页面使用native开发,如个人信息;而电影、影院信息等页面使用HTML(这个应用是买电影票的~)。为了使HTML页面的效果好一点,甚至接近原生的效果,需要对webview进行一系列的优化,于是苦逼的事情来啦···
二、优化手段
其实目前许多流行的应用,特别是数据庞大的应用(如淘宝、京东等)会采用的hybrid方式,看他们移动端的页面就能看出一些端倪。在页面加载时,几乎和app加载的效果差不多,先加载出整体的框架,然后填充页面中的数据,加载完后不就是个app嘛。
说了这么多,只想表明优化的关键在前端页面。如果在webview页面中加载的是淘宝的链接,体验会非常棒;然而加载自家的app,几秒钟过去了,却还是空白~,有种泪奔的感觉。
既然上面提出了优化的需求,我这个iOS程序猿总不能把HTML页面框架重写一遍,只好在webview上做文章:
1、支持右滑后退功能
2、上下拖动时,navigationBar对应隐藏和显示
3、缓存
下面分别介绍这些方法。
三、右滑后退功能
这是从CocoaChina上看到的现成例子,直接拿来用了,详见 仿微信内嵌网页
该demo中有个缺陷,右滑后退后页面会重载,不能像原生那样就稳定到滑动过程中看到页面,而自己目前没想到解决方案。
右滑后退功能用到了截屏、手势识别、自定义视图的变换。在页面加载后会截屏,并保存到图片队列中。进入子页面后,如果有右滑操作,则回调相应方法,让webview右偏并逐渐隐藏,而之前页面的截图随机出现。具体的原理可看demo。
四、上下拖动的效果
webview页面展示时,顶部的导航栏占据了一定区域,也不能提供什么有价值的信息,将它隐藏起来会使页面看着更大(biger than biger)。
如果在webview上加上上下拖动识别,发现拖动时并不能接收到回调信息。自己当时想了好久也不知咋办,于是了解了webview的原理,
NS_CLASS_AVAILABLE_IOS(2_0) __TVOS_PROHIBITED @interface UIWebView : UIView <NSCoding, UIScrollViewDelegate>原来webview中使用了scrollView的协议,上下拖动会被scrollview截获,用于显示页面信息,而不能触发手势识别。此时,在自定义类中使用UIScrollViewDelegate,就能接收到拖动时的信息,程序根据回调信息显示和隐藏navigationBar。
使用UIScrollViewDelegate需要重写scrollViewWillBeginDragging、scrollViewDidScroll、scrollViewDidEndDragging三个方法。开始拖动时,记录下scrollView的偏移(scrollView.contentOffset);在拖动过程中,会返回当前的偏移值,根据当前值和初始值,确定偏移量。navigationBar根据偏移量上下偏移,向上隐藏时,设置alpha值逐渐降低,直至透明。navigationBar向下偏移显示时,alpha升高,最后完全不透明;拖动结束后,根据偏移量确定navigationBar的最终状态,完全隐藏或者完全显示。这就是原理,以下是实现代码。
#pragma mark scrollView Delegate - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView{ scrollOffset = scrollView.contentOffset; } - (void)scrollViewDidScroll:(UIScrollView *)scrollView{ if (self.tabBarController.tabBar.hidden) { //往上拖,offset.y为正; CGPoint offset = scrollView.contentOffset; if (scrollView.contentSize.height <= webView.frame.size.height + defHeight) { return; } //在拖动过程中,超过NAVIGATION_BAR_HEIGHT的距离后,默认为44 if (fabs(offset.y - scrollOffset.y) > defHeight) { if (offset.y > scrollOffset.y) { offset.y = defHeight + scrollOffset.y; }else if (offset.y < scrollOffset.y){ offset.y = scrollOffset.y - defHeight; } } //拖动完成后 //navBar隐藏后,往上拖则不响应 if ((navBarState == NavBarStateHide) && (offset.y > scrollOffset.y) ) { return; //navBar显示后,往下拖则不响应 }else if (navBarState == NavBarStateShow && offset.y < scrollOffset.y){ return; } //返回后,页面已拖到底部,往下拖则不响应 if (scrollOffset.y >= scrollView.contentSize.height - webView.frame.size.height - defHeight/2 && navBarState == NavBarStateShow) { return; } [self setNavigationBarFrame:(offset.y - scrollOffset.y)]; } } - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{ if (!self.tabBarController.tabBar.hidden) { return; } CGPoint offset = scrollView.contentOffset; //navBar隐藏后,网页在上面隐藏的高度小于nav的高度时,往下拖动的距离大于5,则显示navBar if (navBarState == NavBarStateHide && offset.y < scrollOffset.y - 5) { [self navBarShowState]; navBarState = NavBarStateShow; return; } //如果页面的大小 < webView的大小,则不响应 if (scrollView.contentSize.height <= webView.frame.size.height + 44) { return; } //navBar隐藏后,往上拖则不响应 if ((navBarState == NavBarStateHide) && (offset.y > scrollOffset.y) ) { return; //navBar显示后,往下拖则不响应 }else if (navBarState == NavBarStateShow && offset.y < scrollOffset.y){ return; } CGFloat distance = scrollOffset.y - offset.y; //移动距离超过22,则状态改变,否则不改变 if (fabs(distance) > defHeight/2) { switch (navBarState) { case NavBarStateShow: [self navBarHideState]; navBarState = NavBarStateHide; break; case NavBarStateHide: [self navBarShowState]; navBarState = NavBarStateShow; break; default: break; } }else{ switch (navBarState) { case NavBarStateShow: [self navBarShowState]; navBarState = NavBarStateShow; break; case NavBarStateHide: [self navBarHideState]; navBarState = NavBarStateHide; break; default: break; } } } /** * 拖动过程中,更改页面 * * @param y 拖动的距离,向上拖为正,向下拖为负 */ - (void)setNavigationBarFrame:(CGFloat) y{ CGRect webViewRect; CGRect navBarRect; switch (navBarState) { case NavBarStateShow: self.navigationController.navigationBar.alpha = 1 - y/44; webViewRect = initWebViewRect; navBarRect = initNavBarRect; break; case NavBarStateHide: [self.navigationController setNavigationBarHidden:NO animated:NO]; self.navigationController.navigationBar.alpha = fabs(y/44); webViewRect = hidedWebViewRect; navBarRect = hidedNavBarRect; break; default: break; } webViewRect.size.height += y; webView.frame = CGRectOffset(webViewRect, 0, -y); self.navigationController.navigationBar.frame = CGRectOffset(navBarRect, 0, -y); } - (void)navBarShowState{ webView.frame = initWebViewRect; [self.navigationController setNavigationBarHidden:NO animated:NO]; self.navigationController.navigationBar.frame = initNavBarRect; self.navigationController.navigationBar.alpha = 1.0; } - (void)navBarHideState{ webView.frame = hidedWebViewRect; [self.navigationController setNavigationBarHidden:YES animated:NO]; self.navigationController.navigationBar.frame = hidedNavBarRect; }
五、缓存
目前缓存部分的代码已经完成,但还在完善中,等写好了再贴上代码。
先简单说下原理。
最初的想法是把图片存到本地,但是想使用本地的缓存图片,则必须把HTML文件中的图片地址设为本地的。经过思考后,决定将HTML、css、js等所有文件加载到本地保存,对HTML文件进行解析,找出需要下载的文件,异步缓存到本地。使用CoreData保存每个页面的信息,如页面的URL地址,保存在文件中的路径,图片、css等文件是否下载完成。这部分代码折腾了一个多星期,挺麻烦的。在初次加载时,每下载完一个页面,webview便刷新,给人感觉不连贯。而且当一个页面中需要下载的文件过多时,花费的时间比直接用webview长很多。
这一节先写这么多,有进展了再补充下一姐。