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 欧美激情黄色,亚洲国产精品67194成人,久久国产精品二国产精品

          整合營銷服務(wù)商

          電腦端+手機(jī)端+微信端=數(shù)據(jù)同步管理

          免費(fèi)咨詢熱線:

          「設(shè)計模式(六) - Builder模式」

          「設(shè)計模式(六) - Builder模式」

          、可定制化的

          電腦的組裝在生活中并不陌生,大家都有電腦,當(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模式很好的解釋,看看具體的定義是怎樣的。

          二、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模式建造者模式。

          三、結(jié)構(gòu)組成

          • 結(jié)構(gòu)類圖:

          • 構(gòu)造器接口Builder:通常會包含構(gòu)建部件模塊的抽象方法,以及對象創(chuàng)建完成后的結(jié)果方法getResult()
          • 構(gòu)造器具體實(shí)現(xiàn)類ConcreteBuilder:對Builder接口的具體實(shí)現(xiàn),構(gòu)建產(chǎn)品Product各個部件,組裝成完整的產(chǎn)品對象,并通過getResult()返回具體的對象。
          • 產(chǎn)品的抽象接口Product:需要構(gòu)造的對象,一般包含多個組成部分,構(gòu)成較為復(fù)雜的對象。
          • 定制者(指揮者)Director:決定了Product的具體創(chuàng)建組成,例如包含何種組件。指導(dǎo)出Product復(fù)雜對象的創(chuàng)建,類圖表示較為清晰;一般使用Builder來完成創(chuàng)建過程。

          四、代碼實(shí)現(xiàn)

          1.設(shè)計一個簡單的文件導(dǎo)出系統(tǒng)

          根據(jù)需要配置對應(yīng)的文檔系統(tǒng)并導(dǎo)出文件,例如導(dǎo)出純文本內(nèi)容.text、.html等文件。

          • 元素Element對應(yīng)這里的Product,即產(chǎn)品的接口
          public interface Element {
          //no method
          }

          1.為提供默認(rèn)的方法,由實(shí)現(xiàn)類自行決定實(shí)現(xiàn)何種細(xì)節(jié)。

          • 元素Element具體實(shí)現(xiàn)類-文本
          /**
          * 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;
          }
          }
          • 元素Element具體實(shí)現(xiàn)類-圖片
          /**
          * 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;
          }
          }
          • 建造器接口Builder,這里既然是文檔類型的建造器,命名規(guī)范一點(diǎn)的話則為DocumentBuilder,下同:
          /**
          * 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。

          • 建造器的具體實(shí)現(xiàn)-純文本文檔建造器
          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),為空方法。

          • 由于html結(jié)構(gòu)相對于純文本來說較為復(fù)雜,包括標(biāo)簽,段落等等,如果完全依賴于Element不利于擴(kuò)展,實(shí)現(xiàn)起來比較麻煩,因此對于html類型的文檔文件,單獨(dú)實(shí)現(xiàn)自己的一套元素組件。以Element為媒介動態(tài)添加進(jìn)去。
          • 單獨(dú)實(shí)現(xiàn)html特有的元素HtmlElement
          public class HtmlElement {
          //empty method
          }
          • Html圖片元素組件HtmlImage繼承自HtmlElement
          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);
          }
          }
          • Html段落文本組件HtmlParagraph同樣繼承自HtmlElement
          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);
          }
          }
          • 一份完整的Html文檔可能包含多種HtmlParagraphHtmlImage的集合,定義html文檔文件類-HtmlDocument
          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();
          }
          }
          • 對于html建造器的實(shí)現(xiàn):由于html文檔的特殊性,雖然依據(jù)類圖的實(shí)現(xiàn)流程需要實(shí)現(xiàn)DocumentBuilder接口,顯然僅僅DocumentBuilder中方法并不能很好的滿足需求的定制,但是搭配重新定義一套html文檔組裝規(guī)則(HtmlDocument、HtmlElement、HtmlImage、HtmlParagraph)則能夠很好的完成擴(kuò)展:
          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();
          }
          }
          • 完整的文檔導(dǎo)出類-Document
          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();
          }
          }
          • 測試Demo
          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.htmlsai.text文件。

          • “純文本文檔”內(nèi)容

          • “html”文檔

          2.優(yōu)缺點(diǎn)與局限性思考

          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í)際應(yīng)用中的變形

          實(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)用,或者說也是一種“取巧”的做法。

          • 自身項(xià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é),希望能對大家有所幫助。

          2、什么是排版?

          所謂排版,即是在固定頁面內(nèi),將內(nèi)容以適合的方式層現(xiàn)
          對于客戶端來講,文本顯示到屏幕上的過程,大體可以分為
          解析->排版->渲染三個步驟,其中解析是多格式以及跨平臺的基礎(chǔ),排版重在方案與設(shè)計,部分API需要使用平臺特性,而渲染過程則是主要依賴原生平臺提供的API方法。
          本文主要介紹文本排版過程中一些設(shè)計特性與策略抉擇,以幫助讀者建立對排版工作的基礎(chǔ)認(rèn)知。
          (以下內(nèi)容適用于iOS及安卓雙端,本文僅以iOS舉例闡述,盡量忽略平臺及語言特性,有什么寫得不清楚的地方請多多包涵。)

          3、文本布局Text Layout基礎(chǔ)概念

          我們先看一下字體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,在兩行及以上為正值

          3.1 高度計算

          在排版的時候?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.2 文本定位

          一個字符所在文本位置可以用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)。

          3.3 段落管理

          每一段的數(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ā)。

          4、排版特性

          4.1 計算單個字符寬高信息

          文字排版的前提是要知道每一個字符(漢字、字母、數(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)化上述判斷減少寬高計算頻次,或者有其他更好的方案也歡迎多多指教哦~

          4.2 動態(tài)調(diào)整單行文字水平間距

          大部分語言文本布局都是從左往右從上到下排列的,在文字排列的過程中,左端文字的起始位置固定,由于符號寬度不定導(dǎo)致每一行的文字?jǐn)?shù)量不盡相等,所以會出現(xiàn)右端不對齊的情況。
          出于美觀考慮,我們希望每一行文字都右側(cè)對齊,對此我們的做法是在每一行排版完成后,將最右端字符到右端繪制區(qū)域的距離均勻分配到字符間距中,這樣就不會顯得很突兀了。
          先看一張沒有調(diào)整水平間距時的顯示圖,由于
          標(biāo)點(diǎn)符號不能在一行開頭的排版規(guī)則,雖然第一行剩余的空間足夠再放下一個字符,但“然”及其后的標(biāo)點(diǎn)符號在只能放到第二行顯示。


          為了保持兩端對齊,在下圖中第一行人為增加了字符之間的間距,而第二行由于是段落的最后一行則無需增加字間距。(由于使用的是模擬器截圖所以引號看起來是半角的,真機(jī)是全角字符所占寬度會更大一些)


          計算過程如下:

          1. 獲取實(shí)際顯示寬度realWidth,初始值為屏幕寬度減去左右邊距 realWidth=screenWidth - leftMargin - rightMargin
          2. 獲取第一個中文全角字符寬度fullWidth,第一個半角字符寬度halfWidth
          3. 如果是第一行文字,減去首行縮進(jìn) realWidth -=2 * fullWidth
          4. 扣除“青...當(dāng)”12個字符(其中11個全角字符1個半角字符)的總寬度 realWidth -=fullWidth * 11 + halfWidth
          5. 單個字符字間距 wordSpace=realWidth / 11

          動態(tài)調(diào)整字間距的時機(jī)在單行信息計算完成之后、存儲單行字符位置信息之前,在渲染時直接根據(jù)存儲的排版數(shù)據(jù)進(jìn)行繪制,所以它不會影響其前后一行的排版結(jié)果。

          4.3 動態(tài)調(diào)整單頁多行文字行、段間距

          在實(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á)成目的。

          5、排版策略:全排版 vs 動態(tài)排版

          全排版

          所謂全排版,其實(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)明顯卡頓問題。
          雖然使用全排版計算非常方便,但是此方案卻存在一些問題:

          1. 如果我們需要調(diào)整字體或間距等樣式信息,就需要重新排版計算,一般當(dāng)前頁的內(nèi)容都會發(fā)生改變,以改變前的第一個字定位(相距首字偏移量),需要在分頁后重新計算此文字處于第幾頁然后跳轉(zhuǎn)到這一頁,在調(diào)整前后內(nèi)容會出現(xiàn)不確定性的變化,無法在第一時間找到之前在讀的位置。同類產(chǎn)品中微信讀書、小說就是用的此方案,如下圖所示,每次調(diào)整字體后內(nèi)容將會發(fā)生顯著變化:
          2. 如果章節(jié)內(nèi)容比較長,就需要先花費(fèi)不定的時間計算出全部的排版結(jié)果,打開閱讀器及跳章就會變慢;
          3. 頁面內(nèi)容是固定的,且缺乏更精密的元素信息,假如我們需要在內(nèi)容視圖中實(shí)時插入其他內(nèi)容,如“神段評”或者是“文字環(huán)繞廣告”,就需要提前計算位置并預(yù)留出占位空間,擴(kuò)展性很差。
            綜上,全排版無法應(yīng)對復(fù)雜的業(yè)務(wù)需求,故在重構(gòu)時我們選用了更靈活的方案,即為
            文字動態(tài)排版方案。

          動態(tài)排版

          動態(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)排版是如何解決上述問題的呢?

          1. 動態(tài)排版保持當(dāng)前頁的起始游標(biāo)(即第一個字符)不變,當(dāng)字符字體、字號或間距等發(fā)生改變時,重新排版計算結(jié)束游標(biāo),如下圖所示,切換字體“只”字所在的位置并不會發(fā)生改變;
          2. 不需要提前將所有排版結(jié)果都計算出來,只需要根據(jù)上次記錄的起始游標(biāo)位置直接排版即可,自然會節(jié)省消耗;(可以對相鄰頁做異步預(yù)加載,但無需等待排版完成)
          3. 只需要在頁面展示時判斷是否需要插入對應(yīng)內(nèi)容即可。(如果有做頁面緩存,情況會更復(fù)雜一些,在頁面切換的時候需要確定緩存是否需要更新)

          動態(tài)排版需要結(jié)合緩存策略使用,每次單頁計算完成成緩存頁信息,以節(jié)省用戶來回翻頁的性能開銷。
          由于動態(tài)排版無需計算出所有內(nèi)容的排版結(jié)果,所以初始時是不知道這一章總共有多少頁及當(dāng)前處于全部內(nèi)容的第幾頁的,只有開啟異步任務(wù)遞歸算出當(dāng)前頁之前以及之后所有的內(nèi)容才可確定其頁碼位置。

          預(yù)加載

          為了提升翻頁速度及滑動流暢性,需要在當(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異步繪制。

          內(nèi)容重復(fù)是如何產(chǎn)生的?

          不少用戶反饋了一個問題,為什么翻著翻著會發(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ù)。

          補(bǔ)字邏輯

          左右翻頁模式下,當(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)布局。

          未來規(guī)劃與設(shè)計思路

          在終端場景,正??磿^程并不會出現(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)與性能的平衡。

          6、復(fù)雜排版設(shè)計

          以上介紹了文本排版過程中的一些技巧與考量,但是在電子書排版過程中我們將面臨更多的挑戰(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)定性。

          7、結(jié)語

          相信讀完本文,你對閱讀器排版已經(jīng)有了一定的了解,我們會繼續(xù)將更多有關(guān)閱讀器的知識整理成文,敬請關(guān)注。
          有興趣的童鞋還可以去看一下李哥的《我在百度做閱讀器》主題分享,基于CoreText框架衍生的設(shè)計也是干貨多多。

          作者:彭章錕

          出處:https://tech.qimao.com/reader/


          主站蜘蛛池模板: 日韩色视频一区二区三区亚洲| 国产麻豆媒一区一区二区三区| 国产乱人伦精品一区二区在线观看| 福利在线一区二区| 亚洲一区二区三区免费观看| 国产免费一区二区三区VR| 亚洲视频一区调教| 一级毛片完整版免费播放一区 | 亚洲AV无码一区二区一二区| 国产另类ts人妖一区二区三区| 精品国产高清自在线一区二区三区| 一区二区在线电影| 成人无码AV一区二区| 精品伦精品一区二区三区视频 | 日本免费一区二区三区四区五六区| 国产精品被窝福利一区| 日韩a无吗一区二区三区| 精品国产一区二区二三区在线观看| 国产精品一区二区久久| 亚洲一区二区三区国产精品无码| 国产内射999视频一区| 国产一区二区内射最近更新| 色噜噜AV亚洲色一区二区| 成人午夜视频精品一区| 中文字幕VA一区二区三区| 日韩精品无码一区二区三区四区| 综合久久一区二区三区 | 人妻av无码一区二区三区| 无码国产精品一区二区免费式芒果| 国产激情з∠视频一区二区| 无码欧精品亚洲日韩一区| 精品无码人妻一区二区三区| 亚洲一区二区三区在线| 成人无号精品一区二区三区| 色系一区二区三区四区五区| 国产无码一区二区在线| 日本中文字幕一区二区有码在线| 一区高清大胆人体| 麻豆AV天堂一区二区香蕉 | 久久se精品一区二区影院| 国产午夜精品免费一区二区三区|