基礎概念目錄介紹
- 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 富文本開源庫
- 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時就會觸發動畫。
- 向一個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動畫。這裏就不貼分析源碼的思路呢!
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”
- (效果:軟鍵盤不彈出,光標顯示在第一個輸入框中)
- 在第一個輸入框的最直接父佈局加入:android:focusable=“true”;android:focusableInTouchMode=“true”
- 需求2:editText不獲取焦點,當然軟鍵盤不會主動彈出(光標也不顯示)
- 在第一個輸入框的最直接父佈局加入:android:focusable=“true”;android:focusableInTouchMode=“true”
- (效果:軟鍵盤不彈出,光標不顯示,其他輸入框也不獲取焦點,ps非直接父佈局沒有效果)
- 在父佈局最頂部添加一個高度爲0的EditText,搶了焦點但不展示;
- 在第一個輸入框的最直接父佈局加入:android:focusable=“true”;android:focusableInTouchMode=“true”
- 軟鍵盤遮擋界面的問題
- 當界面中有輸入框,需要彈起軟鍵盤輸入信息的時候,軟鍵盤可能遮擋部分佈局,更有甚者,當前輸入框如果在屏幕下方,軟鍵盤也會直接遮擋輸入框,這種情況對用戶體驗是相當不友好的,所以要根據具體的情況作出相應的處理。
- 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