基礎概念目錄介紹
- 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 富文本效果圖

1.3 富文本開源庫
- 富文本控件支援動态插入文字,圖檔等圖文混排内容。圖檔可以支援本地圖檔,也支援插入網絡連結圖檔;
- 富文本又兩種狀态:編輯狀态 + 預覽狀态 。兩種狀态可以互相進行切換;
- 富文本在編輯狀态,可以同時選擇插入超過一張以上的多張圖檔,并且可以動态設定圖檔之間的top間距;
- 在編輯狀态,支援利用光标删除文字内容,同時也支援用光标删除圖檔;
- 在編輯狀态,插入圖檔後,圖檔的寬度填充滿手機螢幕的寬度,然後高度可以動态設定,圖檔是劇中裁剪顯示;
- 在編輯狀态,插入圖檔後,如果本地圖檔過大,要求對圖檔進行品質壓縮,大小壓縮;
- 在編輯狀态,插入多張圖檔時,添加插入過渡動畫,避免顯示圖檔生硬。結束後,光标移到插入圖檔中的最後一行顯示;
- 編輯狀态中,圖檔點選暴露點選事件接口,可以在4個邊角位置動态設定一個删除圖檔的功能,點選删除按鈕則删除圖檔;
- 連續插入多張圖檔時,比如順序1,2,3,注意避免出現圖檔插入順序混亂的問題(異步插入多張圖檔可能出現順序錯亂問題);
- 在編輯富文本狀态的時候,連續多張圖檔之間插入輸入框,友善在圖檔間輸入文本内容;
- 在編輯狀态中,可以設定文字大小和顔色,同時做好拓展需求,後期可能添加文本加粗,下劃線,插入超連結,對齊方式等功能;
- 編輯狀态,連續插入多張圖檔,如果想在圖檔中間插入文字内容,則需要靠譜在圖檔之間預留編輯文本控件,友善操作;
- 支援對文字選中的内容進行設定加粗,添加下劃線,改變顔色,設定對齊方式等等;
- 關于富文本字數統計,由于富文本中包括文字和圖檔,是以圖檔和文字數量統計分開。參考易車是:共n個文字,共n個圖檔顯示
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内容輸出
- 支援設定富文本的文字大小,行間距,圖檔和文本間距,以及插入圖檔的寬和高的屬性
- 圖檔支援點選預覽,支援點選叉号控件去除圖檔,暴露給外部開發者調用。同時加載圖檔的邏輯也是暴露給外部開發者,充分解耦
- 對于自定義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(); }
- 想了一下,當富文本處于編輯的狀态,利用光标可以進行删除插入點之前的字元。删除的時候,根據光标的位置,如果光标遇到是圖檔,則可以用光标删除圖檔;如果光标遇到是文字,則可以用光标删除文字。
- 更詳細的來說,監聽删除鍵的點選的邏輯需要注意,當光标在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(); } }
- 當點選插入圖檔的時候,需要思考兩個問題。第一個是在那個位置插入圖檔,是以需要定位到這個位置;第二個是插入圖檔後,什麼時候折行操作。
- 對于上面兩個問題,這個位置可以取光标所在的位置,但是對于一個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(); } }
- 前面已經提到了,如果一個富文本是:文字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(); } }
- 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
- 在這種情況下 仍需要進行左右側邊界判斷進行删除。這個具體可以看代碼邏輯。
- 這裡僅僅是對字型加粗進行介紹,其實設定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); } } }
- 富文本當然支援插入多張圖檔,那麼插入多張圖檔是如何操作呢。插入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); } });
- 插入圖檔有兩種情況,一種是本地圖檔,一種是網絡圖檔。由于富文本中對插入圖檔的寬高有限制,即可以動态設定圖檔的高度,這就要求請求網絡圖檔後,需要對圖檔進行處理。
- 首先看一下插入圖檔的代碼,在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 { //如果是本地圖檔 } } });
- 加載一個本地的大圖檔或者網絡圖檔,從加載到設定到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。
- 當富文本處于編輯狀态時,點選删除圖檔是可以删除圖檔的,對于删除的邏輯,封裝的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(); } }
- 為什麼要添加插入圖檔的過渡動畫
- 當向一個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時就會觸發動畫。
- 向一個ViewGroup添加控件或者移除控件,這兩種效果的過程是應對應于控件的顯示、控件添加時其他控件的位置移動、控件的消失、控件移除時其他控件的位置移動等四種動畫效果。這些動畫效果在LayoutTransition中,由以下四個關鍵字做出了相關聲明:
- 具體初始化動畫的代碼如下所示:
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動畫。這裡就不貼分析源碼的思路呢!
- 編輯狀态時,由于圖檔有空能比較大,在顯示在富文本的時候,會裁剪局中顯示,也就是圖檔會顯示不全。那麼後期如果是想添加點選圖檔檢視,則需要暴露給開發者監聽事件,需要考慮到後期拓展性,代碼如下所示:
- 這樣做的目的是是暴露給外部開發者調用,點選圖檔的操作隻需要傳遞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()); } } } };
- 針對設定文字加粗,下劃線,删除線等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(); } }
- 在文字中添加圖檔比較特殊,是以這裡單獨拿出來說一下。在文字内容中間插入圖檔,則需要分割字元串,分割成兩個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);
- 軟鍵盤彈出的時機
- 如果不做任何處理,系統預設的是,進入頁面,第一個輸入框自動擷取焦點軟鍵盤自動彈出,這種使用者互動方式,往往不是産品想要的,往往會提出以下優化需求:
- 需求1:editText擷取焦點,但是不彈出軟鍵盤(也就是說光标顯示第一個輸入框,不主動彈軟鍵盤)
- 在第一個輸入框的最直接父布局加入:android:focusable="true";android:focusableInTouchMode="true"
- (效果:軟鍵盤不彈出,光标不顯示,其他輸入框也不擷取焦點,ps非直接父布局沒有效果)
- android:windowSoftInputMode="stateAlwaysHidden"
- (效果:軟鍵盤不彈出,光标顯示在第一個輸入框中)
- 在第一個輸入框的最直接父布局加入:android:focusable="true";android:focusableInTouchMode="true"
- 需求2:editText不擷取焦點,當然軟鍵盤不會主動彈出(光标也不顯示)
-
- 在父布局最頂部添加一個高度為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);
- 由于富文本中,使用者會輸入很多的内容,當關閉頁面時候,需要提醒使用者是否儲存輸入内容。同時,切換到背景的時候,需要注意儲存輸入内容,避免長時間切換背景程序記憶體吃緊,在回到前台輸入的内容沒有呢,查閱了汽車之家,易車等app等手機上的富文本編輯器,都會有這個細節點的優化。
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不熟悉,也不太好處理。
- 參考了易車釋出文章,送出資料到伺服器,針對富文本,是把它拼接成對象。将文字,圖檔按照富文本的順序拼接成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傳回給用戶端,然後解析填充到富文本中。這個就沒什麼好說的……
- 大多數開發者會采用的方式:
- 先在編輯器裡顯示本地圖檔,等待使用者編輯完成再上傳全部圖檔,然後用上傳傳回的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