前言:最近我也写了个资讯app并且开源了,这里我就以我写的这个app为例,来简单实现网易新闻详情页。相信大家都玩过不少资讯新闻app,今日头条、网易新闻、新浪新闻。。等等各种新闻app都是非常优秀和值得借鉴学习的。测试截图我已经开启了慢网速模式,所以加载速度比较慢,这样才能明显的看到我们的需求。
功能分析
图片缓存:
详情正文是在请求到数据后才开始刷新页面,并且第一次刷新后,会先加载文字内容部分,而图片会先以同等尺寸的占位图代替,等图片下载完成后,会自动替换掉占位图。如果下一次再次打开这篇文章,文字和图片会同时加载出来,说明上次加载图片已经将图片给缓存了。
图片点击:
点击页面内的图片,会加载一个图片浏览器用于单独展示图片。实现这个功能,被点击的图片必定会有事件监听,我们可以为图片添加js点击事件,来监听图片点击。在图片点击后,向swift发送事件,并传递被点击的图片或者图片的索引。
修改字体:
动态修改字体文件和文字尺寸功能在网页中非常容易实现,因为网页中我们所看到的样式其实基本都是由css(层叠样式表)来控制的。我们这里也不例外,也是通过修改css来实现这个效果。在我们点击了修改字体或者文字尺寸按钮后,通过调用js方法,传递字体或字号,修改字体并重新计算页面的高度。从而实现动态修改字体的功能。
接口分析
从功能分析可以大致猜出接口返回数据的结构,比如图片缓存部分。缓存图片会先用同等尺寸的占位图占位,可以得出我们一定需要在图片为加载前获取到图片的尺寸和位置。根据参考网易新闻的api接口,仿写出如下api接口,这里我就贴数据吧。
{ "data" : { "newstime" : "1465999158", "title" : "长着人类牙齿的鱼", "titlepic" : "http:\/\/photo.6ag.cn\/2016-06-15\/small12564f1952c7eb18827cc1d8944f631e1465999276.jpg", "morepic" : [ ], "classname" : "动物植物", "top" : "0", "smalltext" : "事情发生在加利福尼亚州,上周末,一位名叫 Gallo的男子在当地池塘里钓鱼,当他感到鱼钩被咬住的时候,用力一提,一条鱼上钩了 。但在他准备提起鱼竿取下这条鱼的时候,鱼咬断了线,坠落到了地上,这时候他开始注意到情况不对劲了", "havefava" : "0", "flashurl" : "", "down" : "0", "titleurl" : "http:\/\/www.6ag.cn\/\/dongwuzhiwu\/2654.html", "keyid" : "", "id" : "2654", "onclick" : "8", "allphoto" : [ { "pixel" : { "width" : 600, "height" : 409 }, "caption" : "长着人类牙齿的鱼", "url" : "http:\/\/photo.6ag.cn\/2016-06-15\/12564f1952c7eb18827cc1d8944f631e.jpg", "ref" : "<!--IMG#0-->" }, { "pixel" : { "width" : 599, "height" : 353 }, "caption" : "长着人类牙齿的鱼", "url" : "http:\/\/photo.6ag.cn\/2016-06-15\/1e86e55e38ff93df99a636a3f1c4b989.jpg", "ref" : "<!--IMG#1-->" } ], "newstext" : "<p>事情发生在加利福尼亚州,上周末,一位名叫 Gallo的男子在当地池塘里钓鱼,当他感到鱼钩被咬住的时候,用力一提,一条鱼上钩了 。但在他准备提起鱼竿取下这条鱼的时候,鱼咬断了线,坠落到了地上,这时候他开始注意到情况不对劲了。<\/p>\r\n<p>该男子接受新闻采访时称,“当鱼落在地上时,我清楚地知道这跟之前见过的任何鱼类都不同。”<\/p>\r\n<p>Gallo后来得知他钓上的是一只锯腹脂鲤(也称“切蛋鱼”,传言会咬食男性睾丸),杂食性鱼类,原产于南美洲的亚马逊流域,靠着一张诡异长着人类牙齿的嘴著名。该物种属于食人鱼的近亲,由于水族馆宠物店养殖的疏忽,现在已经在美国的很多河流和池塘中发现了其踪迹。在美国,食人鱼深受青少年人群的喜爱,因此为满足这一需求,宠物商店大肆进货,由于食物充足,缺少天敌,很多食人鱼在室内的鱼缸内过度生长,在成年甚至可以达到10-12英寸长。<\/p>\r\n<p><!--IMG#0--><\/p>\r\n<p>“切蛋鱼”首次于新泽西州被捕获后,近几年来在各地接连不断地上了头条,先后出现在密歇根州,华盛顿州,伊利诺斯州,巴黎,斯堪的纳维亚,巴布亚新几内亚。在纽约甚至有一只人工捕获饲养的黑色切蛋鱼,取名buttkiss,人们认为这可能是这几座城市中幸存的最古老的鱼。由于考虑到buttkiss属于侵入的品种,人们并不打算将其放回自然里。<\/p>\r\n<p><\/p>\r\n<p>切蛋鱼有着一副怪异夸张的人类牙齿。它们使用牙齿去研磨食物,从树上掉落到水里的坚果便成了切蛋鱼的食物。<\/p>\r\n<p><!--IMG#1--><\/p>\r\n<p>“亚马逊流域中的切蛋鱼通常都是素食主义者。”生物学家Jeremy Wade在《时尚先生》(Esquire)中说道。“如果这儿的食物供给正常,它们会很高兴的。食人鱼主要食用从树上掉入水中的坚果和种子,用其非常强大的牙齿去咬开,毕竟有一些坚果还是很难咬开的。”<\/p>\r\n<p>在2013年,食人鱼的出现引发恐慌后,它们常被人们贴上错误的标签,有咬食男人的睾丸的行为。Wade称人们可能并不会相信这种传言,但也许他们对于选择裸泳上更加谨慎了。<\/p>\r\n<p>“是的,你的蛋蛋有可能会被咬,但是可能性很低。如果你真的不放心,就多穿点,我觉得你应该会没事,”Jeremy补充说。<\/p>", "otherLink" : [ ], "keyboard" : "人类牙齿", "classid" : "13", "plnum" : "0", "befrom" : "世界未解之谜大全" }, "err_msg" : "success", "info" : "读取信息内容成功!" }
我们新闻正文部分的图片用指定的占位符替代,并单独返回所有图片信息数组。我们在加载页面的时候,根据图片信息数组返回的图片尺寸和占位符,将正文部分的占位符先替换成本地的一张占位图片连接。
图片缓存:
正文部分是使用UIWebView来加载的,所以图片我们肯定不能直接使用UIImage这样的原生对象。实现这个图片缓存我这里使用的是YYWebImage进行的图片缓存,默认YY的磁盘缓存策略是超过20kb才会存储本地文件,其余存储到数据库中。所以,我们需要修改第三方库的缓存策略限制,以满足我们的需求。
缓存流程:
先拼接我们的正文部分的HTML代码,img标签的src属性则先赋值一个本地图片,并指定宽、高。然后在img标签中添加一个id并赋值图片的url,用于标识图片。
下面是完整代码,包括字体替换、图片点击事件部分。
func loadWebViewContent(model: JFArticleDetailModel) { // 如果不熟悉网页,可以换成GRMutache模板更配哦 var html = "" html += "<div class=\"title\">\(model.title!)</div>" html += "<div class=\"time\">\(model.befrom!) \(model.newstime!.timeStampToString())</div>" // 临时正文 - 这样做的目的是不修改模型 var tempNewstext = model.newstext! // 有图片才去拼接图片 if model.allphoto!.count > 0 { // 拼接图片标签 for (index, dict) in model.allphoto!.enumerate() { // 图片占位符范围 let range = (tempNewstext as NSString).rangeOfString(dict["ref"] as! String) // 默认宽、高为0 var width: CGFloat = 0 var height: CGFloat = 0 if let w = dict["pixel"]!!["width"] as? NSNumber { width = CGFloat(w.floatValue) } if let h = dict["pixel"]!!["height"] as? NSNumber { height = CGFloat(h.floatValue) } // 如果图片超过了最大宽度,才等比压缩 这个最大宽度是根据css里的container容器宽度来自适应的 if width >= SCREEN_WIDTH - 40 { let rate = (SCREEN_WIDTH - 40) / width width = width * rate height = height * rate } // 加载中的占位图 let loading = NSBundle.mainBundle().pathForResource("www/images/loading.jpg", ofType: nil)! // img标签 let imgTag = "<img onclick='didTappedImage(\(index));' src='\(loading)' id='\(dict["url"] as! String)' width='\(width)' height='\(height)' />" tempNewstext = (tempNewstext as NSString).stringByReplacingOccurrencesOfString(dict["ref"] as! String, withString: imgTag, options: NSStringCompareOptions.CaseInsensitiveSearch, range: range) } // 加载图片 - 从缓存中获取图片的本地绝对路径,发送给webView显示 getImageFromDownloaderOrDiskByImageUrlArray(model.allphoto!) } let fontSize = NSUserDefaults.standardUserDefaults().integerForKey(CONTENT_FONT_SIZE_KEY) let fontName = NSUserDefaults.standardUserDefaults().stringForKey(CONTENT_FONT_TYPE_KEY)! html += "<div id=\"content\" style=\"font-size: \(fontSize)px; font-family: '\(fontName)';\">\(tempNewstext)</div>" // 从本地加载网页模板,替换新闻主页 let templatePath = NSBundle.mainBundle().pathForResource("www/html/article.html", ofType: nil)! let template = (try! String(contentsOfFile: templatePath, encoding: NSUTF8StringEncoding)) as NSString html = template.stringByReplacingOccurrencesOfString("<p>mainnews</p>", withString: html, options: NSStringCompareOptions.CaseInsensitiveSearch, range: template.rangeOfString("<p>mainnews</p>")) let baseURL = NSURL(fileURLWithPath: templatePath) webView.loadHTMLString(filterHTML(html), baseURL: baseURL) // 已经加载过就修改标记 isLoaded = true }
完成拼接后的HTML代码:
<!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width, initial-scale=1" /> <link rel="stylesheet" type="text/css" href="../css/article.css"> <script type="text/javascript" src="../js/article.js"></script> </head> <body id="container"> <div class="title">长着人类牙齿的鱼</div><div class="time">世界未解之谜大全 06-15 21:59</div><div id="content" style="font-size: 18px; font-family: '';"><p>事情发生在加利福尼亚州,上周末,一位名叫 Gallo的男子在当地池塘里钓鱼,当他感到鱼钩被咬住的时候,用力一提,一条鱼上钩了 。但在他准备提起鱼竿取下这条鱼的时候,鱼咬断了线,坠落到了地上,这时候他开始注意到情况不对劲了。</p> <p>该男子接受新闻采访时称,“当鱼落在地上时,我清楚地知道这跟之前见过的任何鱼类都不同。”</p> <p>Gallo后来得知他钓上的是一只锯腹脂鲤(也称“切蛋鱼”,传言会咬食男性睾丸),杂食性鱼类,原产于南美洲的亚马逊流域,靠着一张诡异长着人类牙齿的嘴著名。该物种属于食人鱼的近亲,由于水族馆宠物店养殖的疏忽,现在已经在美国的很多河流和池塘中发现了其踪迹。在美国,食人鱼深受青少年人群的喜爱,因此为满足这一需求,宠物商店大肆进货,由于食物充足,缺少天敌,很多食人鱼在室内的鱼缸内过度生长,在成年甚至可以达到10-12英寸长。</p> <p><img onclick='didTappedImage(0);' src='/Users/feng/Library/Developer/CoreSimulator/Devices/8146D9C3-0D7A-4E1C-92D5-32FE42903A43/data/Containers/Bundle/Application/F786DCF8-42FB-48FF-BA98-6DEF21FDFDB9/LiuAGeIOS.app/www/images/loading.jpg' id='http://photo.6ag.cn/2016-06-15/12564f1952c7eb18827cc1d8944f631e.jpg' width='335.0' height='228.358333333333' /></p> <p>“切蛋鱼”首次于新泽西州被捕获后,近几年来在各地接连不断地上了头条,先后出现在密歇根州,华盛顿州,伊利诺斯州,巴黎,斯堪的纳维亚,巴布亚新几内亚。在纽约甚至有一只人工捕获饲养的黑色切蛋鱼,取名buttkiss,人们认为这可能是这几座城市中幸存的最古老的鱼。由于考虑到buttkiss属于侵入的品种,人们并不打算将其放回自然里。</p> <p></p> <p>切蛋鱼有着一副怪异夸张的人类牙齿。它们使用牙齿去研磨食物,从树上掉落到水里的坚果便成了切蛋鱼的食物。</p> <p><img onclick='didTappedImage(1);' src='/Users/feng/Library/Developer/CoreSimulator/Devices/8146D9C3-0D7A-4E1C-92D5-32FE42903A43/data/Containers/Bundle/Application/F786DCF8-42FB-48FF-BA98-6DEF21FDFDB9/LiuAGeIOS.app/www/images/loading.jpg' id='http://photo.6ag.cn/2016-06-15/1e86e55e38ff93df99a636a3f1c4b989.jpg' width='335.0' height='197.420701168614' /></p> <p>“亚马逊流域中的切蛋鱼通常都是素食主义者。”生物学家Jeremy Wade在《时尚先生》(Esquire)中说道。“如果这儿的食物供给正常,它们会很高兴的。食人鱼主要食用从树上掉入水中的坚果和种子,用其非常强大的牙齿去咬开,毕竟有一些坚果还是很难咬开的。”</p> <p>在2013年,食人鱼的出现引发恐慌后,它们常被人们贴上错误的标签,有咬食男人的睾丸的行为。Wade称人们可能并不会相信这种传言,但也许他们对于选择裸泳上更加谨慎了。</p> <p>“是的,你的蛋蛋有可能会被咬,但是可能性很低。如果你真的不放心,就多穿点,我觉得你应该会没事,”Jeremy补充说。</p></div> </body> </html>
加载后,首先我们看到的是正文文字和loading.jpg占位图。
然后开始图片磁盘缓存,首先我们先去缓存中查找是否已经有缓存的图片,如果有则直接使用。没有则根据URL去下载。下载完成后的操作和已经有缓存的操作是相同的。
缓存成功后,我们可以根据url获取到图片在本地磁盘的路径,这个路径我们只要想办法替换掉我们HTML代码里img标签的对应src值就行。
并且每个img标签我们都已经给了一个唯一标识替代,所以很容易实现这一点,我这里使用的是 WebViewJavascriptBridge 来完成js和swift交互的。具体可以百度或者去github看看官方说明,这里就不具体说明了。
下面代码是缓存图片和发送图片路径给js的,重点是 bridge?.send() 方法。
func getImageFromDownloaderOrDiskByImageUrlArray(imageArray: [AnyObject]) { // 循环加载图片 for dict in imageArray { // 图片url let imageString = dict["url"] as! String // 判断本地磁盘是否已经缓存 if JFArticleStorage.getArticleImageCache().containsImageForKey(imageString, withType: YYImageCacheType.Disk) { let imagePath = JFArticleStorage.getFilePathForKey(imageString) // 发送图片占位标识和本地绝对路径给webView bridge?.send("replaceimage\(imageString),\(imagePath)") } else { YYWebImageManager(cache: JFArticleStorage.getArticleImageCache(), queue: NSOperationQueue()).requestImageWithURL(NSURL(string: imageString)!, options: YYWebImageOptions.UseNSURLCache, progress: { (_, _) in }, transform: { (image, url) -> UIImage? in return image }, completion: { (image, url, type, stage, error) in dispatch_sync(dispatch_get_main_queue(), { // 确保已经下载完成并没有出错 - 这样做其实已经修改了YYWebImage的磁盘缓存策略。默认YYWebImage缓存文件时超过20kb的文件才会存储为文件,所以需要在 YYDiskCache.m的171行修改 guard let _ = image where error == nil else {return} let imagePath = JFArticleStorage.getFilePathForKey(imageString) // 发送图片占位标识和本地绝对路径给webView self.bridge?.send("replaceimage\(imageString),\(imagePath)") }) }) } } }
然后js在接收到发送的消息后,根据id替换img的src值。
connectWebViewJavascriptBridge(function (bridge) { // 从iOS bridge.send 方法过来的 就会调用到这个方法 bridge.init(function (message, responseCallback) { if (message.match("replaceimage")) { var index = message.indexOf(",") // 截取图片占位标识 例如:replaceimagehttp://photo.6ag.cn/2016-06-06/8ed0df6ea3282c42caa0b29c0cfc1832.jpg var messagereplace = message.substring(0, index) // 截取到本地图片的路径 例如:/Users/feng/Library/Developer/CoreSimulator/Devices/8146D9C3-0D7A-4E1C-92D5-32FE42903A43/data/Containers/Bundle/Application/A6A30D66-A27A-4751-A014-63AA86988676/LiuAGeIOS.app/loading.png var messagepath = message.substring(index + 1) messagereplace = messagereplace.replace(/replaceimage/, "") element = document.getElementById(messagereplace) if (element.src.match("loading")) { element.src = messagepath } } }) })
此致,缓存图片并替换占位图就完成了。
图片点击:
给图片添加点击事件,也就是给img标签添加一个onclick事件,这个事件将接收一个数值参数,标识点击的是哪一张图片。并在事件函数中将被点击的图片的标识数值发送给swift,在swift中处理后续操作。
下面代码是HTML拼接的代码:
// img标签 let imgTag = "<img onclick='didTappedImage(\(index));' src='\(loading!)' id='\(dict["url"] as! String)' width='\(width)' height='\(height)' />"
下面代码是事件处理函数:
// 图片点击事件 function didTappedImage(index) { bridge.send(index) }
bridge.send发送的消息在下面方法中接收(OC方法):
+ (instancetype)bridgeForWebView:(WVJB_WEBVIEW_TYPE*)webView webViewDelegate:(WVJB_WEBVIEW_DELEGATE_TYPE*)webViewDelegate handler:(WVJBHandler)handler;
我们是使用的swift,下面是我处理的代码:
private func setupWebViewJavascriptBridge() { bridge = WebViewJavascriptBridge(forWebView: webView, webViewDelegate: self, handler: { (data, responseCallback) in responseCallback("Response for message from ObjC") // 接收js发送过来的图片点击事件 let newsPhotoBrowserVc = JFNewsPhotoBrowserViewController() newsPhotoBrowserVc.photoParam = (self.model!.allphoto!, Int(data as! NSNumber)) self.presentViewController(newsPhotoBrowserVc, animated: true, completion: {}) }) }
至此,图片点击交互已经完成。上文中用到的WebViewJavascriptBridge如果大家比较陌生,可以先去学习一下,再来看这篇文章。
修改字体:
修改字体也比较简单,我是直接调用预先写好的js代码,传入字体、字号参数进行修改。
设置字体字号:
// 设置字体 function setFontName(name) { var content = document.getElementById('content'); content.style.fontFamily = name; } // 设置字体大小 function setFontSize(size) { var content = document.getElementById('content'); content.style.fontSize = size + "px"; }
修改字体并重新加载:
// 自动布局webView,HTML内容高度也可以通过KVO监听webView的contentSize来获取(待测) func autolayoutWebView() { let result = webView.stringByEvaluatingJavaScriptFromString("getHtmlHeight();") if let height = result { webView.frame = CGRectMake(0, 0, SCREEN_WIDTH, CGFloat((height as NSString).floatValue) + 20) tableView.tableHeaderView = webView self.activityView.stopAnimating() } } // 修改了正文字体大小,需要重新显示 添加图片缓存后,目前还有问题 func didChangeFontSize(fontSize: Int) { webView.stringByEvaluatingJavaScriptFromString("setFontSize(\"\(fontSize)\");") NSUserDefaults.standardUserDefaults().setInteger(fontSize, forKey: CONTENT_FONT_SIZE_KEY) autolayoutWebView() } // 修改了正文字体 func didChangedFontName(fontName: String) { webView.stringByEvaluatingJavaScriptFromString("setFontName(\"\(fontName)\");") NSUserDefaults.standardUserDefaults().setObject(fontName, forKey: CONTENT_FONT_TYPE_KEY) autolayoutWebView() }
如果还有什么不明白的地方可以在本文章后面回复,我看到会第一是时间回复你。
demo比较大:https://github.com/6ag/LiuAGeIOS