好久好久不写代码了,也好久没更新博客了,这次就和大家分享一个电子书阅读器分页的算法吧。
像一些主流的阅读器,如QQ阅读、iReader等,都实现了txt文档分页显示的功能,打开一个txt文档可以快速把文档分割成若干页,每页文字正好铺满屏幕,点击翻屏显示下一页,这样不用操作滚动条,阅读体验更好。那么如何实现txt文件的快速分页呢?如果计算屏幕大小和字体大小算出一屏可以有多少字,这样肯定不行,因为有英文和其他字符比如中文和标点,还有换行回车,所以肯定要先设置一块和显示屏一样大的容器(可变高度),首先根据屏幕大小和字体大小算出一屏可以容纳的字数最大值,从文件读出这些字数,将这段文字写入设置好的那个容器,如果容器高度大于屏幕高度,则减少文字读取的长度,再次试探容器高度是否大于屏幕高度,直到容器高度和屏幕高度一致,则当前屏幕应该显示的内容就是容器内的内容。要是一个字一个字试探效率太低,我改进了算法,每次减去20字,当减到容器高度小于屏幕高度时,在反向微调,添加一个字,这样分页的效率大大提高,点击显示下一页看不出延迟。核心代码如下图
前些日子写了个阅读器demo,只是实现了文件浏览,打开文件和文件分页功能,阅读器主题设置,阅读历史存档等还没做,因为这些都比较简单了,懒得去做了,大家有兴趣可以在此基础上修改成成品。下面先上效果图再上完整代码,这个是用xml做的视图层,JScript做的逻辑层,可以运行于windows7及以上系统。
首先是效果图:
主题设置目前只做了字体和背景颜色的设置,所有的设置都存在一个对象之中,可以通过读写json文件存储,读写我已经封装到IOMod这个模块了,可以直接拿来用getSetting和setSetting两个方法。
最后编译完成的文件如下:
下面上代码:首先是视图模块viewMod.js
/*应用程序界面模块*/
this.viewMod={
root:"F:\\电子书",totalNum:0,ramPoint:0,textData:"",S:{},screen:"",
init:function(screen){
this.S=ioMod.getSetting();
/*读取配置,fontSize,color,background,lastRead*/
var SC=document.querySelector(screen);
SC.style.fontSize=this.S.fontSize+"px";
SC.style.color=this.S.color;
document.body.style.background=this.S.background;
var SC_BAR=document.querySelector("readerViewBar");
SC_BAR.style.borderBottom="1px solid "+this.S.color;
SC_BAR.style.color=this.S.color;
this.screenX=SC.offsetWidth;
this.screenY=SC.offsetHeight;
this.screen=screen;
},
print:function(item,data){
document.querySelector(item).innerHTML=data;
},
printT:function(item,data){
document.querySelector(item).innerText=data;
},
fileList:function(folder){
folder=(folder==undefined)?this.root:folder;
/*文件夹和电子书列表的显示*/
var lastFolder;
if(folder.indexOf("\\")<0){
lastFolder=folder;
}else{
lastFolder=folder.split("\\");
lastFolder.splice(lastFolder.length-1,1);
lastFolder=lastFolder.join("\\");
}
var view="<bookItem onclick=\"viewMod.fileList('"+lastFolder.replace(/\\/g,"\\\\")+"')\"><<返回上级</bookItem>";
var data=ioMod.getFolderList(folder);
for(var i in data){
view+="<bookItem onclick=\"viewMod.fileList('"+data[i].url.replace(/\\/g,"\\\\")+"')\"><img src='./src/folder.png'/>";
view+=data[i].name;
view+="</bookItem>";
}
data=ioMod.getBookList(folder);
for(var i in data){
view+="<bookItem onclick=\"viewMod.openBook('"+data[i].url.replace(/\\/g,"\\\\")+"','"+data[i].name+"')\"><img src='./src/app.ico'/>";
view+=data[i].name;
view+="</bookItem>";
}
this.print("bookFileList",view);
},
openBook:function(url,name){
/*打开文件*/
this.ramPoint=0;
this.textData=ioMod.openFile(url);
var data=this.getTextData("next");
this.printT(this.screen,data);
this.printT("titleBar","文件->"+name);
},
getTextData:function(direction){
/*分页*/
if(document.querySelector("SYS_TEXT_SCREEN")==null){
var SYS_TEXT_SC=document.createElement("SYS_TEXT_SCREEN");
SYS_TEXT_SC.style.position="absolute";
SYS_TEXT_SC.style.top="0px";
SYS_TEXT_SC.style.left="0px";
SYS_TEXT_SC.style.visibility="hidden";
document.body.appendChild(SYS_TEXT_SC);
}//创建文本读取容器
var SYS_TEXT=document.querySelector("SYS_TEXT_SCREEN");
SYS_TEXT.style.width=this.screenX+"px";
SYS_TEXT.style.fontSize=this.S.fontSize+"px";
var ramLength=Math.round((this.screenX/this.S.fontSize)*(this.screenY/this.S.fontSize));
ramLength=ramLength>1500?1500:ramLength;
var data=this.textData;
var ramLength=data.length>ramLength?ramLength:data.length;//每页大约的文字长度
if(direction=="next"){
SYS_TEXT.innerText=data.substr(this.ramPoint,ramLength);
while(SYS_TEXT.offsetHeight>this.screenY){
ramLength-=20;
SYS_TEXT.innerText=data.substr(this.ramPoint,ramLength);
}
while(SYS_TEXT.offsetHeight<this.screenY){
ramLength+=1;
SYS_TEXT.innerText=data.substr(this.ramPoint,ramLength);
}
ramLength-=1;
SYS_TEXT.innerText=SYS_TEXT.innerText.substr(0,ramLength);
this.ramPoint+=ramLength;
}else{
ramLength=this.ramPoint-ramLength<=0?this.ramPoint:ramLength;
SYS_TEXT.innerText=data.substr(this.ramPoint-ramLength,ramLength);
var IS_LONG=false;
while(SYS_TEXT.offsetHeight>this.screenY){
IS_LONG=true;
ramLength-=20;
SYS_TEXT.innerText=data.substr(this.ramPoint-ramLength,ramLength);
}
if(IS_LONG){
while(SYS_TEXT.offsetHeight<this.screenY){
ramLength+=1;
SYS_TEXT.innerText=data.substr(this.ramPoint-ramLength,ramLength);
}
ramLength-=1;
}
this.ramPoint-=ramLength;
}
this.printT("speedBar","进度"+(this.ramPoint/this.textData.length*100).toFixed(2)+"%");
return SYS_TEXT.innerText;
},
nextPage:function(){
/*下一页*/
if(this.ramPoint>this.textData.length){
}else{
var data=this.getTextData("next");
this.printT(this.screen,data);
}
},
lastPage:function(){
/*上一页*/
if(this.ramPoint<=0){
}else{
var data=this.getTextData("last");
this.printT(this.screen,data);
}
},
diaLog:function(view){
return showModalDialog(view+".view");
},
setColors:function(type){
var color=this.diaLog("colors");
if(color==undefined){
return;
}
if(type=="color"){
this.S.color=color;
document.querySelector(this.screen).style.color=this.S.color;
document.querySelector("readerViewBar").style.color=this.S.color;
document.querySelector("readerViewBar").style.borderBottom="1px solid "+this.S.color;
document.querySelector("fontColor").style.color=this.S.color;
}else{
this.S.background=color;
document.body.style.background=this.S.background;
document.querySelector("backgroundColor").style.color=this.S.background;
}
}
};
其次是文件读写模块ioMod.js
/*文件读写模块*/
this.ioMod={
getBookList:function(folder){
var data=[];
/*取得书籍列表*/
var fso=new ActiveXObject("Scripting.FileSystemObject");
var f=fso.GetFolder(folder);
var fc=new Enumerator(f.files);
var books=/(.txt|.pdf)/i;
for (; !fc.atEnd(); fc.moveNext())
{
if(books.test(fc.item())==true){
data.push({url:""+fc.item(),name:fc.item().name});
}
}
return data;
},
getFolderList:function(folder){
/*取得文件夹列表*/
var data=[];
var fso=new ActiveXObject("Scripting.FileSystemObject");
var f=fso.GetFolder(folder);
var fc=new Enumerator(f.SubFolders);
var str="\r\n";
for (; !fc.atEnd(); fc.moveNext())
{
data.push({url:""+fc.item(),name:fc.item().name});
}
return data;
},
openFile:function(url){
/*读取文件流*/
var fso=new ActiveXObject("Scripting.FileSystemObject");
var fl=fso.OpenTextFile(url,1);
doc=fl.ReadAll();
fl.Close();
return doc;
},
getSetting:function(){
/*读取配置*/
var fso=new ActiveXObject("Scripting.FileSystemObject");
var fl=fso.OpenTextFile("setting.json",1);
data=fl.ReadAll();
data=JSON.parse(data);
fl.Close();
return data;
},
setSetting:function(data){
/*写入配置*/
data=JSON.stringify(data);
var fso=new ActiveXObject("Scripting.FileSystemObject");
var tf=fso.CreateTextFile("setting.json",true);
tf.Write(data);
tf.Close();
},
write:function(data,fileName){
/*写入配置*/
var fso=new ActiveXObject("Scripting.FileSystemObject");
var tf=fso.CreateTextFile(fileName,true);
tf.Write(data);
tf.Close();
},
}
然后是主视图main.hta
<!doctype html>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=9" >
<title>文本阅读器v1.0</title>
<!-- hta应用程序描述 -->
<hta:application
id=musicPlayer
scroll=no
innerborder=no
icon=./src/app.ico
contextMenu=no
selection=no
windowState=maximize
>
<style>body{scrollbar-highlight-color:#000;}</style>
<body style="background: #ccc">
<bookListView
style="position:absolute;left:0px;top:0px;width:24%;height:100%;min-width:340px;min-height:700px;filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#666666', endColorstr='#000000',GradientType=0 ); ">
<bookListBar style="position: absolute;left:10px;top:10px;right:10px;padding:5px;font-size:25px;border:1px solid #fff;border-radius:5px;font-size:22px;background:#ccc;">
最近阅读
</bookListBar>
<style>
settingItem,bookItem{display: block;padding: 5px;margin-top: 5px;font-size: 22px;color:#fff;border:1px solid #fff;border-radius: 10px;overflow: hidden;text-overflow: ellipsis;white-space: nowrap;cursor:pointer;
}
settingItem:hover,bookItem:hover{display: block;padding: 5px;margin-top: 5px;font-size: 22px;color:#fff;border:1px solid #fff;border-radius: 10px;overflow: hidden;text-overflow: ellipsis;white-space: nowrap;cursor:pointer;background:#38f;
}
bookItem img{width: 22px;height: 22px;}
</style>
<readHistoryList style="position: absolute;left:10px;top:50px;right:10px;height:100px;padding:5px;font-size:25px;border:1px solid #fff;border-radius:5px;font-size:22px;color:#fff;overflow-y: scroll;">
<bookItem><img src='./src/app.ico'/>测试文件.pdf</bookItem>
</readHistoryList>
<bookListBar style="position: absolute;left:10px;top:180px;right:10px;padding:5px;font-size:25px;border:1px solid #fff;border-radius:5px;font-size:22px;background:#ccc;">
浏览文件
</bookListBar>
<bookFileList style="position: absolute;left:10px;top:220px;right:10px;bottom:20px;padding:5px;font-size:25px;border:1px solid #fff;border-radius:5px;font-size:22px;color:#fff;overflow-y: scroll;">
</bookFileList>
</bookListView>
<readerViewBar style="position: absolute;left:25%;right: 25%;min-width:660px;top:5px;height:25px;padding:5px 0px 0px 5px;">
<speedBar>进度:</speedBar>
<titleBar>文件-></titleBar>
</readerViewBar>
<readerView style="position: absolute;left:25%;right: 25%;min-width:660px;top:40px;bottom:20px;" onclick="viewMod.nextPage()" oncontextMenu="viewMod.lastPage();">
</readerView>
<settingListView
style="position:absolute;right:0px;top:0px;width:24%;height:100%;min-width:340px;min-height:700px;filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#666666', endColorstr='#000000',GradientType=0 );">
<themeBar style="position: absolute;left:10px;top:10px;right:10px;padding:5px;font-size:25px;border:1px solid #fff;border-radius:5px;font-size:22px;background:#ccc;">
主题设置
</themeBar>
<themeList style="position: absolute;left:10px;top:50px;right:10px;height:100px;padding:5px;font-size:25px;border:1px solid #fff;border-radius:5px;font-size:22px;color:#fff;">
<span onClick=viewMod.setColors("color")>字体颜色:</span><fontColor style="color: green;">■</fontColor><br>
<span onClick=viewMod.setColors("background")>背景颜色:</span><backgroundColor style="color: #000;">■</backgroundColor><br>
<span>字体大小:24px</span><br>
<span>字体设置:宋体</span>
</themeList>
<functionBar style="position: absolute;left:10px;top:180px;right:10px;padding:5px;font-size:25px;border:1px solid #fff;border-radius:5px;font-size:22px;background:#ccc;">
系统设置
</functionBar>
<functionList style="position: absolute;left:10px;top:220px;right:10px;bottom:20px;padding:5px;font-size:25px;border:1px solid #fff;border-radius:5px;font-size:22px;color:#fff;overflow-y: scroll;">
<settingItem>添加书签</settingItem>
<settingItem>书签管理</settingItem>
</functionList>
</settingListView>
</body>
<script src="./ioMod.js"></script>
<script src="./pageMod.js"></script>
<script src="./viewMod.js"></script>
<script>
this.onload=function(){
viewMod.init("readerView");
viewMod.fileList();
}
</script>
然后是颜色选择器视图colors.view
<!DOCTYPE html>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=9" >
<title>文本阅读器【颜色选择控件】</title>
<body>
</body>
<script>
this.colors=[{"name":"浅粉色","value":"#FFB6C1"},{"name":"粉红","value":"#FFC0CB"},{"name":"猩红","value":"#DC143C"},{"name":"脸红的淡紫色","value":"#FFF0F5"},{"name":"苍白的紫罗兰红色","value":"#DB7093"},{"name":"热情的粉红","value":"#FF69B4"},{"name":"深粉色","value":"#FF1493"},{"name":"适中的紫罗兰红色","value":"#C71585"},{"name":"兰花的紫色","value":"#DA70D6"},{"name":"蓟","value":"#D8BFD8"},{"name":"李子","value":"#DDA0DD"},{"name":"紫罗兰","value":"#EE82EE"},{"name":"洋红","value":"#FF00FF"},{"name":"灯笼海棠(紫红色)","value":"#FF00FF"},{"name":"深洋红色","value":"#8B008B"},{"name":"紫色","value":"#800080"},{"name":"适中的兰花紫","value":"#BA55D3"},{"name":"深紫罗兰色","value":"#9400D3"},{"name":"深兰花紫","value":"#9932CC"},{"name":"靛青","value":"#4B0082"},{"name":"深紫罗兰的蓝色","value":"#8A2BE2"},{"name":"适中的紫色","value":"#9370DB"},{"name":"适中的板岩暗蓝灰色","value":"#7B68EE"},{"name":"板岩暗蓝灰色","value":"#6A5ACD"},{"name":"深岩暗蓝灰色","value":"#483D8B"},{"name":"薰衣草花的淡紫色","value":"#E6E6FA"},{"name":"幽灵的白色","value":"#F8F8FF"},{"name":"纯蓝","value":"#0000FF"},{"name":"适中的蓝色","value":"#0000CD"},{"name":"午夜的蓝色","value":"#191970"},{"name":"深蓝色","value":"#00008B"},{"name":"海军蓝","value":"#000080"},{"name":"宝蓝","value":"#4169E1"},{"name":"矢车菊的蓝色","value":"#6495ED"},{"name":"淡钢蓝","value":"#B0C4DE"},{"name":"浅石板灰","value":"#778899"},{"name":"石板灰","value":"#708090"},{"name":"道奇蓝","value":"#1E90FF"},{"name":"爱丽丝蓝","value":"#F0F8FF"},{"name":"钢蓝","value":"#4682B4"},{"name":"淡蓝色","value":"#87CEFA"},{"name":"天蓝色","value":"#87CEEB"},{"name":"深天蓝","value":"#00BFFF"},{"name":"淡蓝","value":"#ADD8E6"},{"name":"火药蓝","value":"#B0E0E6"},{"name":"军校蓝","value":"#5F9EA0"},{"name":"蔚蓝色","value":"#F0FFFF"},{"name":"淡青色","value":"#E1FFFF"},{"name":"苍白的绿宝石","value":"#AFEEEE"},{"name":"青色","value":"#00FFFF"},{"name":"水绿色","value":"#00FFFF"},{"name":"深绿宝石","value":"#00CED1"},{"name":"深石板灰","value":"#2F4F4F"},{"name":"深青色","value":"#008B8B"},{"name":"水鸭色","value":"#008080"},{"name":"适中的绿宝石","value":"#48D1CC"},{"name":"浅海洋绿","value":"#20B2AA"},{"name":"绿宝石","value":"#40E0D0"},{"name":"绿玉","value":"#7FFFAA"},{"name":"适中的碧绿色","value":"#00FA9A"},{"name":"适中的春天的绿色","value":"#F5FFFA"},{"name":"薄荷奶油","value":"#00FF7F"},{"name":"春天的绿色","value":"#3CB371"},{"name":"海洋绿","value":"#2E8B57"},{"name":"蜂蜜","value":"#F0FFF0"},{"name":"淡绿色","value":"#90EE90"},{"name":"苍白的绿色","value":"#98FB98"},{"name":"深海洋绿","value":"#8FBC8F"},{"name":"酸橙绿","value":"#32CD32"},{"name":"酸橙色","value":"#00FF00"},{"name":"森林绿","value":"#228B22"},{"name":"纯绿","value":"#008000"},{"name":"深绿色","value":"#006400"},{"name":"查特酒绿","value":"#7FFF00"},{"name":"草坪绿","value":"#7CFC00"},{"name":"绿黄色","value":"#ADFF2F"},{"name":"橄榄土褐色","value":"#556B2F"},{"name":"米色(浅褐色)","value":"#6B8E23"},{"name":"浅秋麒麟黄","value":"#FAFAD2"},{"name":"象牙色","value":"#FFFFF0"},{"name":"浅黄色","value":"#FFFFE0"},{"name":"纯黄","value":"#FFFF00"},{"name":"橄榄","value":"#808000"},{"name":"深卡其布","value":"#BDB76B"},{"name":"柠檬薄纱","value":"#FFFACD"},{"name":"灰秋麒麟","value":"#EEE8AA"},{"name":"卡其布","value":"#F0E68C"},{"name":"金","value":"#FFD700"},{"name":"玉米色","value":"#FFF8DC"},{"name":"秋麒麟","value":"#DAA520"},{"name":"花的白色","value":"#FFFAF0"},{"name":"老饰带","value":"#FDF5E6"},{"name":"小麦色","value":"#F5DEB3"},{"name":"鹿皮鞋","value":"#FFE4B5"},{"name":"橙色","value":"#FFA500"},{"name":"番木瓜","value":"#FFEFD5"},{"name":"漂白的杏仁","value":"#FFEBCD"},{"name":"Navajo白","value":"#FFDEAD"},{"name":"古代的白色","value":"#FAEBD7"},{"name":"晒黑","value":"#D2B48C"},{"name":"结实的树","value":"#DEB887"},{"name":"(浓汤)乳脂,番茄等","value":"#FFE4C4"},{"name":"深橙色","value":"#FF8C00"},{"name":"亚麻布","value":"#FAF0E6"},{"name":"秘鲁","value":"#CD853F"},{"name":"桃色","value":"#FFDAB9"},{"name":"沙棕色","value":"#F4A460"},{"name":"巧克力","value":"#D2691E"},{"name":"马鞍棕色","value":"#8B4513"},{"name":"海贝壳","value":"#FFF5EE"},{"name":"黄土赭色","value":"#A0522D"},{"name":"浅鲜肉(鲑鱼)色","value":"#FFA07A"},{"name":"珊瑚","value":"#FF7F50"},{"name":"橙红色","value":"#FF4500"},{"name":"深鲜肉(鲑鱼)色","value":"#E9967A"},{"name":"番茄","value":"#FF6347"},{"name":"薄雾玫瑰","value":"#FFE4E1"},{"name":"鲜肉(鲑鱼)色","value":"#FA8072"},{"name":"雪","value":"#FFFAFA"},{"name":"淡珊瑚色","value":"#F08080"},{"name":"玫瑰棕色","value":"#BC8F8F"},{"name":"印度红","value":"#CD5C5C"},{"name":"纯红","value":"#FF0000"},{"name":"棕色","value":"#A52A2A"},{"name":"耐火砖","value":"#B22222"},{"name":"深红色","value":"#8B0000"},{"name":"栗色","value":"#800000"},{"name":"纯白","value":"#FFFFFF"},{"name":"白烟","value":"#F5F5F5"},{"name":"Gainsboro","value":"#DCDCDC"},{"name":"浅灰色","value":"#D3D3D3"},{"name":"银白色","value":"#C0C0C0"},{"name":"深灰色","value":"#A9A9A9"},{"name":"灰色","value":"#808080"},{"name":"暗淡的灰色","value":"#696969"},{"name":"纯黑","value":"#000000"},{"name":"纯黑","value":"#000000"},{"name":"纯黑","value":"#000000"},{"name":"纯黑","value":"#000000"},{"name":"纯黑","value":"#000000"},{"name":"纯黑","value":"#000000"},{"name":"纯黑","value":"#000000"},{"name":"纯黑","value":"#000000"},{"name":"纯黑","value":"#000000"}];
var str="<table border=1 style='width: 100%;font-size:25px;cursor: pointer;'><tr>";
for(var i in colors){
if(i%12==0){
str+="</tr><tr>";
}
str+="<td onclick='setColor(this)' title='"+colors[i].name+"' style='background:"+colors[i].value+"'> </td>";
}
str+="</tr>";
document.body.innerHTML=str;
this.setColor=function(obj){
window.returnValue=obj.style.background;
window.close();
}
</script>
基础的功能已完成,剩下的只是添加功能了。暂时没添加对pdf的支持,可以使用“xpdf-chinese-simplified”库将pdf转换为txt在进行读取,还可以添加对SAPI的支持,使软件具有朗读电子书的功能。