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 富文本效果图
      1.3 富文本开源库
      • https://github.com/yangchong2…

        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 CREATOR = new Creator() {
                                    @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,具体逻辑如下所示

                            /**
                             * 保存重要信息
                            
                             */
                            @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回退事件
                              
                               */
                              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
                                • 代码思路如下所示

                                  /**
                                   * 插入一张图片
                                  
                                   */
                                  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。

                                    /**
                                    
                                     */
                                    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() {
                                                                    @Override
                                                                    public void subscribe(ObservableEmitter emitter) {
                                                                        try{
                                                                            hte_content.measure(0, 0);
                                                                            List 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() {
                                                                            @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中使用某个图片加载库加载图片,而应该是暴露给外部开发者去加载图片。

                                                                  /**
                                                                  
                                                                   */
                                                                  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代表按红叉按钮删除
                                                                                    
                                                                                     */
                                                                                    private void onImageCloseClick(View view) {
                                                                                        try {
                                                                                            //判断过渡动画是否结束,只能等到结束才可以操作
                                                                                            if (!mTransition.isRunning()) {
                                                                                                disappearingImageIndex = layout.indexOfChild(view);
                                                                                                //删除文件夹里的图片
                                                                                                List 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 {
                                                                                                            private Class clazzE;
                                                                                                            public NormalStyle() {
                                                                                                                //利用反射
                                                                                                                clazzE = (Class) ((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 {
                                                                                                              @Override
                                                                                                              protected ItalicStyleSpan newSpan() {
                                                                                                                  return new ItalicStyleSpan();
                                                                                                              }
                                                                                                          }
                                                                                                          public class UnderlineStyle extends NormalStyle {
                                                                                                              @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案例。
                                                                                                                              • 软键盘及时退出的问题

                                                                                                                                • 当用户输入完成之后,必须手动点击软键盘的收回键,软键盘才收起。如果能通过代码主动将软键盘收起,这对于用户体验来说,是一个极大的提升,思前想后,参考网上的文档,个人比较喜欢的实现方式是通过事件分发机制来解决这个问题。
                                                                                                                                • 解决点击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 buildEditData() {
                                                                                                                                                            List 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 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/578…
                                                                                                                                                                      • Android 端 (图文混排)富文本编辑器的开发(一):https://www.jianshu.com/p/155…
                                                                                                                                                                      • 图文混排富文本文章编辑器实现详解:https://blog.csdn.net/ljzdyh/…

                                                                                                                                                                        富文本开源库:https://github.com/yangchong2…

                                                                                                                                                                        你的star是我开源的动力,谢谢!

                                                                                                                                                                        作者:杨充

                                                                                                                                                                        原文地址:https://segmentfault.com/a/1190000021400746

                                                                                                                                                                        喜欢 0