富文本编辑器RichTextEditor

主要实现图、文、视频、音频混排输入的效果。以下以图文混排举例,视频音频混排原理是一样的,效果如图:


device-2018-01-29-183104.png

实现:

1.自定义ImageView,里面包含媒体文件的本地path、远程url等信息,方便后期使用时的赋值与取值。DataImageView.java:

/**
 * 这只是一个简单的ImageView,可以存放Bitmap和Path等信息
 * @author wangtt
 */
public class DataImageView extends ImageView {
    private String absolutePath;
    private Bitmap bitmap;
    private String type;//video-视频 audio-音频 pic-图片
    public DataImageView(Context context) {
        this(context, null);
    }
    public DataImageView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }
    public DataImageView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }
    public String getAbsolutePath() {
        return absolutePath;
    }
    public void setAbsolutePath(String absolutePath) {
        this.absolutePath = absolutePath;
    }

    public Bitmap getBitmap() {
        return bitmap;
    }

    public void setBitmap(Bitmap bitmap) {
        this.bitmap = bitmap;
    }

    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }
} 

2.创建资源文件,在资源文件里引入刚刚自定义的ImageView,并添加上用于删除媒体文件的按钮。edit_imageview.xml:




    

    

3.创建富文本的编辑器自定义的ScrollView(RichTextEditor),并给外部提供insertImage接口方便外部进行插入图片的操作,添加的图片跟当前光标所在位置有关。主要变量:

  private static final int EDIT_PADDING = 10; // edittext常规padding是10dp
  private static final int EDIT_FIRST_PADDING_TOP = 10; // 第一个EditText的 paddingTop值  
  private int viewTagIndex = 1; // 新生的view都会打一个tag,对每个view来说,这个tag是唯一的。
  private LinearLayout allLayout; // 这个是所有子view的容器,scrollView内部的唯一一个ViewGroup
  private OnKeyListener keyListener; // 所有EditText的软键盘监听器
  private OnClickListener btnListener; // 图片右上角红叉按钮监听器
  private OnFocusChangeListener focusListener; // 所有EditText的焦点监听listener
  private EditText lastFocusEdit; // 最近被聚焦的EditText
  private LayoutTransition mTransitioner; // 只在图片View添加或remove时,触发transition动画
  private int editNormalPadding = 0; //EditText的初始间距
  private int disappearingImageIndex = 0;//被删除的媒体文件的索引
  private OnClickListener picListener;//图片点击事件

