Android富文本開發

基礎概念目錄介紹

  • 01.業務需求簡單介紹
  • 02.實現的方案介紹
  • 03.異常狀態下保存狀態信息
  • 04.處理軟鍵盤迴刪按鈕邏輯
  • 05.在指定位置插入圖片
  • 06.在指定位置插入輸入文字
  • 07.如果對選中文字加粗
  • 08.利用Span對文字屬性處理
  • 09.如何設置插入多張圖片
  • 10.如何設置插入網絡圖片
  • 11.如何避免插入圖片OOM
  • 12.如何刪除圖片或者文字
  • 13.刪除和插入圖片添加動畫
  • 14.點擊圖片可以查看大圖
  • 15.如何暴露設置文字屬性方法
  • 16.文字中間添加圖片注意事項
  • 17.鍵盤彈出和收縮優化
  • 18.前後臺切換編輯富文本優化
  • 19.生成html片段上傳服務器
  • 20.生成json片段上傳服務器
  • 21.圖片上傳策略問題思考

00.該控件介紹

1.1 富文本介紹

  • 自定義文本控件,支持富文本,包含兩種狀態:編輯狀態和預覽狀態。編輯狀態中,可以對插入本地或者網絡圖片,可以同時插入多張有序圖片和刪除圖片,支持圖文混排,並且可以對文字內容簡單操作加粗字體,設置字體下劃線,支持設置文字超鏈接(超鏈接支持跳轉),還可以統計富文本中的字數,功能正在開發中和完善中……

1.2 富文本效果圖

image
image
image
image
image

1.3 富文本開源庫

  • https://github.com/yangchong211/YCCustomText

01.業務需求簡單介紹

  • 富文本控件支持動態插入文字,圖片等圖文混排內容。圖片可以支持本地圖片,也支持插入網絡鏈接圖片;
  • 富文本又兩種狀態:編輯狀態 + 預覽狀態 。兩種狀態可以相互進行切換;
  • 富文本在編輯狀態,可以同時選擇插入超過一張以上的多張圖片,並且可以動態設置圖片之間的top間距;
  • 在編輯狀態,支持利用光標刪除文字內容,同時也支持用光標刪除圖片;
  • 在編輯狀態,插入圖片後,圖片的寬度填充滿手機屏幕的寬度,然後高度可以動態設置,圖片是劇中裁剪顯示;
  • 在編輯狀態,插入圖片後,如果本地圖片過大,要求對圖片進行質量壓縮,大小壓縮;
  • 在編輯狀態,插入多張圖片時,添加插入過渡動畫,避免顯示圖片生硬。結束後,光標移到插入圖片中的最後一行顯示;
  • 編輯狀態中,圖片點擊暴露點擊事件接口,可以在4個邊角位置動態設置一個刪除圖片的功能,點擊刪除按鈕則刪除圖片;
  • 連續插入多張圖片時,比如順序1,2,3,注意避免出現圖片插入順序混亂的問題(異步插入多張圖片可能出現順序錯亂問題);
  • 在編輯富文本狀態的時候,連續多張圖片之間插入輸入框,方便在圖片間輸入文本內容;
  • 在編輯狀態中,可以設置文字大小和顏色,同時做好拓展需求,後期可能添加文本加粗,下劃線,插入超鏈接,對齊方式等功能;
  • 編輯狀態,連續插入多張圖片,如果想在圖片中間插入文字內容,則需要靠譜在圖片之間預留編輯文本控件,方便操作;
  • 支持對文字選中的內容進行設置加粗,添加下劃線,改變顏色,設置對齊方式等等;
  • 關於富文本字數統計,由於富文本中包括文字和圖片,因此圖片和文字數量統計分開。參考易車是:共n個文字,共n個圖片顯示

02.實現的方案介紹

2.0 頁面構成分析

  • 整個界面的要求
    • 整體界面可滾動,可以編輯,也可以預覽
    • 內容可編輯可以插入文字、圖片等。圖片提供按鈕操作
    • 軟鍵盤刪除鍵可刪除圖片,也可以刪除文字內容
    • 文字可以修改屬性,比如加粗,對齊,下劃線
  • 根據富文本作出以下分析
    • 使用原生控件,可插入圖片、文字界面不能用一個EditText來做,需要使用LinearLayout添加不同的控件,圖片部分用ImageView,界面可滑動最外層使用ScrollView。
    • 使用WebView+js+css方式,富文本格式用html方式展現,比較複雜,對標籤要非常熟悉纔可以嘗試使用
  • 使用原生控件多焦點問題分析
    • 界面是由多個輸入區域拼接而成,暫且把輸入區域稱爲EditText,圖片區域稱爲ImageView,外層是LinearLayout。
    • 如果一個富文本是:文字1+圖片1+文字2+文字3+圖片3+圖片4;那麼使用LinearLayout包含多個EditText實現的難點:
      • 如何處理記錄當前的焦點區域
      • 如何處理在文字區域的中間位置插入ImageView樣式的拆分和合並
      • 如何處理輸入區域的刪除鍵處理

2.2 第一種方案

  • 使用ScrollView作爲最外層,佈局包含LineaLayout,圖文混排內容,則是用TextView/EditText和ImageView去填充。
  • 富文本編輯狀態:ScrollView + LineaLayout + n個EditText+Span + n個ImageView
  • 富文本預覽狀態:ScrollView + LineaLayout + n個TextView+Span + n個ImageView
  • 刪除的時候,根據光標的位置,如果光標遇到是圖片,則可以用光標刪除圖片;如果光標遇到是文字,則可以用光標刪除文字
  • 當插入或者刪除圖片的時候,可以添加一個過渡動畫效果,避免直接生硬的顯示。如何在ViewGroup中添加view,刪除view時給相應view和受影響的其他view添加動畫,不太容易做。如果只是對受到影響的view添加動畫,可以通過設置view的高度使之顯示和隱藏,還可以利用ScrollView通過滾動隱藏和顯示動畫,但其他受影響的view則比較難處理,最終選擇佈局動畫LayoutTransition 就可以很好地完成這個功能。

