相关文章:
鉴于目前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长很多。
这一节先写这么多,有进展了再补充下一节。
注:demo的原来的地址已经失效了,可以换成这个地址:http://61.191.24.229:2042/IosAiMovie/FilmList/FilmList
demo下载