初始化工作:

  // 1. 初始化allLayout
  allLayout = new LinearLayout(context);
  allLayout.setOrientation(LinearLayout.VERTICAL);
  allLayout.setBackgroundColor(Color.WHITE);
  LayoutParams layoutParams = new LayoutParams(LayoutParams.MATCH_PARENT,
                LayoutParams.WRAP_CONTENT);
        layoutParams.topMargin = 15;
        layoutParams.leftMargin = 15;
        layoutParams.rightMargin = 15;
        addView(allLayout, layoutParams);
   setupLayoutTransitions();
   initListener();
   LinearLayout.LayoutParams firstEditParam = new LinearLayout.LayoutParams(
                LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
        editNormalPadding = dip2px(EDIT_PADDING);
        EditText firstEdit = createEditText("input here",
                dip2px(EDIT_FIRST_PADDING_TOP));
        allLayout.addView(firstEdit, firstEditParam);
        lastFocusEdit = firstEdit;

监听操作:

private void initListener(){

  //  初始化键盘退格监听,主要用来处理点击回删按钮时,view的一些列合并操作
keyListener = new OnKeyListener() {
   @Override
   public boolean onKey(View v, int keyCode, KeyEvent event) {
       if (event.getAction() == KeyEvent.ACTION_DOWN
                   && event.getKeyCode() == KeyEvent.KEYCODE_DEL) {
              EditText edit = (EditText) v;
              onBackspacePress(edit);
            }
               return false;
      }
  };

 // 图片叉掉处理
   btnListener = new OnClickListener() {
        @Override
      public void onClick(View v) {
           RelativeLayout parentView = (RelativeLayout) v.getParent();
           onImageCloseClick(parentView);
       }
  };

    // 图片点击事件
    picListener = new OnClickListener() {
          @Override
         public void onClick(View v) {
              RelativeLayout parentView = (RelativeLayout) v.getParent();
              DataImageView view = (DataImageView) parentView.findViewById(R.id.edit_imageView);
              Log.e("====> 被点击图片的路径 = ",view.getAbsolutePath());
           }
      };

// 所有EditText的焦点监听listener
    focusListener = new OnFocusChangeListener() {
            @Override
     public void onFocusChange(View v, boolean hasFocus) {
            if (hasFocus) {
                 lastFocusEdit = (EditText) v;
            }
         }
        };
}

具体实现方法:

  • 处理软键盘backSpace回退事件
 /**
     * 处理软键盘backSpace回退事件
     * @param editTxt 光标所在的文本输入框
     */
    private void onBackspacePress(EditText editTxt) {
        int startSelection = editTxt.getSelectionStart();
        // 只有在光标已经顶到文本输入框的最前方,在判定是否删除之前的图片,或两个View合并
        if (startSelection == 0) {
            int editIndex = allLayout.indexOfChild(editTxt);
            View preView = allLayout.getChildAt(editIndex - 1); // 如果editIndex-1<0,
            // 则返回的是null
            if (null != preView) {
                if (preView instanceof RelativeLayout) {
                    // 光标EditText的上一个view对应的是图片
                    onImageCloseClick(preView);
                } else if (preView instanceof EditText) {
                    // 光标EditText的上一个view对应的还是文本框EditText
                    String str1 = editTxt.getText().toString();
                    EditText preEdit = (EditText) preView;
                    String str2 = preEdit.getText().toString();
                    // 合并文本view时,不需要transition动画
                    allLayout.setLayoutTransition(null);
                    allLayout.removeView(editTxt);
                    allLayout.setLayoutTransition(mTransitioner); // 恢复transition动画
                    // 文本合并
                    preEdit.setText(str2 + str1);
                    preEdit.requestFocus();
                    preEdit.setSelection(str2.length(), str2.length());
                    lastFocusEdit = preEdit;
                }
            }
        }
    }
  • 处理图片叉掉的点击事件
  /**
     * 处理图片叉掉的点击事件
     * @param view 整个image对应的relativeLayout view
     * @type 删除类型 0代表backspace删除 1代表按红叉按钮删除
     */
    private void onImageCloseClick(View view) {
        if (!mTransitioner.isRunning()) {
            disappearingImageIndex = allLayout.indexOfChild(view);
            allLayout.removeView(view);
        }
    }
  • 生成文本输入框
 /**
     * 生成文本输入框
     */
    private EditText createEditText(String hint, int paddingTop) {
        EditText editText = (EditText) inflater.inflate(R.layout.edit_item1,
                null);
        editText.setOnKeyListener(keyListener);
        editText.setTag(viewTagIndex++);
        editText.setPadding(editNormalPadding, paddingTop, editNormalPadding, 0);
        editText.setHint(hint);
        editText.setOnFocusChangeListener(focusListener);
        return editText;
    }
  • 生成图片view
   /**
     * 生成图片View
     */
    private RelativeLayout createImageLayout() {
        RelativeLayout layout = (RelativeLayout) inflater.inflate(
                R.layout.edit_imageview, null);
        layout.setTag(viewTagIndex++);
        View pic = layout.findViewById(R.id.edit_imageView);
        pic.setTag(layout.getTag());
        View closeView = layout.findViewById(R.id.image_close);
        closeView.setTag(layout.getTag());
        closeView.setOnClickListener(btnListener);
        pic.setOnClickListener(picListener);
        return layout;
    }
  • 根据图片的绝对路径添加view
    /**
     * 根据绝对路径添加view
     * @param imagePath
     */
    public void insertImage(String imagePath, String url) {
        Bitmap bmp = getScaledBitmap(imagePath, getWidth());
        insertImage(bmp, imagePath, url);
    }

    /**
     * 插入一张图片
     */
    private void insertImage(Bitmap bitmap, String imagePath, String url) {
        String lastEditStr = lastFocusEdit.getText().toString();
        int cursorIndex = lastFocusEdit.getSelectionStart();
        String editStr1 = lastEditStr.substring(0, cursorIndex).trim();
        int lastEditIndex = allLayout.indexOfChild(lastFocusEdit);
        if (lastEditStr.length() == 0 || editStr1.length() == 0) {
            // 如果EditText为空,或者光标已经顶在了editText的最前面,则直接插入图片,并且EditText下移即可
            addImageViewAtIndex(lastEditIndex, bitmap, imagePath, url);
        } else {
            // 如果EditText非空且光标不在最顶端,则需要添加新的imageView和EditText
            lastFocusEdit.setText(editStr1);
            String editStr2 = lastEditStr.substring(cursorIndex).trim();  //subString() 是截取字符串 将剩余字符串赋值给editStr2
            if (allLayout.getChildCount() - 1 == lastEditIndex
                    || editStr2.length() > 0) {
                addEditTextAtIndex(lastEditIndex + 1, editStr2);
            }

            addImageViewAtIndex(lastEditIndex + 1, bitmap, imagePath, url);
            lastFocusEdit.requestFocus();
            lastFocusEdit.setSelection(editStr1.length(), editStr1.length());
        }
        hideKeyBoard();
    } 
  • 在特定的位置插入EditText
    /**
     * 在特定位置插入EditText
     * @param index   位置
     * @param editStr EditText显示的文字
     */
    private void addEditTextAtIndex(final int index, String editStr) {
        EditText editText2 = createEditText("", getResources()
                .getDimensionPixelSize(R.dimen.dp_0));
        editText2.setText(editStr);
        // 请注意此处,EditText添加、或删除不触动Transition动画
        allLayout.setLayoutTransition(null);
        allLayout.addView(editText2, index);
        allLayout.setLayoutTransition(mTransitioner); // remove之后恢复transition动画
    }
  • 在特定位置添加ImageView
 private void addImageViewAtIndex(final int index, Bitmap bmp,
                                     String imagePath, String url) {
        final RelativeLayout imageLayout = createImageLayout();
        DataImageView imageView = (DataImageView) imageLayout
                .findViewById(R.id.edit_imageView);
        imageView.setImageBitmap(bmp);
        imageView.setBitmap(bmp);
        imageView.setAbsolutePath(imagePath);
        imageView.setUrl(url);
        imageView.setType("pic");
        // 调整imageView的高度
        int imageHeight = getWidth() * bmp.getHeight() / bmp.getWidth();
        RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(
                LayoutParams.MATCH_PARENT, imageHeight);
        lp.leftMargin = 15;
        lp.rightMargin = 15;
        lp.bottomMargin = 15;
        lp.topMargin = 15;
        imageView.setLayoutParams(lp);

        // onActivityResult无法触发动画,此处post处理
        allLayout.postDelayed(new Runnable() {
            @Override
            public void run() {
                allLayout.addView(imageLayout, index);
            }
        }, 200);
    }
  • 根据view的宽度,动态缩放bitmap尺寸
    /**
     * 根据view的宽度,动态缩放bitmap尺寸
     * @param width view的宽度
     */
    private Bitmap getScaledBitmap(String filePath, int width) {
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeFile(filePath, options);
        int sampleSize = options.outWidth > width ? options.outWidth / width
                + 1 : 1;
        options.inJustDecodeBounds = false;
        options.inSampleSize = sampleSize;
        return BitmapFactory.decodeFile(filePath, options);
    }
  • 初始化transition动画
 private void setupLayoutTransitions() {
        mTransitioner = new LayoutTransition();
        allLayout.setLayoutTransition(mTransitioner);
        mTransitioner.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();
                }
            }
        });
        mTransitioner.setDuration(300);
    }
  • 对外提供接口,生成编辑数据上传
 public List buildEditData() {
        List dataList = new ArrayList();
        int num = allLayout.getChildCount();
        for (int index = 0; index < num; index++) {
            View itemView = allLayout.getChildAt(index);
            EditData itemData = new EditData();
            if (itemView instanceof EditText) {
                EditText item = (EditText) itemView;
                itemData.inputStr = item.getText().toString();
            } else if (itemView instanceof RelativeLayout) {
                DataImageView item = (DataImageView) itemView
                        .findViewById(R.id.edit_imageView);
                itemData.imagePath = item.getAbsolutePath();
                itemData.type = item.getType();
                itemData.url = item.getUrl();
            }
            dataList.add(itemData);
        }
        return dataList;
    }

  public class EditData {
        public String inputStr;//输入的字符串
        public String imagePath;//媒体文件的本地路径
        public String url;//媒体文件的远程路径
        public String type;//区分媒体文件类型 video-视频 audio-音频 pic-图片
    }

4.使用:

  • 布局文件引入自定义的ScrollView
 
  • Activity中引用
    @Bind(R.id.edit_content)
    RichTextEditor editor;
/***
* 图片上传处理(略)本地图片路径 picPath,图片上传成功后返回的路径picUrl
*/
  editor.insertImage(path, picUrl);
  • 处理结果

/**
 * 获取富文本编辑框里的内容
 * 最终结果以html格式上传
 */
List data = editor.buildEditData();
        for (int i = 0; i < data.size(); i++) {
            String imagUrl = data.get(i).url;//媒体文件的远程路径
            String inputStr = data.get(i).inputStr;//输入的文本
            String type = data.get(i).type;//判断媒体文件的类型
            if (null != inputStr && !"".equals(inputStr))
                content = content + "

" + inputStr + "

"; if (null != imagUrl && !"".equals(imagUrl)) { if (type.equals("pic")) { String html = "

"; content = content + html; } else if (type.equals("video")) { String html = "

"; content = content + html; } else if (type.equals("audio")) { String html = "

"; content = content + html; } } }

你可能感兴趣的:(富文本编辑器RichTextEditor)