2.3 第二種方法

  • 使用WebView實現編輯器,支持n多格式,例如常見的html或者markdown格式。利用html標籤對富文本處理,這種方式就需要專門處理標籤的樣式。
  • 注意這種方法的實現,需要深入研究js,css等,必須非常熟悉纔可以用到實際開發中,可以當作學習一下。這種方式對於圖片的顯示和上傳,相比原生要麻煩一些。

2.4 富文本支持功能

  • 支持加粗、斜體、刪除線、下劃線行內樣式,一行代碼即可設置文本span屬性,十分方便
  • 支持添加單張或者多張圖片,並且插入過渡動畫友好,同時可以保證插入圖片順序
  • 支持富文本編輯狀態和預覽狀態的切換,支持富文本內容轉化爲json內容輸出,轉化爲html內容輸出
  • 支持設置富文本的文字大小,行間距,圖片和文本間距,以及插入圖片的寬和高的屬性
  • 圖片支持點擊預覽,支持點擊叉號控件去除圖片,暴露給外部開發者調用。同時加載圖片的邏輯也是暴露給外部開發者,充分解耦
  • 關於富文本字數統計,由於富文本中包括文字和圖片,因此圖片和文字數量統計分開。參考易車是:共n個文字,共n個圖片顯示

03.異常狀態下保存狀態信息

  • 對於自定義View,如果頁面出現異常導致自定義View異常退出,則當然希望保存一些重要的信息。自定義保存狀態類,繼承BaseSavedState,代碼如下所示
    public class TextEditorState extends View.BaseSavedState {
    
        public int rtImageHeight;
    
        public static final Creator<TextEditorState> CREATOR = new Creator<TextEditorState>() {
            @Override
            public TextEditorState createFromParcel(Parcel in) {
                return new TextEditorState(in);
            }
    
            @Override
            public TextEditorState[] newArray(int size) {
                return new TextEditorState[size];
            }
        };
    
        public TextEditorState(Parcelable superState) {
            super(superState);
        }
    
        public TextEditorState(Parcel source) {
            super(source);
            rtImageHeight = source.readInt();
        }
    
        @Override
        public void writeToParcel(Parcel out, int flags) {
            super.writeToParcel(out, flags);
            out.writeInt(rtImageHeight);
        }
    }
    
  • 如何使用該保存狀態欄,自定義View中,有兩個特別的方法,分別是onSaveInstanceState和onRestoreInstanceState,具體邏輯如下所示
    /**
     * 保存重要信息
     * @return
     */
    @Nullable
    @Override
    protected Parcelable onSaveInstanceState() {
        Parcelable superState = super.onSaveInstanceState();
        TextEditorState viewState = new TextEditorState(superState);
        viewState.rtImageHeight = rtImageHeight;
        return viewState;
    }
    
    /**
     * 復現
     * @param state								state
     */
    @Override
    protected void onRestoreInstanceState(Parcelable state) {
        TextEditorState viewState = (TextEditorState) state;
        rtImageHeight = viewState.rtImageHeight;
        super.onRestoreInstanceState(viewState.getSuperState());
        requestLayout();
    }
    

