整個界面的要求css
根據富文本做出如下分析html
使用原生控件多焦點問題分析前端
若是一個富文本是:文字1+圖片1+文字2+文字3+圖片3+圖片4;那麼使用LinearLayout包含多個EditText實現的難點:java
對於自定義View,若是頁面出現異常致使自定義View異常退出,則固然但願保存一些重要的信息。自定義保存狀態類,繼承BaseSavedState,代碼以下所示android
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,具體邏輯以下所示git
/** * 保存重要信息
*/ @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(); } ```
建立一個鍵盤退格監聽事件,代碼以下所示:github
// 初始化鍵盤退格監聽,主要用來處理點擊回刪按鈕時,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; } };
而後針對退格刪除,分爲兩種狀況,第一種是刪除圖片,第二種是刪除文字內容。具體代碼以下所示:web
/** * 處理軟鍵盤backSpace回退事件
*/ 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輸入文本,插入圖片這個位置能夠分多種狀況:json
代碼思路以下所示數組
/** * 插入一張圖片
*/ 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(); } } ```
既然能夠記錄最後焦點輸入文本,那麼如何監聽當前的輸入控件呢,這就用到了OnFocusChangeListener,這個又是在哪裏用到,具體以下面所示。要先setOnFocusChangeListener(focusListener) 再 requestFocus。
/**
*/ 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 的分類介紹
字符外觀,這種類型修改字符的外形可是不影響字符的測量,會觸發文本從新繪製可是不觸發從新佈局。
字符大小布局,這種類型Span會更改文本的大小和佈局,會觸發文本的從新測量繪製
影響段落級別,這種類型Span 在段落級別起做用,更改文本塊在段落級別的外觀,修改對齊方式,邊距等。
當前選中區域不存在 bold 樣式 這裏咱們選中BB。兩種狀況
當前選中區域存在了Bold 樣式 選中 ABBC。四種狀況:
接下來逐步分解,而後處理span的邏輯順序以下所示
何時取消span呢,這個邏輯是比較複雜的,具體看看下面的舉例。
用戶能夠隨意的刪除文本,在刪除過程當中可能會出現以下的狀況:
這裏僅僅是對字體加粗進行介紹,其實設置span能夠找到規律。多個span樣式,考慮到後期的拓展性,確定要進行封裝和抽象,具體該如何處理呢?
/** * 修改加粗樣式 */ 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); }
/** * 修改加粗樣式 */ 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中使用某個圖片加載庫加載圖片,而應該是暴露給外部開發者去加載圖片。
/**
*/ 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。
加載圖片的內存都去哪裏呢?
爲什麼容易OOM?
如何對圖片進行壓縮?
具體設置圖片壓縮的代碼以下所示
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這個參數是幹什麼的?
爲什麼設置兩次inJustDecodeBounds屬性?
當富文本處於編輯狀態時,點擊刪除圖片是能夠刪除圖片的,對於刪除的邏輯,封裝的lib能夠給開發者暴露一個刪除的監聽事件。注意刪除圖片有兩種操做:第一種是利用光標刪除,第二種是點擊觸發刪除。刪除圖片後,不只僅是要刪除圖片數據,並且還要刪除圖片ImageView控件。
/** * 處理圖片上刪除的點擊事件 * 刪除類型 0表明backspace刪除 1表明按紅叉按鈕刪除
*/ 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(); } } ```
爲何要添加插入圖片的過渡動畫
LayoutTransition簡單介紹
如何運用到插入或者刪除圖片場景中
向一個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); } }
動畫執行前後的順序
編輯狀態時,因爲圖片有空能比較大,在顯示在富文本的時候,會裁剪局中顯示,也就是圖片會顯示不全。那麼後期若是是想添加點擊圖片查看,則須要暴露給開發者監聽事件,須要考慮到後期拓展性,代碼以下所示:
// 圖片處理 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
*/ 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中間插入圖片,那麼這個光標又定位在何處呢?
//獲取光標所在位置 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"
android:windowSoftInputMode="stateAlwaysHidden"
需求2:editText不獲取焦點,固然軟鍵盤不會主動彈出(光標也不顯示)
在第一個輸入框的最直接父佈局加入:android:focusable="true";android:focusableInTouchMode="true"
軟鍵盤遮擋界面的問題
stateUnspecified-未指定狀態:軟件默認採用的交互方式,系統會根據當前界面自動調整軟鍵盤的顯示模式。 stateUnchanged-不改變狀態:當前界面軟鍵盤狀態由上個界面軟鍵盤的狀態決定; stateHidden-隱藏狀態:進入頁面,不管是否有輸入需求,軟鍵盤是隱藏的,可是若是跳轉到下一個頁面軟鍵盤是展現的,回到這個頁面,軟鍵盤可能也是展現的,這個屬性區別下個屬性。 stateAlwaysHidden-老是隱藏狀態:當設置該狀態時,軟鍵盤老是被隱藏,和stateHidden不一樣的是,當咱們跳轉到下個界面,若是下個頁面的軟鍵盤是顯示的,而咱們再次回來的時候,軟鍵盤就會隱藏起來。 stateVisible-可見狀態:當設置爲這個狀態時,軟鍵盤老是可見的,即便在界面上沒有輸入框的狀況下也能夠強制彈出來出來。 stateAlwaysVisible-老是顯示狀態:當設置爲這個狀態時,軟鍵盤老是可見的,和stateVisible不一樣的是,當咱們跳轉到下個界面,若是下個頁面軟鍵盤是隱藏的,而咱們再次回來的時候,軟鍵盤就會顯示出來。 adjustUnspecified-未指定模式:設置軟鍵盤與軟件的顯示內容之間的顯示關係。當你跟咱們沒有設置這個值的時候,這個選項也是默認的設置模式。在這中狀況下,系統會根據界面選擇不一樣的模式。 adjustResize-調整模式:當軟鍵盤顯示的時候,當前界面會自動重繪,會被壓縮,軟鍵盤消失以後,界面恢復正常(正常佈局,非scrollView父佈局);當父佈局是scrollView的時候,軟鍵盤彈出,會將佈局頂起(保證輸入框不被遮擋),不壓縮,並且能夠軟鍵盤不消失的狀況下,手動滑出被遮擋的佈局; adjustPan-默認模式:軟鍵盤彈出,軟鍵盤會遮擋屏幕下半部分佈局,當輸入框在屏幕下方佈局,軟鍵盤彈起,會自動將當前佈局頂起,保證,軟鍵盤不遮擋當前輸入框(正常佈局,非scrollView父佈局)。當父佈局是scrollView的時候,感受沒啥變化,仍是自定將佈局頂起,輸入框不被遮擋,不能夠手動滑出被遮擋的佈局(白瞎了scrollView);
<activity android:name=".NewArticleActivity" android:windowSoftInputMode="adjustResize|stateHidden"/>
軟鍵盤及時退出的問題
解決點擊EditText彈出收起鍵盤時出現的黑屏閃現現象
View rootView = hte_content.getRootView(); rootView.setBackgroundColor(Color.WHITE);
客戶端生成html片斷到服務器
轉化成html
最後想說的是
服務器返回html給客戶端加載
加載html文件流暢問題
用原生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); //提交服務器省略
大多數開發者會採用的方式:
這樣會遇到不少問題:
解決辦法探討:
這種場景很容易想到:
參考博客