网易新闻app内容详情页实现分析

/ 0

前言:最近我也写了个资讯app并且开源了,这里我就以我写的这个app为例,来简单实现网易新闻详情页。相信大家都玩过不少资讯新闻app,今日头条、网易新闻、新浪新闻。。等等各种新闻app都是非常优秀和值得借鉴学习的。测试截图我已经开启了慢网速模式,所以加载速度比较慢,这样才能明显的看到我们的需求。

功能分析

1

图片缓存:

详情正文是在请求到数据后才开始刷新页面,并且第一次刷新后,会先加载文字内容部分,而图片会先以同等尺寸的占位图代替,等图片下载完成后,会自动替换掉占位图。如果下一次再次打开这篇文章,文字和图片会同时加载出来,说明上次加载图片已经将图片给缓存了。

2

图片点击:

点击页面内的图片,会加载一个图片浏览器用于单独展示图片。实现这个功能,被点击的图片必定会有事件监听,我们可以为图片添加js点击事件,来监听图片点击。在图片点击后,向swift发送事件,并传递被点击的图片或者图片的索引。

3

修改字体:

动态修改字体文件和文字尺寸功能在网页中非常容易实现,因为网页中我们所看到的样式其实基本都是由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>该男子接受新闻采访时称,&ldquo;当鱼落在地上时,我清楚地知道这跟之前见过的任何鱼类都不同。&rdquo;<\/p>\r\n<p>Gallo后来得知他钓上的是一只锯腹脂鲤(也称&ldquo;切蛋鱼&rdquo;,传言会咬食男性睾丸),杂食性鱼类,原产于南美洲的亚马逊流域,靠着一张诡异长着人类牙齿的嘴著名。该物种属于食人鱼的近亲,由于水族馆宠物店养殖的疏忽,现在已经在美国的很多河流和池塘中发现了其踪迹。在美国,食人鱼深受青少年人群的喜爱,因此为满足这一需求,宠物商店大肆进货,由于食物充足,缺少天敌,很多食人鱼在室内的鱼缸内过度生长,在成年甚至可以达到10-12英寸长。<\/p>\r\n<p><!--IMG#0--><\/p>\r\n<p>&ldquo;切蛋鱼&rdquo;首次于新泽西州被捕获后,近几年来在各地接连不断地上了头条,先后出现在密歇根州,华盛顿州,伊利诺斯州,巴黎,斯堪的纳维亚,巴布亚新几内亚。在纽约甚至有一只人工捕获饲养的黑色切蛋鱼,取名buttkiss,人们认为这可能是这几座城市中幸存的最古老的鱼。由于考虑到buttkiss属于侵入的品种,人们并不打算将其放回自然里。<\/p>\r\n<p><\/p>\r\n<p>切蛋鱼有着一副怪异夸张的人类牙齿。它们使用牙齿去研磨食物,从树上掉落到水里的坚果便成了切蛋鱼的食物。<\/p>\r\n<p><!--IMG#1--><\/p>\r\n<p>&ldquo;亚马逊流域中的切蛋鱼通常都是素食主义者。&rdquo;生物学家Jeremy Wade在《时尚先生》(Esquire)中说道。&ldquo;如果这儿的食物供给正常,它们会很高兴的。食人鱼主要食用从树上掉入水中的坚果和种子,用其非常强大的牙齿去咬开,毕竟有一些坚果还是很难咬开的。&rdquo;<\/p>\r\n<p>在2013年,食人鱼的出现引发恐慌后,它们常被人们贴上错误的标签,有咬食男人的睾丸的行为。Wade称人们可能并不会相信这种传言,但也许他们对于选择裸泳上更加谨慎了。<\/p>\r\n<p>&ldquo;是的,你的蛋蛋有可能会被咬,但是可能性很低。如果你真的不放心,就多穿点,我觉得你应该会没事,&rdquo;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!)&nbsp;&nbsp;&nbsp;&nbsp;\(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">世界未解之谜大全&nbsp;&nbsp;&nbsp;&nbsp;06-15 21:59</div><div id="content" style="font-size: 18px; font-family: '';"><p>事情发生在加利福尼亚州,上周末,一位名叫 Gallo的男子在当地池塘里钓鱼,当他感到鱼钩被咬住的时候,用力一提,一条鱼上钩了 。但在他准备提起鱼竿取下这条鱼的时候,鱼咬断了线,坠落到了地上,这时候他开始注意到情况不对劲了。</p>
<p>该男子接受新闻采访时称,&ldquo;当鱼落在地上时,我清楚地知道这跟之前见过的任何鱼类都不同。&rdquo;</p>
<p>Gallo后来得知他钓上的是一只锯腹脂鲤(也称&ldquo;切蛋鱼&rdquo;,传言会咬食男性睾丸),杂食性鱼类,原产于南美洲的亚马逊流域,靠着一张诡异长着人类牙齿的嘴著名。该物种属于食人鱼的近亲,由于水族馆宠物店养殖的疏忽,现在已经在美国的很多河流和池塘中发现了其踪迹。在美国,食人鱼深受青少年人群的喜爱,因此为满足这一需求,宠物商店大肆进货,由于食物充足,缺少天敌,很多食人鱼在室内的鱼缸内过度生长,在成年甚至可以达到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>&ldquo;切蛋鱼&rdquo;首次于新泽西州被捕获后,近几年来在各地接连不断地上了头条,先后出现在密歇根州,华盛顿州,伊利诺斯州,巴黎,斯堪的纳维亚,巴布亚新几内亚。在纽约甚至有一只人工捕获饲养的黑色切蛋鱼,取名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>&ldquo;亚马逊流域中的切蛋鱼通常都是素食主义者。&rdquo;生物学家Jeremy Wade在《时尚先生》(Esquire)中说道。&ldquo;如果这儿的食物供给正常,它们会很高兴的。食人鱼主要食用从树上掉入水中的坚果和种子,用其非常强大的牙齿去咬开,毕竟有一些坚果还是很难咬开的。&rdquo;</p>
<p>在2013年,食人鱼的出现引发恐慌后,它们常被人们贴上错误的标签,有咬食男人的睾丸的行为。Wade称人们可能并不会相信这种传言,但也许他们对于选择裸泳上更加谨慎了。</p>
<p>&ldquo;是的,你的蛋蛋有可能会被咬,但是可能性很低。如果你真的不放心,就多穿点,我觉得你应该会没事,&rdquo;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