04.處理軟鍵盤迴刪按鈕邏輯

  • 想了一下,當富文本處於編輯的狀態,利用光標可以進行刪除插入點之前的字符。刪除的時候,根據光標的位置,如果光標遇到是圖片,則可以用光標刪除圖片;如果光標遇到是文字,則可以用光標刪除文字。
  • 更詳細的來說,監聽刪除鍵的點擊的邏輯需要注意,當光標在EditText 輸入中間,點擊刪除不進行處理正常刪除;當光標在EditText首端,判斷前一個控件,如果是圖片控件,刪除圖片控件,如果是輸入控件,刪除當前控件並將輸入區域合併成一個輸入區域。
  • 創建一個鍵盤退格監聽事件,代碼如下所示:
    // 初始化鍵盤退格監聽,主要用來處理點擊回刪按鈕時,view的一些列合併操作
    keyListener = new OnKeyListener() {
        @Override
        public boolean onKey(View v, int keyCode, KeyEvent event) {
            //KeyEvent.KEYCODE_DEL    刪除插入點之前的字符
            if (event.getAction() == KeyEvent.ACTION_DOWN && event.getKeyCode() == KeyEvent.KEYCODE_DEL) {
                EditText edit = (EditText) v;
                //處於退格刪除的邏輯
                onBackspacePress(edit);
            }
            return false;
        }
    };
    
  • 然後針對退格刪除,分爲兩種情況,第一種是刪除圖片,第二種是刪除文字內容。具體代碼如下所示:
    /**
     * 處理軟鍵盤backSpace回退事件
     * @param editTxt 					光標所在的文本輸入框
     */
    private void onBackspacePress(EditText editTxt) {
        try {
            int startSelection = editTxt.getSelectionStart();
            // 只有在光標已經頂到文本輸入框的最前方,在判定是否刪除之前的圖片,或兩個View合併
            if (startSelection == 0) {
                int editIndex = layout.indexOfChild(editTxt);
                // 如果editIndex-1<0,
                View preView = layout.getChildAt(editIndex - 1);
                if (null != preView) {
                    if (preView instanceof RelativeLayout) {
                        // 光標EditText的上一個view對應的是圖片,刪除圖片操作
                        onImageCloseClick(preView);
                    } else if (preView instanceof EditText) {
                        // 光標EditText的上一個view對應的還是文本框EditText
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    

05.在指定位置插入圖片

  • 當點擊插入圖片的時候,需要思考兩個問題。第一個是在那個位置插入圖片,所以需要定位到這個位置;第二個是插入圖片後,什麼時候折行操作。
  • 對於上面兩個問題,這個位置可以取光標所在的位置,但是對於一個EditText輸入文本,插入圖片這個位置可以分多種情況:
    • 如果光標已經頂在了editText的最前面,則直接插入圖片,並且EditText下移即可
    • 如果光標已經頂在了editText的最末端,則需要添加新的imageView
    • 如果光標已經頂在了editText的最中間,則需要分割字符串,分割成兩個EditText,並在兩個EditText中間插入圖片
    • 如果當前獲取焦點的EditText爲空,直接在EditText下方插入圖片,並且插入空的EditText
  • 代碼思路如下所示
    /**
     * 插入一張圖片
     * @param imagePath							圖片路徑地址
     */
    public void insertImage(String imagePath) {
        if (TextUtils.isEmpty(imagePath)){
            return;
        }
        try {
            //lastFocusEdit獲取焦點的EditText
            String lastEditStr = lastFocusEdit.getText().toString();
            //獲取光標所在位置
            int cursorIndex = lastFocusEdit.getSelectionStart();
            //獲取光標前面的字符串
            String editStr1 = lastEditStr.substring(0, cursorIndex).trim();
            //獲取光標後的字符串
            String editStr2 = lastEditStr.substring(cursorIndex).trim();
            //獲取焦點的EditText所在位置
            int lastEditIndex = layout.indexOfChild(lastFocusEdit);
            if (lastEditStr.length() == 0) {
                //如果當前獲取焦點的EditText爲空,直接在EditText下方插入圖片,並且插入空的EditText
            } else if (editStr1.length() == 0) {
                //如果光標已經頂在了editText的最前面,則直接插入圖片,並且EditText下移即可
            } else if (editStr2.length() == 0) {
                // 如果光標已經頂在了editText的最末端,則需要添加新的imageView和EditText
            } else {
                //如果光標已經頂在了editText的最中間,則需要分割字符串,分割成兩個EditText,並在兩個EditText中間插入圖片
            }
            hideKeyBoard();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    

06.在指定位置插入輸入文字

  • 前面已經提到了,如果一個富文本是:文字1+圖片1+文字2+文字3+圖片3+圖片4,那麼點擊文字1控件則在此輸入文字,點擊文字3控件則在此輸入文字。
  • 所以,這樣操作,確定處理記錄當前的焦點區域位置十分重要。當前的編輯器已經添加了多個輸入文本EditText,現在的問題在於需要記錄當前編輯的EditText,在應用樣式的時候定位到輸入的控件,在編輯器中添加一個變量lastFocusEdit。具體可以看代碼……
  • 既然可以記錄最後焦點輸入文本,那麼如何監聽當前的輸入控件呢,這就用到了OnFocusChangeListener,這個又是在哪裏用到,具體如下面所示。要先setOnFocusChangeListener(focusListener) 再 requestFocus。
    /**
     * 所有EditText的焦點監聽listener
     */
    private OnFocusChangeListener focusListener;
    
    
    focusListener = new OnFocusChangeListener() {
        @Override
        public void onFocusChange(View v, boolean hasFocus) {
            if (hasFocus) {
                lastFocusEdit = (EditText) v;
                HyperLogUtils.d("HyperTextEditor---onFocusChange--"+lastFocusEdit);
            }
        }
    };
    
    /**
     * 在特定位置插入EditText
     * @param index							位置
     * @param editStr						EditText顯示的文字
     */
    public void addEditTextAtIndex(final int index, CharSequence editStr) {
        //省略部分代碼
    	try {
    		EditText editText = createEditText("插入文字", EDIT_PADDING);
    		editText.setOnFocusChangeListener(focusListener);
    		layout.addView(editText, index);
    		//插入新的EditText之後,修改lastFocusEdit的指向
    		lastFocusEdit = editText;
    		//獲取焦點
    		lastFocusEdit.requestFocus();
    		//將光標移至文字指定索引處
    		lastFocusEdit.setSelection(editStr.length(), editStr.length());
    	} catch (Exception e) {
    		e.printStackTrace();
    	}
    }
    

07.如果對選中文字加粗

  • Span 的分類介紹
    • 字符外觀,這種類型修改字符的外形但是不影響字符的測量,會觸發文本重新繪製但是不觸發重新佈局。
      • ForegroundColorSpan,BackgroundColorSpan,UnderlineSpan,StrikethrougnSpan
    • 字符大小布局,這種類型Span會更改文本的大小和佈局,會觸發文本的重新測量繪製
      • StyleSpan,RelativeSizeSpan,AbsoluteSizeSpan
    • 影響段落級別,這種類型Span 在段落級別起作用,更改文本塊在段落級別的外觀,修改對齊方式,邊距等。
      • AlignmentSpan,BulletSpan,QuoteSpan
  • 實現基礎樣式 粗體、 斜體、 下劃線 、中劃線 的設置和取消。舉個例子,對文本加粗,文字設置span樣式注意要點,這裏需要區分幾種情況
  • 當前選中區域不存在 bold 樣式 這裏我們選中BB。兩種情況
    • 當前區域緊靠左側或者右側不存在粗體樣式: AABBCC 這時候直接設置 span即可
    • 當前區域緊靠左側或者右側存在粗體樣式如: AABBCC AABBCC AABBCC。這時候需要合併左右兩側的span,只剩下一個 span
  • 當前選中區域存在了Bold 樣式 選中 ABBC。四種情況:
    • 選中樣式兩側不存在連續的bold樣式 AABBCC
    • 選中內部兩端存在連續的bold 樣式 AABBCC
    • 選中左側存在連續的bold 樣式 AABBCC
    • 選中右側存在連續的bold 樣式 AABBCC
    • 這時候需要合併左右兩側已經存在的span,只剩下一個 span
  • 接下來逐步分解,然後處理span的邏輯順序如下所示
    • 首先對選中文字內容樣式情況判斷
    • 邊界判斷與設置
    • 取消Span(當我們選中的區域在一段連續的 Bold 樣式裏面的時候,再次選擇Bold將會取消樣式)
  • 什麼時候取消span呢,這個邏輯是比較複雜的,具體看看下面的舉例。
    • 當我們選中的區域在一段連續的 Bold 樣式裏面的時候,再次選擇Bold將會取消樣式
    • 用戶可以隨意的刪除文本,在刪除過程中可能會出現如下的情況:
      • 用戶輸入了 AABBCCDD
      • 用戶選擇了粗體樣式 AABBCCDD
      • 用戶刪除了CC然後顯示如下 : AABB DD
      • 這個時候選中其中的BD 此時,在該區域中 存在兩個span ,並且沒有一個 span 完全包裹選中的 BD
      • 在這種情況下 仍需要進行左右側邊界判斷進行刪除。這個具體可以看代碼邏輯。

08.利用Span對文字屬性處理

  • 這裏僅僅是對字體加粗進行介紹,其實設置span可以找到規律。多個span樣式,考慮到後期的拓展性,肯定要進行封裝和抽象,具體該如何處理呢?
    • 設置文本選中內容加粗模式,代碼如下所示,可以看到這裏只需要傳遞一個lastFocusEdit對象即可,這個對象是最近被聚焦的EditText。
    /**
     * 修改加粗樣式
     */
    public void bold(EditText lastFocusEdit) {
        //獲取editable對象
        Editable editable = lastFocusEdit.getEditableText();
        //獲取當前選中的起始位置
        int start = lastFocusEdit.getSelectionStart();
        //獲取當前選中的末尾位置
        int end = lastFocusEdit.getSelectionEnd();
        HyperLogUtils.i("bold select  Start:" + start + "   end:  " + end);
        if (checkNormalStyle(start, end)) {
            return;
        }
        new BoldStyle().applyStyle(editable, start, end);
    }
    
    • 然後如何調用這個,在HyperTextEditor類中代碼如下所示。爲何要這樣寫,可以把HyperTextEditor富文本類中設置span的邏輯放到SpanTextHelper類中處理,該類專門處理各種span屬性,這樣代碼結構更加清晰,也方便後期增加更多span屬性,避免一個類代碼太臃腫。
    /**
     * 修改加粗樣式
     */
    public void bold() {
        SpanTextHelper.getInstance().bold(lastFocusEdit);
    }
    
  • 然後看一下new BoldStyle().applyStyle(editable, start, end)具體做了什麼?下面這段代碼邏輯,具體可以看07.如果對選中文字加粗的分析思路。
    public void applyStyle(Editable editable, int start, int end) {
        //獲取 從  start 到 end 位置上所有的指定 class 類型的 Span數組
        E[] spans = editable.getSpans(start, end, clazzE);
        E existingSpan = null;
        if (spans.length > 0) {
            existingSpan = spans[0];
        }
        if (existingSpan == null) {
            //當前選中內部無此樣式,開始設置span樣式
            checkAndMergeSpan(editable, start, end, clazzE);
        } else {
            //獲取 一個 span 的起始位置
            int existingSpanStart = editable.getSpanStart(existingSpan);
            //獲取一個span 的結束位置
            int existingSpanEnd = editable.getSpanEnd(existingSpan);
            if (existingSpanStart <= start && existingSpanEnd >= end) {
                //在一個 完整的 span 中
                //刪除 樣式
                //
                removeStyle(editable, start, end, clazzE, true);
            } else {
                //當前選中區域存在了某某樣式,需要合併樣式
                checkAndMergeSpan(editable, start, end, clazzE);
            }
        }
    }
    

09.如何設置插入多張圖片

  • 富文本當然支持插入多張圖片,那麼插入多張圖片是如何操作呢。插入1,2,3這三張圖片,如何保證它們的插入順序,從而避免插入錯位,帶着這幾個問題看一下插入多張圖片操作。
    Observable.create(new ObservableOnSubscribe<String>() {
        @Override
        public void subscribe(ObservableEmitter<String> emitter) {
            try{
                hte_content.measure(0, 0);
                List<Uri> mSelected = Matisse.obtainResult(data);
                // 可以同時插入多張圖片
                for (Uri imageUri : mSelected) {
                    String imagePath = HyperLibUtils.getFilePathFromUri(NewActivity.this,  imageUri);
                    Bitmap bitmap = HyperLibUtils.getSmallBitmap(imagePath, screenWidth, screenHeight);
                    //壓縮圖片
                    imagePath = SDCardUtil.saveToSdCard(bitmap);
                    emitter.onNext(imagePath);
                }
                emitter.onComplete();
            }catch (Exception e){
                e.printStackTrace();
                emitter.onError(e);
            }
        }
    })
            .subscribeOn(Schedulers.io())//生產事件在io
            .observeOn(AndroidSchedulers.mainThread())//消費事件在UI線程
            .subscribe(new Observer<String>() {
                @Override
                public void onComplete() {
                    ToastUtils.showRoundRectToast("圖片插入成功");
                }
    
                @Override
                public void onError(Throwable e) {
                    ToastUtils.showRoundRectToast("圖片插入失敗:"+e.getMessage());
                }
    
                @Override
                public void onSubscribe(Disposable d) {
    
                }
    
                @Override
                public void onNext(String imagePath) {
                    //插入圖片
                    hte_content.insertImage(imagePath);
                }
            });
    

10.如何設置插入網絡圖片

  • 插入圖片有兩種情況,一種是本地圖片,一種是網絡圖片。由於富文本中對插入圖片的寬高有限制,即可以動態設置圖片的高度,這就要求請求網絡圖片後,需要對圖片進行處理。
  • 首先看一下插入圖片的代碼,在HyperTextEditor類中,由於封裝lib,不建議在lib中使用某個圖片加載庫加載圖片,而應該是暴露給外部開發者去加載圖片。
    /**
     * 在特定位置添加ImageView
     */
    public void addImageViewAtIndex(final int index, final String imagePath) {
        if (TextUtils.isEmpty(imagePath)){
            return;
        }
        try {
            imagePaths.add(imagePath);
            final RelativeLayout imageLayout = createImageLayout();
            HyperImageView imageView = imageLayout.findViewById(R.id.edit_imageView);
            imageView.setAbsolutePath(imagePath);
            HyperManager.getInstance().loadImage(imagePath, imageView, rtImageHeight);
            layout.addView(imageLayout, index);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
  • 那麼具體在那個地方去loadImage設置加載圖片呢?可以發現這樣極大地提高了代碼的拓展性,原因是你可能用glide,他可能用Picasso,還有的用ImageLoader,所以最好暴露給外部。
    HyperManager.getInstance().setImageLoader(new ImageLoader() {
        @Override
        public void loadImage(final String imagePath, final ImageView imageView, final int imageHeight) {
            Log.e("---", "imageHeight: "+imageHeight);
            //如果是網絡圖片
            if (imagePath.startsWith("http://") || imagePath.startsWith("https://")){
                //直接用圖片加載框架加載圖片即可
            } else { //如果是本地圖片
                
            }
        }
    });
    

11.如何避免插入圖片OOM

  • 加載一個本地的大圖片或者網絡圖片,從加載到設置到View上,如何減下內存,避免加載圖片OOM。
    • 在展示高分辨率圖片的時候,最好先將圖片進行壓縮。壓縮後的圖片大小應該和用來展示它的控件大小相近,在一個很小的ImageView上顯示一張超大的圖片不會帶來任何視覺上的好處,但卻會佔用相當多寶貴的內存,而且在性能上還可能會帶來負面影響。
  • 加載圖片的內存都去哪裏呢?
    • 其實我們的內存就是去bitmap裏了,BitmapFactory的每個decode函數都會生成一個bitmap對象,用於存放解碼後的圖像,然後返回該引用。如果圖像數據較大就會造成bitmap對象申請的內存較多,如果圖像過多就會造成內存不夠用自然就會出現out of memory的現象。
  • 爲何容易OOM?
    • 通過BitmapFactory的decode的這些方法會嘗試爲已經構建的bitmap分配內存,這時就會很容易導致OOM出現。爲此每一種解析方法都提供了一個可選的BitmapFactory.Options參數,將這個參數的inJustDecodeBounds屬性設置爲true就可以讓解析方法禁止爲bitmap分配內存,返回值也不再是一個Bitmap對象,而是null。
  • 如何對圖片進行壓縮?
    • 1.解析圖片,獲取圖片資源的屬性
    • 2.計算圖片的縮放值
    • 3.最後對圖片進行質量壓縮
  • 具體設置圖片壓縮的代碼如下所示
    public static Bitmap getSmallBitmap(String filePath, int newWidth, int newHeight) {
        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeFile(filePath, options);
        // Calculate inSampleSize
        // 計算圖片的縮放值
        options.inSampleSize = calculateInSampleSize(options, newWidth, newHeight);
        // Decode bitmap with inSampleSize set
        options.inJustDecodeBounds = false;
        Bitmap bitmap = BitmapFactory.decodeFile(filePath, options);
        // 質量壓縮
        Bitmap newBitmap = compressImage(bitmap, 500);
        if (bitmap != null){
            //手動釋放資源
            bitmap.recycle();
        }
        return newBitmap;
    }
    
  • 思考:inJustDecodeBounds這個參數是幹什麼的?
    • 如果設置爲true則表示decode函數不會生成bitmap對象,僅是將圖像相關的參數填充到option對象裏,這樣我們就可以在不生成bitmap而獲取到圖像的相關參數了。
  • 爲何設置兩次inJustDecodeBounds屬性?
    • 第一次:設置爲true則表示decode函數不會生成bitmap對象,僅是將圖像相關的參數填充到option對象裏,這樣我們就可以在不生成bitmap而獲取到圖像的相關參數。
    • 第二次:將inJustDecodeBounds設置爲false再次調用decode函數時就能生成bitmap了。而此時的bitmap已經壓縮減小很多了,所以加載到內存中並不會導致OOM。

12.如何刪除圖片或者文字

  • 當富文本處於編輯狀態時,點擊刪除圖片是可以刪除圖片的,對於刪除的邏輯,封裝的lib可以給開發者暴露一個刪除的監聽事件。注意刪除圖片有兩種操作:第一種是利用光標刪除,第二種是點擊觸發刪除。刪除圖片後,不僅僅是要刪除圖片數據,而且還要刪除圖片ImageView控件。
    /**
     * 處理圖片上刪除的點擊事件
     * 刪除類型 0代表backspace刪除 1代表按紅叉按鈕刪除
     * @param view 							整個image對應的relativeLayout view
     */
    private void onImageCloseClick(View view) {
        try {
            //判斷過渡動畫是否結束,只能等到結束纔可以操作
            if (!mTransition.isRunning()) {
                disappearingImageIndex = layout.indexOfChild(view);
                //刪除文件夾裏的圖片
                List<HyperEditData> dataList = buildEditData();
                HyperEditData editData = dataList.get(disappearingImageIndex);
                if (editData.getImagePath() != null){
                    if (onHyperListener != null){
                        onHyperListener.onRtImageDelete(editData.getImagePath());
                    }
                    //SDCardUtil.deleteFile(editData.imagePath);
                    //從圖片集合中移除圖片鏈接
                    imagePaths.remove(editData.getImagePath());
                }
                //然後移除當前view
                layout.removeView(view);
                //合併上下EditText內容
                mergeEditText();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    

13.刪除和插入圖片添加動畫

  • 爲什麼要添加插入圖片的過渡動畫
    • 當向一個ViewGroup添加控件或者移除控件;這種場景雖然能夠實現效果,並沒有一點過度效果,直來直去的添加或者移除,顯得有點生硬。有沒有辦法添加一定的過度效果,讓實現的效果顯得圓滑呢?
  • LayoutTransition簡單介紹
    • LayoutTransition類實際上Android系統中的一個實用工具類。使用LayoutTransition類在一個ViewGroup中對佈局更改進行動畫處理。
  • 如何運用到插入或者刪除圖片場景中
    • 向一個ViewGroup添加控件或者移除控件,這兩種效果的過程是應對應於控件的顯示、控件添加時其他控件的位置移動、控件的消失、控件移除時其他控件的位置移動等四種動畫效果。這些動畫效果在LayoutTransition中,由以下四個關鍵字做出了相關聲明:
      • APPEARING:元素在容器中顯現時需要動畫顯示。
      • CHANGE_APPEARING:由於容器中要顯現一個新的元素,其它元素的變化需要動畫顯示。
      • DISAPPEARING:元素在容器中消失時需要動畫顯示。
      • CHANGE_DISAPPEARING:由於容器中某個元素要消失,其它元素的變化需要動畫顯示。
    • 也就是說,ViewGroup中有多個ImageView對象,如果需要刪除其中一個ImageView對象的話,該ImageView對象可以設置動畫(即DISAPPEARING 動畫形式),ViewGroup中的其它ImageView對象此時移動到新的位置的過程中也可以設置相關的動畫(即CHANGE_DISAPPEARING 動畫形式);
    • 若向ViewGroup中添加一個ImageView,ImageView對象可以設置動畫(即APPEARING 動畫形式),ViewGroup中的其它ImageView對象此時移動到新的位置的過程中也可以設置相關的動畫(即CHANGE_APPEARING 動畫形式)。
    • 給ViewGroup設置動畫很簡單,只需要生成一個LayoutTransition實例,然後調用ViewGroup的setLayoutTransition(LayoutTransition)函數就可以了。當設置了佈局動畫的ViewGroup添加或者刪除內部view時就會觸發動畫。
  • 具體初始化動畫的代碼如下所示:
    mTransition = new LayoutTransition();
    mTransition.addTransitionListener(new LayoutTransition.TransitionListener() {
    
        @Override
        public void startTransition(LayoutTransition transition, ViewGroup container, View view, int transitionType) {
        }
        
        @Override
        public void endTransition(LayoutTransition transition,
                ViewGroup container, View view, int transitionType) {
            if (!transition.isRunning() && transitionType == LayoutTransition.CHANGE_DISAPPEARING) {
                // transition動畫結束,合併EditText
                 mergeEditText();
            }
        }
    });
    mTransition.enableTransitionType(LayoutTransition.APPEARING);
    mTransition.setDuration(300);
    layout.setLayoutTransition(mTransition);
    
  • 有個問題需要注意一下,當控件銷燬的時候,記得把監聽給移除一下更好,代碼如下所示
    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        if (mTransition!=null){
            //移除Layout變化監聽
            mTransition.removeTransitionListener(transitionListener);
        }
    }
    
  • 動畫執行先後的順序
    • 分析源碼可以知道,默認情況下DISAPPEARING和CHANGE_APPEARING類型動畫會立即執行,其他類型動畫則會有個延遲。也就是說如果刪除view,被刪除的view將先執行動畫消失,經過一些延遲受影響的view會進行動畫補上位置,如果添加view,受影響的view將會先給添加的view騰位置執行CHANGE_APPEARING動畫,經過一些時間的延遲纔會執行APPEARING動畫。這裏就不貼分析源碼的思路呢!

14.點擊圖片可以查看大圖

  • 編輯狀態時,由於圖片有空能比較大,在顯示在富文本的時候,會裁剪局中顯示,也就是圖片會顯示不全。那麼後期如果是想添加點擊圖片查看,則需要暴露給開發者監聽事件,需要考慮到後期拓展性,代碼如下所示:
    • 這樣做的目的是是暴露給外部開發者調用,點擊圖片的操作只需要傳遞view還有圖片即可。
    // 圖片處理
    btnListener = new OnClickListener() {
        @Override
        public void onClick(View v) {
            if (v instanceof HyperImageView){
                HyperImageView imageView = (HyperImageView)v;
                // 開放圖片點擊接口
                if (onHyperListener != null){
                    onHyperListener.onImageClick(imageView, imageView.getAbsolutePath());
                }
            } 
        }
    };
    

15.如何暴露設置文字屬性方法

  • 針對設置文字加粗,下劃線,刪除線等span屬性。同時設置span,有許多類似的地方,考慮到後期的添加和移除,如何封裝能夠提高代碼的擴展性。
    /**
     * 修改加粗樣式
     */
    public void bold() {
        SpanTextHelper.getInstance().bold(lastFocusEdit);
    }
    
    /**
     * 修改斜體樣式
     */
    public void italic() {
        SpanTextHelper.getInstance().italic(lastFocusEdit);
    }
    
    /**
     * 修改刪除線樣式
     */
    public void strikeThrough() {
        SpanTextHelper.getInstance().strikeThrough(lastFocusEdit);
    }
    
    /**
     * 修改下劃線樣式
     */
    public void underline() {
        SpanTextHelper.getInstance().underline(lastFocusEdit);
    }
    
  • 上面實現了選中文本加粗的功能,斜體、 下劃線 、中劃線等樣式的設置和取消與粗體樣式一致,只是創建 span 的區別而已,可以將代碼進行抽取。
    public abstract class NormalStyle<E> {
    
        private Class<E> clazzE;
    
        public NormalStyle() {
            //利用反射
            clazzE = (Class<E>) ((ParameterizedType) this.getClass().getGenericSuperclass()).getActualTypeArguments()[0];
        }
    
        /**
         * 樣式情況判斷
         * @param editable                      editable
         * @param start                         start
         * @param end                           end
         */
        public void applyStyle(Editable editable, int start, int end) {
    
        }
    }
    
  • 其他的設置span的屬性代碼即是如下所示,可以看到添加一種類型很容易,也容易看懂,便於拓展:
    public class ItalicStyle extends NormalStyle<ItalicStyleSpan> {
        @Override
        protected ItalicStyleSpan newSpan() {
            return new ItalicStyleSpan();
        }
    }
    
    public class UnderlineStyle extends NormalStyle<UnderLineSpan> {
        @Override
        protected UnderLineSpan newSpan() {
            return new UnderLineSpan();
        }
    }
    

16.文字中間添加圖片注意事項

  • 在文字中添加圖片比較特殊,因此這裏單獨拿出來說一下。在文字內容中間插入圖片,則需要分割字符串,分割成兩個EditText,並在兩個EditText中間插入圖片,那麼這個光標又定位在何處呢?
    • 對於光標前面的字符串保留,設置給當前獲得焦點的EditText(此爲分割出來的第一個EditText)
    • 把光標後面的字符串放在新創建的EditText中(此爲分割出來的第二個EditText)
    • 在第二個EditText的位置插入一個空的EditText,以便連續插入多張圖片時,有空間寫文字,第二個EditText下移
    • 在空的EditText的位置插入圖片佈局,空的EditText下移。注意,這個過程添加動畫過渡一下插入的效果比較好,不然會比較生硬
    //獲取光標所在位置
    int cursorIndex = lastFocusEdit.getSelectionStart();
    //獲取光標前面的字符串
    String editStr1 = lastEditStr.substring(0, cursorIndex).trim();
    //獲取光標後的字符串
    String editStr2 = lastEditStr.substring(cursorIndex).trim();
    
    lastFocusEdit.setText(editStr1);
    addEditTextAtIndex(lastEditIndex + 1, editStr2);
    addEditTextAtIndex(lastEditIndex + 1, "");
    addImageViewAtIndex(lastEditIndex + 1, imagePath);
    

17.鍵盤彈出和收縮優化

  • 軟鍵盤彈出的時機
    • 如果不做任何處理,系統默認的是,進入頁面,第一個輸入框自動獲取焦點軟鍵盤自動彈出,這種用戶交互方式,往往不是產品想要的,往往會提出以下優化需求:
    • 需求1:editText獲取焦點,但是不彈出軟鍵盤(也就是說光標顯示第一個輸入框,不主動彈軟鍵盤)
      • 在第一個輸入框的最直接父佈局加入:android:focusable=“true”;android:focusableInTouchMode=“true”
        • (效果:軟鍵盤不彈出,光標不顯示,其他輸入框也不獲取焦點,ps非直接父佈局沒有效果)
      • android:windowSoftInputMode=“stateAlwaysHidden”
        • (效果:軟鍵盤不彈出,光標顯示在第一個輸入框中)
    • 需求2:editText不獲取焦點,當然軟鍵盤不會主動彈出(光標也不顯示)
      • 在第一個輸入框的最直接父佈局加入:android:focusable=“true”;android:focusableInTouchMode=“true”
        • (效果:軟鍵盤不彈出,光標不顯示,其他輸入框也不獲取焦點,ps非直接父佈局沒有效果)
      • 在父佈局最頂部添加一個高度爲0的EditText,搶了焦點但不展示;
  • 軟鍵盤遮擋界面的問題
    • 當界面中有輸入框,需要彈起軟鍵盤輸入信息的時候,軟鍵盤可能遮擋部分佈局,更有甚者,當前輸入框如果在屏幕下方,軟鍵盤也會直接遮擋輸入框,這種情況對用戶體驗是相當不友好的,所以要根據具體的情況作出相應的處理。
    • android定義了一個屬性,名字爲windowSoftInputMode, 這個屬性用於設置Activity主窗口與軟鍵盤的交互模式,用於避免軟鍵盤遮擋內容的問題。我們可以在AndroidManifet.xml中對Activity進行設置。
    stateUnspecified-未指定狀態:軟件默認採用的交互方式,系統會根據當前界面自動調整軟鍵盤的顯示模式。
    stateUnchanged-不改變狀態:當前界面軟鍵盤狀態由上個界面軟鍵盤的狀態決定;
    stateHidden-隱藏狀態:進入頁面,無論是否有輸入需求,軟鍵盤是隱藏的,但是如果跳轉到下一個頁面軟鍵盤是展示的,回到這個頁面,軟鍵盤可能也是展示的,這個屬性區別下個屬性。
    stateAlwaysHidden-總是隱藏狀態:當設置該狀態時,軟鍵盤總是被隱藏,和stateHidden不同的是,當我們跳轉到下個界面,如果下個頁面的軟鍵盤是顯示的,而我們再次回來的時候,軟鍵盤就會隱藏起來。
    stateVisible-可見狀態:當設置爲這個狀態時,軟鍵盤總是可見的,即使在界面上沒有輸入框的情況下也可以強制彈出來出來。
    stateAlwaysVisible-總是顯示狀態:當設置爲這個狀態時,軟鍵盤總是可見的,和stateVisible不同的是,當我們跳轉到下個界面,如果下個頁面軟鍵盤是隱藏的,而我們再次回來的時候,軟鍵盤就會顯示出來。
    adjustUnspecified-未指定模式:設置軟鍵盤與軟件的顯示內容之間的顯示關係。當你跟我們沒有設置這個值的時候,這個選項也是默認的設置模式。在這中情況下,系統會根據界面選擇不同的模式。
    adjustResize-調整模式:當軟鍵盤顯示的時候,當前界面會自動重繪,會被壓縮,軟鍵盤消失之後,界面恢復正常(正常佈局,非scrollView父佈局);當父佈局是scrollView的時候,軟鍵盤彈出,會將佈局頂起(保證輸入框不被遮擋),不壓縮,而且可以軟鍵盤不消失的情況下,手動滑出被遮擋的佈局;
    adjustPan-默認模式:軟鍵盤彈出,軟鍵盤會遮擋屏幕下半部分佈局,當輸入框在屏幕下方佈局,軟鍵盤彈起,會自動將當前佈局頂起,保證,軟鍵盤不遮擋當前輸入框(正常佈局,非scrollView父佈局)。當父佈局是scrollView的時候,感覺沒啥變化,還是自定將佈局頂起,輸入框不被遮擋,不可以手動滑出被遮擋的佈局(白瞎了scrollView);
    
    • 看了上面的屬性,那麼該如何設置呢?具體效果可以看demo案例。
    <activity android:name=".NewArticleActivity"
        android:windowSoftInputMode="adjustResize|stateHidden"/>
    
  • 軟鍵盤及時退出的問題
    • 當用戶輸入完成之後,必須手動點擊軟鍵盤的收回鍵,軟鍵盤才收起。如果能通過代碼主動將軟鍵盤收起,這對於用戶體驗來說,是一個極大的提升,思前想後,參考網上的文檔,個人比較喜歡的實現方式是通過事件分發機制來解決這個問題。
  • 解決點擊EditText彈出收起鍵盤時出現的黑屏閃現現象
    View rootView = hte_content.getRootView();
    rootView.setBackgroundColor(Color.WHITE);
    

18.前後臺切換編輯富文本優化

  • 由於富文本中,用戶會輸入很多的內容,當關閉頁面時候,需要提醒用戶是否保存輸入內容。同時,切換到後臺的時候,需要注意保存輸入內容,避免長時間切換後臺進程內存吃緊,在回到前臺輸入的內容沒有呢,查閱了汽車之家,易車等app等手機上的富文本編輯器,都會有這個細節點的優化。

19.生成html片段上傳服務器

19.1 提交富文本

  • 客戶端生成html片段到服務器
    • 在客戶端提交帖子,文章。富文本包括圖片,文字內容,還有文字span樣式,同時會選擇一些文章,帖子的標籤。還有設置文章的類型,封面圖,作者等許多屬性。
    • 當點擊提交的時候,客戶端把這些數據,轉化成html,還是轉化成json對象提交給服務器呢?思考一下,會有哪些問題……
  • 轉化成html
    • 對於將單個富文本轉化成html相對來說是比較容易的,因爲富文本中之存在文字,圖片等。轉化成html細心就可以。
    • 但是對於設置富文本的標籤,類型,作者,封面圖,日期,其他關聯屬性怎麼合併到html中呢,這個相對麻煩。
  • 最後想說的是
    • 對於富文本寫帖子,文章,如果寫完富文本提交,則可以使用轉化成html數據提交給服務器;
    • 對於富文本寫完帖子,文章,還有下一步,設置標籤,類型,封面圖,作者,時間,還有其他屬性,則可以使用轉化成json數據提交給服務器;

19.2 編輯富文本

  • 服務器返回html給客戶端加載
    • 涉及到富文本的加載,後臺管理端編輯器生成的一段html 代碼要渲染到移動端上面,一種方法是前端做成html頁面,放到服務器上,移動端這邊直接webView 加載url即可。
    • 還有一種後臺接口直接返回這段html富文本的,String類型的,移動端直接加載的;具體的需求按實際情況而定。
  • 加載html文件流暢問題
    • webView直接加載url體驗上沒那麼流暢,相對的加載html文件會好點。但是對比原生,體驗上稍微弱點。
    • 如果不用WebView,使用TextView顯示html富文本,則會出現圖片不顯示,以及格式問題。
    • 如果不用WebView,使用自定義富文本RichText,則需要解析html顯示,如果對html標籤,js不熟悉,也不太好處理。

20.生成json片段上傳服務器

  • 參考了易車發佈帖子,提交數據到服務器,針對富文本,是把它拼接成對象。將文字,圖片按照富文本的順序拼接成json片段,然後提交給服務器。

20.1 提交富文本

  • 用原生ScrollView + LineaLayout + n個EditText+Span + n個ImageView來實現富文本。可以先創建一個對象用來存儲數據,下面這個實體類比較簡單,開發中字段稍微多些。如下所示
    public class HyperEditData implements Serializable {
    
        /**
         * 富文本輸入文字內容
         */
        private String inputStr;
        /**
         * 富文本輸入圖片地址
         */
        private String imagePath;
        /**
         * 類型:1,代表文字;2,代表圖片
         */
        private int type;
    
        //省略很多set,get方法
    }
    
  • 然後怎麼去把富文本數據按照有序去放到集合中呢?如下所示,具體可以看demo中的代碼……
    /**
     * 對外提供的接口, 生成編輯數據上傳
     */
    public List<HyperEditData> buildEditData() {
        List<HyperEditData> dataList = new ArrayList<>();
        try {
            int num = layout.getChildCount();
            for (int index = 0; index < num; index++) {
                View itemView = layout.getChildAt(index);
                HyperEditData hyperEditData = new HyperEditData();
                if (itemView instanceof EditText) {
                    //文本
                    EditText item = (EditText) itemView;
                    hyperEditData.setInputStr(item.getText().toString());
                    hyperEditData.setType(2);
                } else if (itemView instanceof RelativeLayout) {
                    //圖片
                    HyperImageView item = itemView.findViewById(R.id.edit_imageView);
                    hyperEditData.setImagePath(item.getAbsolutePath());
                    hyperEditData.setType(1);
                }
                dataList.add(hyperEditData);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        HyperLogUtils.d("HyperTextEditor----buildEditData------dataList---"+dataList.size());
        return dataList;
    }
    
  • 最後將富文本數據轉化爲json提交到服務器,服務器拿到json後,結合富文本的後續信息,比如,作者,時間,類型,標籤等創建可以用瀏覽器打開的h5頁面,這個需要跟服務器端配合。如下所示
    List<HyperEditData> editList = hte_content.buildEditData();
    //生成json
    Gson gson = new Gson();
    String content = gson.toJson(editList);
    //轉化成json字符串
    String string = HyperHtmlUtils.stringToJson(content);
    //提交服務器省略
    

20.2 編輯富文本

  • 當然,提交了文章肯定還有審覈功能,這個時候想去修改富文本怎麼辦。ok,需要服務器把之前傳遞給它的json返回給客戶端,然後解析填充到富文本中。這個就沒什麼好說的……

21.圖片上傳策略問題思考

  • 大多數開發者會採用的方式:
    • 先在編輯器裏顯示本地圖片,等待用戶編輯完成再上傳全部圖片,然後用上傳返回的url替換之前html中顯示本地圖片的位置。
  • 這樣會遇到很多問題:
    • 如果圖片很多,上傳的數據量會很大,手機的網絡狀態經常不穩定,很容易上傳失敗。另外等待時間會很長,體驗很差。
  • 解決辦法探討:
    • 選圖完成即上傳,得到url之後直接插入,上傳是耗時操作,再加上圖片壓縮的時間,這樣編輯器顯示圖片會有可觀的延遲時間,實際項目中可以加一個默認的佔位圖,另外加一個標記提醒用戶是否上傳完成,避免沒有上傳成功用戶即提交的問題。
  • 這種場景很容易想到:
    • 比如,在簡書,掘金上寫博客。寫文章時,插入本地圖片,即使你沒有提交文章,也會把圖片上傳到服務器,然後返回一個圖片鏈接給你,最後當你發表文章時,圖片只需要用鏈接替代即可。
  • 參考博客
    • Android富文本編輯器(四):HTML文本轉換:https://www.jianshu.com/p/578085fb07d1
    • Android 端 (圖文混排)富文本編輯器的開發(一):https://www.jianshu.com/p/155aa1e9f9d3
    • 圖文混排富文本文章編輯器實現詳解:https://blog.csdn.net/ljzdyh/article/details/82497625

富文本開源庫:https://github.com/yangchong211/YCCustomText

你的star是我開源的動力,謝謝!

發佈了171 篇原創文章 · 獲贊 73 · 訪問量 11萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章