Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537
電腦的組裝在生活中并不陌生,大家都有電腦,當(dāng)然需求不一樣配置也不一樣。以Macbook Pro為例,像UI設(shè)計對圖像模塊GPU要求比較高,跑IDEA的對內(nèi)存要求就比較高,可能會加裝32G內(nèi)存更高的就是64G了。如果是對付日常的辦公,刷劇那默認(rèn)的配置就已經(jīng)足夠了,如8G標(biāo)配。類似的在軟件開發(fā)過程中,需要根據(jù)需要自定義搭配不同的選擇來構(gòu)建對象的一般能夠用Builder模式很好的解釋,看看具體的定義是怎樣的。
維基百科
The builder pattern is a design pattern designed to provide a flexible solution to various object creation problems in object-oriented programming. The intent of the Builder design pattern is to separate the construction of a complex object from its representation. It is one of the Gang of Four design patterns.
通俗地表示:即將一個復(fù)雜對象的構(gòu)建與表示相分離開,同樣的構(gòu)建過程根據(jù)需要可以創(chuàng)建不同的表示。稱之為Builder模式或建造者模式。
根據(jù)需要配置對應(yīng)的文檔系統(tǒng)并導(dǎo)出文件,例如導(dǎo)出純文本內(nèi)容.text、.html等文件。
public interface Element {
//no method
}
1.為提供默認(rèn)的方法,由實(shí)現(xiàn)類自行決定實(shí)現(xiàn)何種細(xì)節(jié)。
/**
* Created by Sai
* on: 25/01/2022 00:48.
* Description:
*/
public class Text implements Element {
private final String content;
public Text(String content) {
this.content=content;
}
public String getContent() {
return content;
}
}
/**
* Created by Sai
* on: 25/01/2022 00:49.
* Description:
*/
public class Image implements Element {
private final String source;
public Image(String source) {
this.source=source;
}
public String getSource() {
return source;
}
}
/**
* Created by Sai
* on: 25/01/2022 00:52.
* Description:
*/
public interface DocumentBuilder {
void addText(Text text);
void addImage(Image image);
String getResult();
}
1.分別提供了兩個添加內(nèi)容的方法,addText、addImage。
2.提供一個返回具體內(nèi)容的方法,getResult。
public class TextDocumentBuilder implements DocumentBuilder {
private final StringBuilder builder=new StringBuilder();
@Override
public void addText(Text text) {
builder.append(text.getContent());
}
@Override
public void addImage(Image image) {
//empty implements
}
@Override
public String getResult() {
return builder.toString();
}
}
1.其中純文本內(nèi)容不支持添加圖片,添加圖片的方法則未實(shí)現(xiàn),為空方法。
public class HtmlElement {
//empty method
}
public class HtmlImage extends HtmlElement {
private final String source;
public HtmlImage(String source) {
this.source=source;
}
@Override
public String toString() {
return String.format("<img src=\"%s\" />", source);
}
}
public class HtmlParagraph extends HtmlElement {
private final String text;
public HtmlParagraph(String text) {
this.text=text;
}
@Override
public String toString() {
return String.format("<p>%s</p>", text);
}
}
public class HtmlDocument {
private final List<HtmlElement> elements=new ArrayList<>();
public void add(HtmlElement element) {
elements.add(element);
}
@Override
public String toString() {
var builder=new StringBuilder();
builder.append("<html>");
for (HtmlElement element : elements)
builder.append(element.toString());
builder.append("</html>");
return builder.toString();
}
}
public class HtmlDocumentBuilder implements DocumentBuilder {
private final HtmlDocument document=new HtmlDocument();
@Override
public void addText(Text text) {
document.add(new HtmlParagraph(text.getContent()));
}
@Override
public void addImage(Image image) {
document.add(new HtmlImage(image.getSource()));
}
@Override
public String getResult() {
return document.toString();
}
}
public class Document {
private final List<Element> elements=new ArrayList<>();
public void add(Element element) {
elements.add(element);
}
public void export(DocumentBuilder builder, String fileName) throws IOException {
for (Element element : elements) {
if (element instanceof Text)
builder.addText((Text) element);
else if (element instanceof Image)
builder.addImage((Image) element);
}
var writer=new FileWriter(fileName);
writer.write(builder.getResult());
writer.close();
}
}
public class Demo {
public static void show() throws IOException {
var document=new Document();
document.add(new Text("\n\n\n\n快樂二狗\nphp才是最好的語言\n"));
document.add(new Image("pic1.jpg"));
document.export(new HtmlDocumentBuilder(), "sai.html");
//文檔不添加圖片
document.export(new TextDocumentBuilder(), "sai.txt");
}
public static void main(String[] args) throws IOException {
show();
}
}
1.簡單的添加了一個文本內(nèi)容與一個“圖片內(nèi)容”,分別組成了純文本的文檔與Html的文檔,但文本的文檔是沒有添加圖片的。
2.導(dǎo)出的文件在同包下,sai.html和sai.text文件。
1.Builder模式很好的將構(gòu)建與表現(xiàn)相分離,客戶端或者Director可以根據(jù)需要靈活的選擇,在同一套構(gòu)建算法下產(chǎn)生不同的產(chǎn)品,使得表現(xiàn)形式與生產(chǎn)的耦合程度降低。
2.具體的構(gòu)建細(xì)節(jié)包含在系統(tǒng)內(nèi)部,客戶端僅僅只需要通過對Builder接口的調(diào)用即可完成創(chuàng)建所需要的對象,降低了系統(tǒng)出錯的風(fēng)險。
3.加強(qiáng)了代碼的復(fù)用性。
4.當(dāng)然缺點(diǎn)也是很明顯的,如果對象太過于復(fù)雜,組裝的配件過多往往不好掌控,造成臃腫,結(jié)構(gòu)也不是很清晰。
5.如果改變產(chǎn)品原有的實(shí)現(xiàn),那么整套流程都需要做出相應(yīng)的調(diào)整,假設(shè)產(chǎn)品本身過于復(fù)雜,那么對于后期的維護(hù)是很不利的。在考慮使用時應(yīng)根據(jù)實(shí)際情況,對于復(fù)雜且變化頻繁的對象并不適合使用。
實(shí)際使用過程中往往簡化了標(biāo)準(zhǔn)化的構(gòu)建流程,當(dāng)然也是根據(jù)具體的業(yè)務(wù)場景,一般會省略了Director指導(dǎo)類,Builder接口以及具體的ConcreteBuilder實(shí)現(xiàn)類,而直接將Builder作為內(nèi)部類實(shí)現(xiàn)在了目標(biāo)產(chǎn)品類之中。根據(jù)調(diào)用者的選擇得到不同的產(chǎn)品,當(dāng)然這種比較單一,可以說是最為簡單的一種。僅僅是一種思想上的應(yīng)用,或者說也是一種“取巧”的做法。
public class FilterGroup {
private int id;
private String title;
private boolean supportMultiSelected;
private List<FilterOrderDTO> filterFactors;
private FilterGroup(Builder builder) {
if (builder==null) {
return;
}
this.id=builder.id;
this.title=builder.title;
this.supportMultiSelected=builder.supportMultiSelected;
this.filterFactors=builder.filterFactors;
}
public int getId() {
return id;
}
public String getTitle() {
return title;
}
public boolean isSupportMultiSelected() {
return supportMultiSelected;
}
public List<FilterOrderDTO> getFilterFactors() {
return filterFactors;
}
public static class Builder {
private int id;
private String title;
private boolean supportMultiSelected;
private List<FilterOrderDTO> filterFactors;
public Builder setId(int id) {
this.id=id;
return this;
}
public Builder setTitle(String title) {
this.title=title;
return this;
}
public Builder setSupportMultiSelected(boolean supportMultiSelected) {
this.supportMultiSelected=supportMultiSelected;
return this;
}
public Builder setFilterFactors(List<FilterOrderDTO> filterFactors) {
this.filterFactors=filterFactors;
return this;
}
public FilterGroup build() {
return new FilterGroup(this);
}
}
}
1.Builder以靜態(tài)內(nèi)部類的形式存在于產(chǎn)品的內(nèi)部,而產(chǎn)品的特征則是:不同的構(gòu)建組成所對應(yīng)的產(chǎn)品不同,表現(xiàn)出來的性質(zhì)也是不同的。其次Builder中只提供了set方法,而沒有get方法。
2.目標(biāo)產(chǎn)品FilterGroup的構(gòu)造方法是私有的,并且以Builder為參數(shù)。另外只提供了get方法而沒有set方法。
3.即對于目標(biāo)的產(chǎn)品的構(gòu)建是通過Builder來完成的,對于用戶它僅僅是“可讀的”。
4.像實(shí)際開發(fā)中,這種分部分構(gòu)建產(chǎn)品與整體相分離的可以考慮使用Builder模式,核心思想即為整體構(gòu)建與部件之間的分離,松散產(chǎn)品構(gòu)建與產(chǎn)品表示之間的耦合。
近工作需要一直在和瀏覽器打交道。每天都為如何解決那些瀏覽器間的兼容性而困擾。時間長了自然而然對瀏覽器也產(chǎn)生了感情。準(zhǔn)備學(xué)習(xí)學(xué)習(xí),自己寫個瀏覽器。為此開始學(xué)習(xí)了 Rust,一門用于寫底層,但看上去又像高級語言的語言。希望 Rust 能有美好的明天,我也跟著受益。
想了想,要寫瀏覽器,首先應(yīng)該了解一下瀏覽器內(nèi)部機(jī)制。今天先放下代碼,帶大家一起走進(jìn)瀏覽器,看看瀏覽器是如何將網(wǎng)頁呈現(xiàn)給您的。
綁定:使用系統(tǒng)級別的 API,將內(nèi)存中位圖繪制到指定窗口(標(biāo)簽對應(yīng)的網(wǎng)頁視圖)上。
渲染:解析 html 和 css 生成渲染樹,將合并后,將渲染樹繪制到屏幕上呈現(xiàn)給用戶。
平臺:兼容(適配)到不同的操作系統(tǒng)
javascript VM :以后單講,準(zhǔn)備寫個demo
首先將 HTML 和 CSS 解析為一定的數(shù)據(jù)結(jié)構(gòu)(渲染對象),然后再將渲染對象按一定規(guī)則(就是將 style 樹 合并到 dom 樹上)形成渲染樹,接下來對生成渲染樹各個節(jié)點(diǎn)進(jìn)行布局(也就是按 dom 節(jié)點(diǎn)的位置信息進(jìn)行排版),最后讀取渲染樹,繪制成圖片放到屏幕上。
HTML 的解析
首先瀏覽器是以超強(qiáng)糾錯形式來解析 html,即便 html 有錯誤,瀏覽器也相對智能地將 html 進(jìn)行解析,所以說對 html 的解析不是一般簡單解析工作,html 解析要相對復(fù)雜。在解析過程是可以被 js 或其他原因所中斷的。例如網(wǎng)絡(luò)不暢通,link 和 style 標(biāo)簽加載,相對高級的瀏覽器為提高效率,提供一定進(jìn)程進(jìn)行預(yù)解析,也可以加載圖這樣耗時的工作可以另一個進(jìn)程中完成
Parser 和 Tokenizer 其實(shí)只是把無意義的字符流變得有某種意義而已。Parse 這個詞其實(shí)可以用在很多的地方,比如說只要你能在一個字符流中標(biāo)識出所有的字符 a,你就在做 Tokenize 和 Parse。你可以看得出,Parse 和 Tokenize 有多難實(shí)際是針對編程的人的目的來說的。
一般解析完了這種形式
html
|-----head
-----body
|--- p. wat
| #text
---- div
---- span
---- #text
HTMLHtmlElement
|-----HTMLHeadElement
-----HTMLBodyElement
|--- HTMLParagraphElement
| ----Text
---- HTMLDivElement
---- HTMLSpanElement
---- Text
下面例子只為說明瀏覽器解析 html 時的糾錯能力,html 中錯誤顯而易見,我就不一一指出了。
javascript 是可以介入 html 解析過程中,如下圖。
隨著產(chǎn)品不斷迭代,閱讀器作為一個占據(jù)用戶99%使用時長的模塊,承載了愈加復(fù)雜的業(yè)務(wù)功能。開發(fā)一個能供人看書的閱讀軟件并不困難,但是如何打造一個高可用的閱讀器卻是門道頗深。 本篇文章結(jié)合本人閱讀器新架構(gòu)實(shí)操經(jīng)驗(yàn),為大家闡述開發(fā)設(shè)計中的諸多細(xì)節(jié),希望能對大家有所幫助。
所謂排版,即是在固定頁面內(nèi),將內(nèi)容以適合的方式層現(xiàn)。
對于客戶端來講,文本顯示到屏幕上的過程,大體可以分為解析->排版->渲染三個步驟,其中解析是多格式以及跨平臺的基礎(chǔ),排版重在方案與設(shè)計,部分API需要使用平臺特性,而渲染過程則是主要依賴原生平臺提供的API方法。
本文主要介紹文本排版過程中一些設(shè)計特性與策略抉擇,以幫助讀者建立對排版工作的基礎(chǔ)認(rèn)知。
(以下內(nèi)容適用于iOS及安卓雙端,本文僅以iOS舉例闡述,盡量忽略平臺及語言特性,有什么寫得不清楚的地方請多多包涵。)
我們先看一下字體UIFont排版相關(guān)的基本屬性:
// Font attributes
open var familyName: String { get } //字體家族的名字
open var fontName: String { get } //字體名稱
open var pointSize: CGFloat { get } //字體大小
open var ascender: CGFloat { get } //升部,基準(zhǔn)線以上的高度
open var descender: CGFloat { get } //降部,基準(zhǔn)線以下的高度(獲取時通常為負(fù)數(shù))
open var capHeight: CGFloat { get } //大寫字母的高度
open var xHeight: CGFloat { get } //小寫x的高度
open var lineHeight: CGFloat { get } //當(dāng)前字體下的文本行高
open var leading: CGFloat { get } //行間距
相信iOS童鞋對這張字形圖應(yīng)該很熟悉了,從字形圖中我們可以獲知:
純字符高度計算公式為: pointSize=ascender + | descender |
文本行高計算公式為: lineHeight=ascender + | descender | + leading
其中leading為行間距,在單行時為0,在兩行及以上為正值
在排版的時候?yàn)榱嗣烙^考慮,我們需要另行添加額外的行間距lineSpace以及段落間距paragraphSpace
對于同一種字體,如果一段文字有多行(row),高度如何計算?
singleParagraphHeight=lineHeight * row + lineSpace *(row-1)
如果有兩段文字,總高度又如何計算?
doubleParagraphHeight=singleParagraphHeight1 + paragraphSpace + singleParagraphHeight2
多段(paragraph)依此類推:
mutileParagraphHeight=(singleParagraphHeight1 + singleParagraphHeight2 + ...) + paragraphSpace * (paragraph - 1)
當(dāng)然,最后一段的段間距也是可以加到總高度中的,但必須在排版的時候明確此特性,如果最后一段后有其他附加內(nèi)容,需要另行調(diào)節(jié)。
上面列出的是比較理想的排版情況,實(shí)際上,行數(shù)row是排版計算完成后的一個結(jié)果,與設(shè)備顯示/繪制寬度以及字體、字號大小有關(guān),事先是無法確定的。
明白了文字高度的計算方法,我們就可以定義統(tǒng)一的文本行信息數(shù)據(jù)結(jié)構(gòu):
class TextLineInfo {
... // 其他文字元素相關(guān)的信息已省略
var leftIndent: Int=0 // 該行文字的向左縮進(jìn)寬度
var width: CGFloat=0 // 該行文字的寬度
var height: CGFloat=0 // 該行文字的高度(已包含行距)
var vSpaceBefore: CGFloat=0 // 段落首行與上段文字額外間距,一般為0,會與vSpaceAfter共同作用于高度計算
var vSpaceAfter: CGFloat=0 // 段間距:段落末行與下段文字額外間距,非最后一行時此數(shù)值為0
}
那么每一行的數(shù)據(jù)信息又從哪里來?在這里我先簡單介紹一下數(shù)據(jù)獲取的的方式,在事先需要依賴換行標(biāo)識符將所有文字按段落拆分,然后在需要的時候填充每一段的數(shù)據(jù)信息。
一個字符所在文本位置可以用3個參數(shù)表示:
var paragraphIndex: Int=0 // 段落索引,文本第幾段
var elementIndex: Int=0 // 詞索引,本段第幾個字
var charIndex: Int=0 // 字母索引:本單詞中第幾個字母
其中字母索引默認(rèn)為0,在中文中此值不會變化,只有在英文單詞中才有意義。
如果一個文本文件比較大,通常我們需要對起進(jìn)行分章或是分節(jié),以優(yōu)化文本讀取及顯示性能。結(jié)合以上參數(shù),加上章節(jié)序號ord,由此我們定位到了任意文本的具體位置坐標(biāo)(ord, paragraphIndex, elementIndex, charIndex)。
每一段的數(shù)據(jù)信息依靠段落游標(biāo)來進(jìn)行管理,通過以下數(shù)據(jù)結(jié)構(gòu)可以靈活的填充數(shù)據(jù)以及獲取指定元素:
protocol ParagraphCursorDatasource: NSObjectProtocol {
func getParagraphCursor(_ index:Int) -> TextParagraphCursor? // 獲取指定段落數(shù)據(jù)模型
func getTextModel() -> TextModel // 獲取文本數(shù)據(jù)模型
}
class TextParagraphCursor {
weak var delegate: ParagraphCursorDatasource?
/// 段落序號:標(biāo)明是第幾段
private(set) var index: Int=0
/// 存儲每個段落的元素
private(set) var myElements=[TextElement]()
/// 填充元素,核心方法
func fill() {
// 為myElements填充元素...
}
/// 移除所有元素
func clear() {
myElements.removeAll()
}
/// 是否為第一段段落
func isFirst() -> Bool {
return index==0
}
/// 是否為最后一個段落
func isLast() -> Bool {
guard let model=delegate?.getTextModel() else { return false }
return index + 1 >=model.getParagraphsNumber()
}
/// 獲取當(dāng)前段落的元素個數(shù)
func getParagraphLength() -> Int {
return myElements.count
}
/// 獲取前一個段落的游標(biāo)
func previous() -> TextParagraphCursor? {
return isFirst() ? nil : delegate?.getParagraphCursor(index - 1)
}
/// 獲取下一個段落的游標(biāo)
func next() -> TextParagraphCursor? {
return isLast() ? nil : delegate?.getParagraphCursor(index + 1)
}
/// 獲取當(dāng)前段落的第幾個元素
func getElement(_ index: Int) -> TextElement? {
if index > (myElements.count - 1) {
return nil
}
return myElements[index]
}
}
對于任意文本文件,在解析其編碼格式后,我們可以獲知其內(nèi)容信息,目前市面上最通用的就是geometer大神的FBReader(FBReader有多厲害我就不多做贅述了)的解析方案,這也是眾多主流閱讀類產(chǎn)品早期的參考方案,其底層是C++書寫的所以支持跨平臺,可以將多種格式的數(shù)據(jù)(如txt、epub、mobi、html、doc、fb2等)統(tǒng)一轉(zhuǎn)換成同一種數(shù)據(jù)模型,開發(fā)者可以完全不依賴其上層代碼進(jìn)行二次開發(fā)。
文字排版的前提是要知道每一個字符(漢字、字母、數(shù)字、符號等)元素的寬高信息,如此才能決定最終的排版情況。
首先我們要知道,計算字符寬高是一個相對耗時的操作,如果每個字符都需要計算,那么一頁顯示文字越多,則計算耗時也就越長。
為了優(yōu)化此場景,我們經(jīng)過測驗(yàn),同一字體和字號下的漢字字符,它們的寬度與高度是一致的;但是對于英文字母、數(shù)字以及符號,全角及半角下的寬度是不一致的。
基于以上結(jié)論,我們可以根據(jù)Unicode編碼判斷文字是否是中文字符,如果是,只需算出相同字體下其中一個漢字的寬高度并緩存即可;而其他元素,我們可以維護(hù)一個緩存池,將寬高緩存起來,在下次命中緩存時取出寬高即可減少重復(fù)計算。
class PaintContext {
/// 存儲富文本字體字號、文字顏色等信息
private var attributes=[NSAttributedString.Key : Any]()
/// chinese character width cache
private var ccwCache=[CGFloat:CGFloat]()
/// other character width cache
private var ocwCache=[String:CGFloat]()
func getStringWidth(_ subString: String) -> CGFloat {
if subString.count==1, let pointSize=(attributes[.font] as? UIFont)?.pointSize {
if "\u{4E00}" <=subString && subString <="\u{9FA5}" {
if let cache=ccwCache[pointSize] {
return cache
}
let size=(subString as NSString).size(withAttributes: attributes)
ccwCache[pointSize]=size.width
return size.width
} else {
// 防止同一頁有多個不同字號的相同字符串,拼接字號大小作為鍵值
let cacheKey="\(subString)_\(pointSize)"
if let cache=ocwCache[cacheKey] {
return cache
}
let size=(subString as NSString).size(withAttributes: attributes)
ocwCache[cacheKey]=size.width
return size.width
}
}
let size=(subString as NSString).size(withAttributes: attributes)
return size.width
}
}
以上并不是最完美的方案,但對于項(xiàng)目的優(yōu)化已經(jīng)非常明顯了,有興趣的小伙伴可以進(jìn)一步優(yōu)化上述判斷減少寬高計算頻次,或者有其他更好的方案也歡迎多多指教哦~
大部分語言文本布局都是從左往右從上到下排列的,在文字排列的過程中,左端文字的起始位置固定,由于符號寬度不定導(dǎo)致每一行的文字?jǐn)?shù)量不盡相等,所以會出現(xiàn)右端不對齊的情況。
出于美觀考慮,我們希望每一行文字都右側(cè)對齊,對此我們的做法是在每一行排版完成后,將最右端字符到右端繪制區(qū)域的距離均勻分配到字符間距中,這樣就不會顯得很突兀了。
先看一張沒有調(diào)整水平間距時的顯示圖,由于標(biāo)點(diǎn)符號不能在一行開頭的排版規(guī)則,雖然第一行剩余的空間足夠再放下一個字符,但“然”及其后的標(biāo)點(diǎn)符號在只能放到第二行顯示。
為了保持兩端對齊,在下圖中第一行人為增加了字符之間的間距,而第二行由于是段落的最后一行則無需增加字間距。(由于使用的是模擬器截圖所以引號看起來是半角的,真機(jī)是全角字符所占寬度會更大一些)
計算過程如下:
動態(tài)調(diào)整字間距的時機(jī)在單行信息計算完成之后、存儲單行字符位置信息之前,在渲染時直接根據(jù)存儲的排版數(shù)據(jù)進(jìn)行繪制,所以它不會影響其前后一行的排版結(jié)果。
在實(shí)際顯示過程中,每一頁首行文字的縱坐標(biāo)是固定的,當(dāng)文字行、段間距高度不相等時,就會導(dǎo)致底部剩余高度不對齊,如下圖所示:
為了優(yōu)化顯示效果,實(shí)現(xiàn)了動態(tài)調(diào)整行、段間距的方案(很遺憾當(dāng)前由于業(yè)務(wù)因素此特性已被移除),以保證最后一行文字到底部的距離為固定值。
同動態(tài)調(diào)整字間距方案一樣,動態(tài)調(diào)整行、段間距方案并不會影響當(dāng)前頁展示的總行數(shù)。
Question
有朋友問了,以上示例是左右翻頁模式下的排版情況,換成上下滑動翻頁方式是怎么處理的?
這個問題的秘密就在距離底部的固定距離上。當(dāng)最后一行文字非段落最后一行時,它等于行間距高度;反之則等于段間距高度;但假如是文末/章末則為0,表示無需調(diào)整。
這一排版特性不關(guān)心外界使用的到底是哪種翻頁方式,保證頁與頁之間銜接自然、均衡。
值得一提的是,上下排布也是有頁的概念的,每一頁都以圖片方式添加到了一個可復(fù)用的視圖上,還可以根據(jù)實(shí)際需要對其裁剪,以保證視圖的連貫性及滑動性能。
4.4 版面灰度分布均勻
雖然我們已經(jīng)有了動態(tài)調(diào)整字距、行距、段距的方案,但是對于整體排版仍然還有很多細(xì)節(jié)可以優(yōu)化。
上圖來源于李澤磊《我在百度做閱讀器》的主題分享,通過擠壓全角字符寬度的方式優(yōu)化文字展示效果,很值得我們團(tuán)隊(duì)探索及學(xué)習(xí)。
還有一些更復(fù)雜的場景,比如超大字體下的英文排版,如果遇到某些過長的英文單詞,會導(dǎo)致頁面分布比較離散。
遵循均勻分布原則,我們可以在長單詞內(nèi)添加連字符(中劃線-)將其拆分,使其跨行顯示達(dá)成目的。
所謂全排版,其實(shí)就是當(dāng)獲取到一章數(shù)據(jù)(一本書需要先按章節(jié)分割)后,直接對其全部內(nèi)容從前往后進(jìn)行排版計算。與Word軟件排版方式相同,當(dāng)前頁內(nèi)容放不下的時候會自動添加下一頁,直到所有文字均顯示后我們就知道了總頁數(shù)以及展示所有內(nèi)容需要的總高度(用于計算最后一行文字到章末的剩余距離),然后緩存每一頁的展示信息,接下來只需要再記錄當(dāng)前處于第幾頁(相對首頁)即可。
大部分通用的閱讀器用的即是此方案,由于提前將分頁區(qū)間數(shù)據(jù)算出并存儲了,只需操作頁碼計數(shù)變化,再利用獲取到的文本數(shù)據(jù)調(diào)用繪制方法生成Bitmap位圖即可,此過程基本不會出現(xiàn)明顯卡頓問題。
雖然使用全排版計算非常方便,但是此方案卻存在一些問題:
動態(tài)排版實(shí)際上是根據(jù)任意字符所在位置作為起始坐標(biāo)(即起始游標(biāo)StartCursor),逐字排版直到繪制完本頁最后一個字為止。它的動態(tài)特性在于這一頁能繪制多少內(nèi)容不是固定的,需要粗排版(不進(jìn)行對齊修正)一次才能決定當(dāng)前頁結(jié)束的位置坐標(biāo)(又稱終止游標(biāo)EndCursor),如果要繪制下一頁,則需要以當(dāng)前頁的終止游標(biāo)作為下一頁起始游標(biāo)往后推算,依此類推直至文本結(jié)束;反之如果是要繪制上一頁,則以當(dāng)前頁的起始游標(biāo)作為上一頁的終止游標(biāo),倒著排直到放不下某一行文字時,以其下一行行首文字作為起始游標(biāo),標(biāo)記檢索完成,依此類推直至文本開始。
剛才全排版列了這么多問題,那么動態(tài)排版是如何解決上述問題的呢?
動態(tài)排版需要結(jié)合緩存策略使用,每次單頁計算完成成緩存頁信息,以節(jié)省用戶來回翻頁的性能開銷。
由于動態(tài)排版無需計算出所有內(nèi)容的排版結(jié)果,所以初始時是不知道這一章總共有多少頁及當(dāng)前處于全部內(nèi)容的第幾頁的,只有開啟異步任務(wù)遞歸算出當(dāng)前頁之前以及之后所有的內(nèi)容才可確定其頁碼位置。
為了提升翻頁速度及滑動流暢性,需要在當(dāng)前頁文字計算完成后,附加計算其前后一頁內(nèi)容排版情況,此為預(yù)加載過程。
其中當(dāng)前若是本章第一頁則需要預(yù)加載上一章最后一頁,如果是最后一頁則還要額外預(yù)加載下一章首頁以優(yōu)化切章速度,實(shí)際開發(fā)過程中需要充分利用LRU算法緩存最近看過的內(nèi)容以減少重復(fù)計算量,并在異步線程中完成此計算過程。
如果是在短時間內(nèi)快速滑動翻頁呢?更進(jìn)一步的優(yōu)化是及時cancel掉不需要顯示在屏幕上的任務(wù),方案可以參考YYAsyncLayer異步繪制。
不少用戶反饋了一個問題,為什么翻著翻著會發(fā)現(xiàn)有內(nèi)容重復(fù)?
其實(shí)這是因?yàn)?/span>前翻算法的局限性,觸發(fā)了補(bǔ)字邏輯。
我們團(tuán)隊(duì)在實(shí)際開發(fā)中發(fā)現(xiàn),當(dāng)設(shè)置了段間距時,就會出現(xiàn)前后翻頁數(shù)據(jù)不一致的情況。當(dāng)段間距越大于標(biāo)準(zhǔn)行間距時,偏差會更加明顯,這來源于我們排版算法層面的問題。
在正向(后翻)排版計算過程中,我們采用的是最大化使用剩余空間的策略,即如果最后剩余距離小于文字高度加段間距時,會去除段間距保證文字排列上去;然而在逆向(前翻)排版計算過程時,由于是倒著算就優(yōu)先去除了文字的段間距,就可能會出現(xiàn)最上一行文字放不下的情況,即比正向計算少一行,再繼續(xù)前翻則可能使此誤差放大,累積下來直到第一頁(第一頁一定是正向排版),導(dǎo)致了第一頁與第二頁內(nèi)容間的重復(fù)。
左右翻頁模式下,當(dāng)前一頁內(nèi)容無法占滿所有繪制空間時,為了排版的美觀性,我們會將下一頁的部分文字補(bǔ)齊到前一頁中占位,這樣就出現(xiàn)了文字重復(fù)現(xiàn)象。舉個:
某一章節(jié)第一頁第二頁展示結(jié)果如下:
我們前往第二頁,調(diào)整減小字號,首字位置不發(fā)生改變并重新排版,此時第一頁第二頁的結(jié)果如下:
由于第一頁的文字縮小后會空出一部分區(qū)域,所以將第二頁的文字往第一頁末尾補(bǔ)齊直到放不下下一行,而多出來的這些字就會與原第二頁的文字重復(fù)。
去重
觸發(fā)補(bǔ)字的情況下,我們其中一個解決思路是做去重處理。具體方案是比對第一頁與第二頁的內(nèi)容,如果有交叉,則先將重復(fù)的文字去除,然后調(diào)整文字行間距與段間距,以保證最后一行文字到底部的距離固定。
去重方案仍然具有局限性。當(dāng)重復(fù)內(nèi)容只有一兩行時,動態(tài)調(diào)整間距可能看不出來,效果也比較美觀;但是如果碰到極端情況,比如這一頁只有幾行的情況,間距可能就會非常大,那么去重就不合適了。
重排版主要用于左右翻頁方式。當(dāng)往前翻頁時,排版完成后發(fā)現(xiàn)與下一頁數(shù)據(jù)有重復(fù),則可以重新進(jìn)行排版。即刪除當(dāng)前頁之后所有的緩存結(jié)果,重新執(zhí)行預(yù)加載邏輯,這樣后一頁一定是以當(dāng)前頁往后排版生成的,就不會出現(xiàn)重復(fù)內(nèi)容了。
由于重排版會清除后續(xù)緩存,所以會造成一定的資源浪費(fèi)。
裁剪只能用于上下翻頁方式。在左右翻頁模式下,每一頁文字的最大顯示區(qū)域是相同的,但是在上下翻頁模式下,我們可以任意指定每一頁的最大高度。為了保持統(tǒng)一,我們設(shè)置所有翻頁模式下的單頁繪制高度為一個固定值。當(dāng)回翻的時候檢測到重復(fù)內(nèi)容,我們就可以將重復(fù)的行刪除,剪掉這部分繪制區(qū)域,這樣也可以達(dá)成目的。
前翻補(bǔ)字導(dǎo)致內(nèi)容重復(fù)問題我們團(tuán)隊(duì)也一直在探索更好的解決方案,僅靠前翻算法層面無法解決此問題,所以需要結(jié)合以上額外的特性做調(diào)整。
對于左右翻頁方式我們可以依靠查重+重排版的方式,一旦發(fā)現(xiàn)重復(fù)內(nèi)容,就可清除后置頁面的所有內(nèi)容緩存,使其重新開始計算;
而對于上下翻頁方式可以依靠去重+裁剪的方式過濾重復(fù)內(nèi)容,也就是說每一頁的高度不是固定值,以實(shí)際展示內(nèi)容需要的高度為基準(zhǔn)布局。
在終端場景,正??磿^程并不會出現(xiàn)頻繁切換閱讀設(shè)置及回翻的行為,對于方案取舍需要靈活把握。
無限制的濫用緩存并不符合我們的預(yù)期。我們雖然可以將所有正翻計算過的信息頁緩存,在不改變字體設(shè)置信息的情況下,每次翻頁嘗試復(fù)用緩存的結(jié)果,但缺點(diǎn)是此方案會增加一定的內(nèi)存開銷,在章節(jié)大小不明或者內(nèi)容經(jīng)常需要變化時會產(chǎn)生額外開銷。
當(dāng)前我們團(tuán)隊(duì)使用的是LRUCache緩存前后相鄰多頁的方式,大小固定,在后續(xù)需要調(diào)整更佳的方案,比如可以根據(jù)章節(jié)大小或字?jǐn)?shù)區(qū)分長章節(jié)與短章節(jié),動態(tài)調(diào)整緩存區(qū)大小,以維持閱讀體驗(yàn)與性能的平衡。
以上介紹了文本排版過程中的一些技巧與考量,但是在電子書排版過程中我們將面臨更多的挑戰(zhàn),比如在文稿排版中常常需要加入圖片甚至是音視頻,需要支持超鏈接跳轉(zhuǎn)等,由于業(yè)務(wù)需要可能還要在內(nèi)容之間插入廣告、評論視圖等,這都會對我們排版的結(jié)果造成影響。
對此,我們需要在設(shè)計上將這些都包含進(jìn)去,將所有內(nèi)容都當(dāng)成一個個子元素是最佳的抽象設(shè)計方法。
以下是基于FBReader設(shè)計閱讀器文本元素設(shè)計類圖:
在解析過程,我們需要對源文件進(jìn)行一次預(yù)處理,在序列化過程中為內(nèi)容插入特定的標(biāo)簽,這樣在反序列化時我們就可以根據(jù)標(biāo)簽信息將之后內(nèi)容交由對應(yīng)的處理類來處理。
目前的設(shè)計上主要定義了文本元素、文本樣式、控制符、圖片元素類型,額外提供了音視頻元素類型的擴(kuò)展,此外還提供了支持業(yè)務(wù)擴(kuò)展的元素類型(目前可用于“神段評、作者說”排版),保證了底層結(jié)構(gòu)的穩(wěn)定性。
相信讀完本文,你對閱讀器排版已經(jīng)有了一定的了解,我們會繼續(xù)將更多有關(guān)閱讀器的知識整理成文,敬請關(guān)注。
有興趣的童鞋還可以去看一下李哥的《我在百度做閱讀器》主題分享,基于CoreText框架衍生的設(shè)計也是干貨多多。
作者:彭章錕
出處:https://tech.qimao.com/reader/
*請認(rèn)真填寫需求信息,我們會在24小時內(nèi)與您取得聯(lián)系。