Xamarin Android 打造属于自己的博客园APP(4)
前言
今天早上终于把本地的Git环境搞好,然后顺利的丢到了Github上,主要是我现在用的电脑之前是其它同事用的 ,是他的Git账号,切换成我自己的用了太长时间了,也主要是以前没有咋用过Git命令。
先说地址吧:https://github.com/HuUncle/Xamarin-Android-CNBlog,觉得还可以的朋友可以点个星,毕竟我是个小菜鸟,写个APP不容易啊。
大概说下项目结构吧,整体分为两层,一个XamarinAndroid工程,一个可移植项目库,里面封装的博客园的API请求,打算在写IOS客户端的时候移植过去。
接口请求的话,全是用的异步在请求,毕竟是APP,总不能UI卡死吧。
CNBlog功能描述:
已实现功能:
1.博客园账号登录
2.分页获取首页文章,分页精华文章,分页知识库文章,分页新闻
3.获取文章、新闻评论列表,发送评论
4.文章、新闻在线收藏、分享
5.查看我的博客,查看我的收藏
6.搜索博主
APP页面效果:
1.主页:
2.文章详情:
3.评论列表:
4.个人中心:
5.搜索博主:
截图好累,更多效果的话,后面给出APK下载地址,各位自己下载安装体验吧。
主页的效果是用的ViewPager+TabLayout实现的滑动效果,也是一种很常用的APP布局方式,贴出关键代码:
布局文件:main.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <LinearLayout android:paddingTop="10dp" android:layout_gravity="center_vertical" android:background="@color/title_bg" android:paddingBottom="10dp" android:layout_height="wrap_content" android:layout_width="match_parent"> <Button android:layout_width="28dp" android:layout_height="28dp" android:layout_marginBottom="5dp" android:layout_marginTop="5dp" android:background="@drawable/titlebar_menu_selector" android:id="@+id/title_bar_left_menu" android:layout_gravity="left|center_vertical" android:layout_marginLeft="10dp" /> <TextView android:layout_height="match_parent" android:gravity="center_vertical" android:textSize="22sp" android:textColor="@android:color/white" android:layout_width="wrap_content" android:layout_marginLeft="25dp" android:text="博客园" /> <LinearLayout android:layout_height="match_parent" android:gravity="right" android:paddingRight="10dp" android:layout_width="match_parent"> <Button android:layout_width="28dp" android:layout_height="28dp" android:id="@+id/btn_blogger_search" android:layout_marginBottom="5dp" android:layout_marginTop="5dp" android:background="@drawable/titlebar_search_selector" android:layout_gravity="left|center_vertical" /> <Button android:layout_width="28dp" android:layout_height="28dp" android:layout_marginBottom="5dp" android:layout_marginTop="5dp" android:background="@drawable/titlebar_more_selector" android:layout_gravity="left|center_vertical" android:layout_marginLeft="15dp" /> </LinearLayout> </LinearLayout> <!--app:tabMode="scrollable" 这个属性我在代码中设置了--> <!-- tabLayout.setTabMode(TabLayout.MODE_SCROLLABLE);--> <android.support.design.widget.TabLayout android:id="@+id/sliding_tabs" style="@style/MyCustomTabLayout" android:background="@android:color/white" android:layout_width="match_parent" android:layout_height="wrap_content" /> <android.support.v4.view.ViewPager android:id="@+id/viewpager" android:layout_width="match_parent" android:layout_height="0px" android:layout_weight="1" android:background="@android:color/white" /> </LinearLayout>
ViewPager适配器:SimpleFragmentPagerAdapter.cs
using Android.Content; using Android.Views; using Android.Widget; using Java.Lang; using Android.Support.V4.App; using Android.Text; using CNBlog.Droid.Activities; namespace CNBlog.Droid { public class SimpleFragmentPagerAdapter : FragmentPagerAdapter { int PAGE_COUNT = 4; private string [] tabTitles = new string[] { "首页","精华", "新闻", "知识库"}; private Context context; public SimpleFragmentPagerAdapter(Android.Support.V4.App.FragmentManager fm, Context context) :base(fm) { this.context = context; } public override int Count { get { return PAGE_COUNT; } } public override Android.Support.V4.App.Fragment GetItem(int position) { switch (position) { case 0: return new IndexPageFragment(); case 1: return new EssencePageFragement(); case 2: return new NewsPageFragment(); case 3: return new RecommendPageFragment(); default: return new NewsPageFragment(); } } public override ICharSequence GetPageTitleFormatted(int position) { SpannableString sb = new SpannableString(tabTitles[position]); return sb; } public View GetTabView(int position) { View view = LayoutInflater.From(context).Inflate(Resource.Layout.tab_item, null); TextView tv = (TextView)view.FindViewById(Resource.Id.textView); tv.Text = tabTitles[position]; //ImageView img = (ImageView)view.FindViewById(Resource.Id.imageView); //img.SetImageResource(Resource.Mipmap.Icon);//设置tab的图标
return view; } } }
然后4个Tab由四个不同的Fragment分别去维护。
实际开发过程中,遇到个有趣的问题,在主页的时候,当前页面如果在第一页的时候,依次往右滑动的话,后面的页面竟然都没有刷新就把文章新闻给获取出来了,如果从第一个页面直接点到第三个页面的话,页面就有刷新效果。这就相当interesting了
然后上网搜索了下Viewpager的用法,发现Viewpager是有缓存机制的,ViewPager切换页面时默认情况下非相邻的页面会被销毁掉(ViewPager默认缓存相邻的页面以便快速切换),
可以通过设置viewPager.OffscreenPageLimit的属性设置缓存页数。But我没有设置数量为4,我不需要一次性缓存这么多,不仅浪费内存不说,也许用户可能只是首页看了一篇文章就退了,而且博客园接口调用也很快,而且CNBlog都是基于异步的,页面很流畅的,所以不需要一次性缓存那么多页面。
ListView异步网络图片加载
相信大家平时做Android应用的时候,多少会接触到异步加载图片,或者加载大量图片的问题,而加载图片我们常常会遇到许多的问题,比如说图片的错乱,OOM等问题。
在CNBlog里头显示图片最多的是用户头像,
起初是我自己写的异步去下载图片,下载完成以后将图片缓存到本地,将图片名称命名成图片下载地址,当Listview滑动加载图片时,先判断本地是否已存在,如果存在就从本地加载,没有则从网络下载,但是问题来了,有时候会发现异步下载图片的时候会错位,经过初步分析应该是异步加载图片的时候,然后listview又重用了 convertview导致的图片错位。
贴上一个园友写的文章,分析的很有道理:http://www.cnblogs.com/lesliefang/p/3619223.html
大致的解决思路是给ImageView设置一个tag,可以设置为图片下载地址,设置Imageview的图片时,比对tag是否和当前下载路径是否一致。
这个问题解决了,oom又来了,频繁下载,频繁从本地读取都是造成oom的原因。这就尴尬了,心想肯定不止自己遇到过这个问题的,果断上网一搜,android原生有一个叫做Universal-Image-Loader的框架,一看介绍,有这么多功能
- 多线程下载图片,图片可以来源于网络,文件系统,项目文件夹assets中以及drawable中等
- 支持随意的配置ImageLoader,例如线程池,图片下载器,内存缓存策略,硬盘缓存策略,图片显示选项以及其他的一些配置
- 支持图片的内存缓存,文件系统缓存或者SD卡缓存
- 支持图片下载过程的监听
- 根据控件(ImageView)的大小对Bitmap进行裁剪,减少Bitmap占用过多的内存
- 较好的控制图片的加载过程,例如暂停图片加载,重新开始加载图片,一般使用在ListView,GridView中,滑动过程中暂停加载图片,停止滑动的时候去加载图片
- 提供在较慢的网络下对图片进行加载
然后上github搜索了下关于imageloader,然后就找到了一个绑定好的imageloader库,一句话,前人种树,后人乘凉。
https://github.com/LukeForder/Xamarin-Bindings-Android-Universal-Image-Loader
用法的话,也基本和Java的一致,CNBlog里头也有实现,大家有兴趣的话可以看一下。
贴上关键代码:
BlogApplication.InitImageLoader(this.Context); ImageLoader imgLoader;= ImageLoader.Instance; DisplayImageOptions displayImageOptions = new DisplayImageOptions.Builder() .ShowImageForEmptyUri(Resource.Drawable.girl)//空URL显示图片 .ShowImageOnFail(Resource.Drawable.girl)//加载失败显示图片 .ShowImageOnLoading(Resource.Drawable.girl)//正在加载显示图片 .CacheInMemory(true)//缓存到内存 .CacheOnDisk(true)//缓存到SD卡 .ResetViewBeforeLoading() .Build();
imgLoader.DisplayImage(item.Avatar, imgView, displayImageOptions);
imgLoader.DisplayImage(“图片下载地址”,”ImageVIew”,”ImageLoader图片显示配置”);
文章详情
刚开始的纠结了很久如何显示文章详情,因为文章内容是带标签的
要在手机上显示,而且还要美观,这真是个big problem。
这里采用的方案是,用webView来显示文章内容。做法是首先在assets新建了一个Html文件,编码css,用于排版文章内容。
html代码:
<html lang="zh-cn"> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> <meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no"> <script> /* http://prismjs.com/download.html?themes=prism&languages=markup+css+clike+javascript */ var _self="undefined"!=typeof window?window:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{},Prism=function(){var e=/\blang(?:uage)?-(\w+)\b/i,t=0,n=_self.Prism={util:{encode:function(e){return e instanceof a?new a(e.type,n.util.encode(e.content),e.alias):"Array"===n.util.type(e)?e.map(n.util.encode):e.replace(/&/g,"&").replace(/</g,"<").replace(/\u00a0/g," ")},type:function(e){return Object.prototype.toString.call(e).match(/\[object (\w+)\]/)[1]},objId:function(e){return e.__id||Object.defineProperty(e,"__id",{value:++t}),e.__id},clone:function(e){var t=n.util.type(e);switch(t){case"Object":var a={};for(var r in e)e.hasOwnProperty(r)&&(a[r]=n.util.clone(e[r]));return a;case"Array":return e.map&&e.map(function(e){return n.util.clone(e)})}return e}},languages:{extend:function(e,t){var a=n.util.clone(n.languages[e]);for(var r in t)a[r]=t[r];return a},insertBefore:function(e,t,a,r){r=r||n.languages;var i=r[e];if(2==arguments.length){a=arguments[1];for(var l in a)a.hasOwnProperty(l)&&(i[l]=a[l]);return i}var o={};for(var s in i)if(i.hasOwnProperty(s)){if(s==t)for(var l in a)a.hasOwnProperty(l)&&(o[l]=a[l]);o[s]=i[s]}return n.languages.DFS(n.languages,function(t,n){n===r[e]&&t!=e&&(this[t]=o)}),r[e]=o},DFS:function(e,t,a,r){r=r||{};for(var i in e)e.hasOwnProperty(i)&&(t.call(e,i,e[i],a||i),"Object"!==n.util.type(e[i])||r[n.util.objId(e[i])]?"Array"!==n.util.type(e[i])||r[n.util.objId(e[i])]||(r[n.util.objId(e[i])]=!0,n.languages.DFS(e[i],t,i,r)):(r[n.util.objId(e[i])]=!0,n.languages.DFS(e[i],t,null,r)))}},plugins:{},highlightAll:function(e,t){var a={callback:t,selector:'code[class*="language-"], [class*="language-"] code, code[class*="lang-"], [class*="lang-"] code'};n.hooks.run("before-highlightall",a);for(var r,i=a.elements||document.querySelectorAll(a.selector),l=0;r=i[l++];)n.highlightElement(r,e===!0,a.callback)},highlightElement:function(t,a,r){for(var i,l,o=t;o&&!e.test(o.className);)o=o.parentNode;o&&(i=(o.className.match(e)||[,""])[1].toLowerCase(),l=n.languages[i]),t.className=t.className.replace(e,"").replace(/\s+/g," ")+" language-"+i,o=t.parentNode,/pre/i.test(o.nodeName)&&(o.className=o.className.replace(e,"").replace(/\s+/g," ")+" language-"+i);var s=t.textContent,u={element:t,language:i,grammar:l,code:s};if(n.hooks.run("before-sanity-check",u),!u.code||!u.grammar)return n.hooks.run("complete",u),void 0;if(n.hooks.run("before-highlight",u),a&&_self.Worker){var c=new Worker(n.filename);c.onmessage=function(e){u.highlightedCode=e.data,n.hooks.run("before-insert",u),u.element.innerHTML=u.highlightedCode,r&&r.call(u.element),n.hooks.run("after-highlight",u),n.hooks.run("complete",u)},c.postMessage(JSON.stringify({language:u.language,code:u.code,immediateClose:!0}))}else u.highlightedCode=n.highlight(u.code,u.grammar,u.language),n.hooks.run("before-insert",u),u.element.innerHTML=u.highlightedCode,r&&r.call(t),n.hooks.run("after-highlight",u),n.hooks.run("complete",u)},highlight:function(e,t,r){var i=n.tokenize(e,t);return a.stringify(n.util.encode(i),r)},tokenize:function(e,t){var a=n.Token,r=[e],i=t.rest;if(i){for(var l in i)t[l]=i[l];delete t.rest}e:for(var l in t)if(t.hasOwnProperty(l)&&t[l]){var o=t[l];o="Array"===n.util.type(o)?o:[o];for(var s=0;s<o.length;++s){var u=o[s],c=u.inside,g=!!u.lookbehind,h=!!u.greedy,f=0,d=u.alias;if(h&&!u.pattern.global){var p=u.pattern.toString().match(/[imuy]*$/)[0];u.pattern=RegExp(u.pattern.source,p+"g")}u=u.pattern||u;for(var m=0,y=0;m<r.length;y+=(r[m].matchedStr||r[m]).length,++m){var v=r[m];if(r.length>e.length)break e;if(!(v instanceof a)){u.lastIndex=0;var b=u.exec(v),k=1;if(!b&&h&&m!=r.length-1){if(u.lastIndex=y,b=u.exec(e),!b)break;for(var w=b.index+(g?b[1].length:0),_=b.index+b[0].length,A=m,S=y,P=r.length;P>A&&_>S;++A)S+=(r[A].matchedStr||r[A]).length,w>=S&&(++m,y=S);if(r[m]instanceof a||r[A-1].greedy)continue;k=A-m,v=e.slice(y,S),b.index-=y}if(b){g&&(f=b[1].length);var w=b.index+f,b=b[0].slice(f),_=w+b.length,x=v.slice(0,w),O=v.slice(_),j=[m,k];x&&j.push(x);var N=new a(l,c?n.tokenize(b,c):b,d,b,h);j.push(N),O&&j.push(O),Array.prototype.splice.apply(r,j)}}}}}return r},hooks:{all:{},add:function(e,t){var a=n.hooks.all;a[e]=a[e]||[],a[e].push(t)},run:function(e,t){var a=n.hooks.all[e];if(a&&a.length)for(var r,i=0;r=a[i++];)r(t)}}},a=n.Token=function(e,t,n,a,r){this.type=e,this.content=t,this.alias=n,this.matchedStr=a||null,this.greedy=!!r};if(a.stringify=function(e,t,r){if("string"==typeof e)return e;if("Array"===n.util.type(e))return e.map(function(n){return a.stringify(n,t,e)}).join("");var i={type:e.type,content:a.stringify(e.content,t,r),tag:"span",classes:["token",e.type],attributes:{},language:t,parent:r};if("comment"==i.type&&(i.attributes.spellcheck="true"),e.alias){var l="Array"===n.util.type(e.alias)?e.alias:[e.alias];Array.prototype.push.apply(i.classes,l)}n.hooks.run("wrap",i);var o="";for(var s in i.attributes)o+=(o?" ":"")+s+'="'+(i.attributes[s]||"")+'"';return"<"+i.tag+' class="'+i.classes.join(" ")+'"'+(o?" "+o:"")+">"+i.content+"</"+i.tag+">"},!_self.document)return _self.addEventListener?(_self.addEventListener("message",function(e){var t=JSON.parse(e.data),a=t.language,r=t.code,i=t.immediateClose;_self.postMessage(n.highlight(r,n.languages[a],a)),i&&_self.close()},!1),_self.Prism):_self.Prism;var r=document.currentScript||[].slice.call(document.getElementsByTagName("script")).pop();return r&&(n.filename=r.src,document.addEventListener&&!r.hasAttribute("data-manual")&&("loading"!==document.readyState?window.requestAnimationFrame?window.requestAnimationFrame(n.highlightAll):window.setTimeout(n.highlightAll,16):document.addEventListener("DOMContentLoaded",n.highlightAll))),_self.Prism}();"undefined"!=typeof module&&module.exports&&(module.exports=Prism),"undefined"!=typeof global&&(global.Prism=Prism); Prism.languages.markup={comment:/<!--[\w\W]*?-->/,prolog:/<\?[\w\W]+?\?>/,doctype:/<!DOCTYPE[\w\W]+?>/,cdata:/<!\[CDATA\[[\w\W]*?]]>/i,tag:{pattern:/<\/?(?!\d)[^\s>\/=$<]+(?:\s+[^\s>\/=]+(?:=(?:("|')(?:\\\1|\\?(?!\1)[\w\W])*\1|[^\s'">=]+))?)*\s*\/?>/i,inside:{tag:{pattern:/^<\/?[^\s>\/]+/i,inside:{punctuation:/^<\/?/,namespace:/^[^\s>\/:]+:/}},"attr-value":{pattern:/=(?:('|")[\w\W]*?(\1)|[^\s>]+)/i,inside:{punctuation:/[=>"']/}},punctuation:/\/?>/,"attr-name":{pattern:/[^\s>\/]+/,inside:{namespace:/^[^\s>\/:]+:/}}}},entity:/&#?[\da-z]{1,8};/i},Prism.hooks.add("wrap",function(a){"entity"===a.type&&(a.attributes.title=a.content.replace(/&/,"&"))}),Prism.languages.xml=Prism.languages.markup,Prism.languages.html=Prism.languages.markup,Prism.languages.mathml=Prism.languages.markup,Prism.languages.svg=Prism.languages.markup; Prism.languages.css={comment:/\/\*[\w\W]*?\*\//,atrule:{pattern:/@[\w-]+?.*?(;|(?=\s*\{))/i,inside:{rule:/@[\w-]+/}},url:/url\((?:(["'])(\\(?:\r\n|[\w\W])|(?!\1)[^\\\r\n])*\1|.*?)\)/i,selector:/[^\{\}\s][^\{\};]*?(?=\s*\{)/,string:{pattern:/("|')(\\(?:\r\n|[\w\W])|(?!\1)[^\\\r\n])*\1/,greedy:!0},property:/(\b|\B)[\w-]+(?=\s*:)/i,important:/\B!important\b/i,"function":/[-a-z0-9]+(?=\()/i,punctuation:/[(){};:]/},Prism.languages.css.atrule.inside.rest=Prism.util.clone(Prism.languages.css),Prism.languages.markup&&(Prism.languages.insertBefore("markup","tag",{style:{pattern:/(<style[\w\W]*?>)[\w\W]*?(?=<\/style>)/i,lookbehind:!0,inside:Prism.languages.css,alias:"language-css"}}),Prism.languages.insertBefore("inside","attr-value",{"style-attr":{pattern:/\s*style=("|').*?\1/i,inside:{"attr-name":{pattern:/^\s*style/i,inside:Prism.languages.markup.tag.inside},punctuation:/^\s*=\s*['"]|['"]\s*$/,"attr-value":{pattern:/.+/i,inside:Prism.languages.css}},alias:"language-css"}},Prism.languages.markup.tag)); Prism.languages.clike={comment:[{pattern:/(^|[^\\])\/\*[\w\W]*?\*\//,lookbehind:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0}],string:{pattern:/(["'])(\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0},"class-name":{pattern:/((?:\b(?:class|interface|extends|implements|trait|instanceof|new)\s+)|(?:catch\s+\())[a-z0-9_\.\\]+/i,lookbehind:!0,inside:{punctuation:/(\.|\\)/}},keyword:/\b(if|else|while|do|for|return|in|instanceof|function|new|try|throw|catch|finally|null|break|continue)\b/,"boolean":/\b(true|false)\b/,"function":/[a-z0-9_]+(?=\()/i,number:/\b-?(?:0x[\da-f]+|\d*\.?\d+(?:e[+-]?\d+)?)\b/i,operator:/--?|\+\+?|!=?=?|<=?|>=?|==?=?|&&?|\|\|?|\?|\*|\/|~|\^|%/,punctuation:/[{}[\];(),.:]/}; Prism.languages.javascript=Prism.languages.extend("clike",{keyword:/\b(as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|var|void|while|with|yield)\b/,number:/\b-?(0x[\dA-Fa-f]+|0b[01]+|0o[0-7]+|\d*\.?\d+([Ee][+-]?\d+)?|NaN|Infinity)\b/,"function":/[_$a-zA-Z\xA0-\uFFFF][_$a-zA-Z0-9\xA0-\uFFFF]*(?=\()/i,operator:/--?|\+\+?|!=?=?|<=?|>=?|==?=?|&&?|\|\|?|\?|\*\*?|\/|~|\^|%|\.{3}/}),Prism.languages.insertBefore("javascript","keyword",{regex:{pattern:/(^|[^\/])\/(?!\/)(\[.+?]|\\.|[^\/\\\r\n])+\/[gimyu]{0,5}(?=\s*($|[\r\n,.;})]))/,lookbehind:!0,greedy:!0}}),Prism.languages.insertBefore("javascript","string",{"template-string":{pattern:/`(?:\\\\|\\?[^\\])*?`/,greedy:!0,inside:{interpolation:{pattern:/\$\{[^}]+\}/,inside:{"interpolation-punctuation":{pattern:/^\$\{|\}$/,alias:"punctuation"},rest:Prism.languages.javascript}},string:/[\s\S]+/}}}),Prism.languages.markup&&Prism.languages.insertBefore("markup","tag",{script:{pattern:/(<script[\w\W]*?>)[\w\W]*?(?=<\/script>)/i,lookbehind:!0,inside:Prism.languages.javascript,alias:"language-javascript"}}),Prism.languages.js=Prism.languages.javascript; </script> <style type="text/css"> body{font-family:Helvetica,"Microsoft Yahei",Verdana,Helvetica,SimSun,Arial,"Arial Unicode MS",MingLiu,PMingLiu,"MS Gothic",sans-serief;margin:0;padding:0 8px;background-color:#efeff0;color:#333;word-wrap:break-word;background-color:#FFFFFF;} p{margin-top:0;margin-bottom:5pt;line-height: 1.6em;} #header{text-align:center;background:transparent white repeat-x scroll center bottom; padding-top:6pt;margin-bottom:5pt;-webkit-background-size:320px 2px;} #header h3{margin-bottom:0px; margin-top:5px;font-size:14pt;padding:0 5pt;color:#464646;line-height:1.3em;} .describe{color:#8e8e8e;font-size:12pt;padding:4pt 0; color:#333;} #info{ font-size:10pt;line-height:1.6; color:#787878;} #content{ font-size:12pt;line-height:1.8;} img{max-width:80%;height:auto;} div.bimg{text-align:center;padding:0;} .photo_title{font-weight:bold;font-size:14pt;margin-top:15px;} .langs_cn{color:#006200;} audio{width:100%} *{-webkit-touch-callout: none; /* prevent callout to copy image, etc when tap to hold */ /*-webkit-text-size-adjust: none;*/ /* prevent webkit from resizing text to fit */ -webkit-tap-highlight-color: rgba(0,0,0,0.15); /* make transparent link selection, adjust last value opacity 0 to 1.0 */ /*-webkit-user-select: none; /* prevent copy paste, to allow, change 'none' to 'text' */ } @media screen and (-webkit-device-pixel-ratio: 2) { #header{background-image:transparent white repeat-x scroll center bottom;-webkit-background-size:320px 1px;} } #content{font-size:15px} pre{border:solid 1px #cdcdcd;background:#f5f5f5;padding:15px 5px} pre {word-break:normal; width:auto; display:block; white-space:pre-wrap;word-wrap : break-word ;overflow: hidden ;} pre { tab-width: 4; font-size:12px; } </style> </head> <body> <div id="header"> <h3> #title# </h3> <div class="describe"><p/>#author#<p/> <div id="info"><p/>#time#</div> </div> </div> <div id="content"> #content# </div> </body> </html>
然后替换html中的title、author、time、content 为实际内容,还加载了prismjs,用来美化code。
一些WebView的设置 ArticleDetailActivity:
using System; using System.IO; using Android.App; using Android.Content; using Android.Graphics.Drawables; using Android.OS; using Android.Support.Design.Widget; using Android.Util; using Android.Views; using Android.Webkit; using Android.Widget; using CNBlog.Droid.PullableView; using CNBlog.Droid.Utils; using CNBlogAPI.Model; using CNBlogAPI.Service; using Newtonsoft.Json; using Msg = Sino.Droid.AppMsg; namespace CNBlog.Droid.Activities { [Activity(Label = "ArticleDetailActivity")] public class ArticleDetailActivity : Activity,OnRefreshListener { PullableWebView webView; PullToRefreshLayout ptrl; Article article; string content; Button btnViewComments; Button btnWriteComments; protected override void OnCreate(Bundle savedInstanceState) { base.OnCreate(savedInstanceState); SetContentView(Resource.Layout.article_detail); article = JsonConvert.DeserializeObject<Article>(Intent.GetStringExtra("current")); FindViewById<TextView>(Resource.Id.head_title).Text = "文章详情"; Button btnBack = FindViewById<Button>(Resource.Id.title_bar_back); btnBack.Click+=delegate { Finish();}; bindControls(); } private async void bindControls() { btnWriteComments = FindViewById<Button>(Resource.Id.footbar_write_comment); btnWriteComments.Click+=delegate { BottomSheetDialog bottomSheetDiaolog = new BottomSheetDialog(this); var inflater = GetSystemService(Context.LayoutInflaterService) as LayoutInflater; EditText textComments = FindViewById<EditText>(Resource.Id.text_comments); View view = inflater.Inflate(Resource.Layout.write_comments, null); TextView textCancel = view.FindViewById<TextView>(Resource.Id.text_cancel); textCancel.Click+=delegate { bottomSheetDiaolog.Dismiss();}; TextView textSend = view.FindViewById<TextView>(Resource.Id.text_send_comments); textSend.Click+=async delegate { string content = textComments.Text; if (string.IsNullOrWhiteSpace(content)) { Msg.AppMsg.MakeText(this, "请输入评论内容", Msg.AppMsg.STYLE_INFO).Show(); return; } Dialog waitDialog = CommonHelper.CreateLoadingDialog(this, "正在发送评论数据,请稍后..."); try { waitDialog.Show(); if (await BlogService.AddArticleComments(CommonHelper.token, article.BlogApp, article.Id, content)) { Msg.AppMsg.MakeText(this, GetString(Resource.String.publish_comments_success), Msg.AppMsg.STYLE_INFO).Show(); bottomSheetDiaolog.Dismiss(); } else Msg.AppMsg.MakeText(this, GetString(Resource.String.publish_comments_fail), Msg.AppMsg.STYLE_INFO).Show(); } catch (Exception ex) { Msg.AppMsg.MakeText(this, GetString(Resource.String.publish_comments_fail), Msg.AppMsg.STYLE_INFO).Show(); Log.Debug("error:", ex.Message); } finally { waitDialog.Cancel(); } }; bottomSheetDiaolog.SetContentView(view); bottomSheetDiaolog.Show(); }; webView = FindViewById<PullableWebView>(Resource.Id.webview); ptrl = FindViewById<PullToRefreshLayout>(Resource.Id.refresh_view); ptrl.setOnRefreshListener(this); webView.Settings.DefaultTextEncodingName = "utf-8"; webView.Settings.LoadsImagesAutomatically = true; webView.SetWebViewClient(new MyWebViewClient()); webView.ScrollBarStyle = ScrollbarStyles.InsideOverlay; webView.Settings.JavaScriptEnabled = false; webView.Settings.SetSupportZoom(false); webView.Settings.BuiltInZoomControls = false; webView.Settings.CacheMode = CacheModes.CacheElseNetwork; webView.Settings.SetLayoutAlgorithm(WebSettings.LayoutAlgorithm.SingleColumn); //webView.Settings.UseWideViewPort = true;//设置此属性,可任意比例缩放 btnViewComments = FindViewById<Button>(Resource.Id.footbar_comments); btnViewComments.Click+=delegate { Intent intent = new Intent(this, typeof(ArticleCommentsActivity)); intent.PutExtra("current", JsonConvert.SerializeObject(article)); StartActivity(intent); }; CommonHelper.InitalShare(this, null, true, article.Author, article.Title, article.Avatar, article.Url); CommonHelper.InitalBookMark(this, article.Url, article.Title); await ptrl.AutoRefresh(); } public void onRefresh(PullToRefreshLayout pullToRefreshLayout) { BaseService.ExeRequest(async () => { content = await BlogService.GetArticleContent(CommonHelper.token, article.Id); content = content.Replace("\\r\\n", "</br>").Replace("\\n","</br>").Replace("\\t", " ").Replace("\\", string.Empty); content = content.Substring(1, content.Length - 2); using (var stream = Assets.Open("articlecontent.html")) { StreamReader sr = new StreamReader(stream); string html = sr.ReadToEnd(); sr.Close(); sr.Dispose(); html = html.Replace("#title#", article.Title) .Replace("#author#", article.Author) .Replace("#time#", article.PostDate.ToShortDateString()) .Replace("#content#", content); webView.LoadDataWithBaseURL("file:///android_asset/", html, "text/html", "utf-8", null); } pullToRefreshLayout.refreshFinish(0); },this); } public void onLoadMore(PullToRefreshLayout pullToRefreshLayout) { } public bool CanLoadMore() { return false; } } public class MyWebViewClient : WebViewClient { public override void OnPageFinished(WebView view, string url) { if (!view.Settings.LoadsImagesAutomatically) { view.Settings.LoadsImagesAutomatically = true; } } public override bool ShouldOverrideUrlLoading(WebView view, IWebResourceRequest request) { return false; } } }
实际中,还遇到点儿坑,返回的内容里面包含了一大堆的\r\n ,\\,\t 等一些转义符,还得把\r\n替换成换行符</br>,\t转换成四个空格。
content = content.Replace("\\r\\n", "</br>").Replace("\\n","</br>").Replace("\\t", " ").Replace("\\", string.Empty);
目前样式的话,勉强还能看,只能慢慢优化了,毕竟前端美化有点儿弱。
一键分享
CNBlog分享用的是Mob ShareSDK,官网地址:http://www.mob.com。
本来还很头痛的,一个群友分享了一个他已经绑定好的例子。地址:https://github.com/wtffly/DroidBinding_ShareSDK
用法也很简单,目前的话只能QQ,以及QQ空间分享。分享到每个平台都需要去申请每个平台的开发者权限…
对于我们个人开发者来说,太麻烦了。目前CNBlog里头的分享Key是官方Demo里头的,暂时只能支持QQ,以及QQ空间分享,只能等着慢慢去申请,微信的已经快一周了,都没反应,估计没戏。
最后
如果从GitHub Clone下来编译不过的话,一般都是你被墙了,因为VS需要还原这些包,要从谷歌下载这些开发包
这里提供我自己收集的一些开发包:http://pan.baidu.com/s/1jHEkh50
然后下载解压后放到C:\Users\__\AppData\Local\Xamarin\zips目录下就行了。
上篇文章提供了一个APK,有个园友说不能安装,应该是我的CPU架构支持的不够,特重新编译了一个APK,下载地址:
http://pan.baidu.com/s/1eRXVcZ4
转载请注明出处 IT胡小帅: http://www.cnblogs.com/CallMeUncle/p/6186006.html