Android 端 (图文混排)富文本编辑器的开发(一)

前段时间 需要做 富文本编辑笔记功能,要求能够插入图片、待办事项、无序列表、引用,能够修改字体大小、段落对齐方式、粗体、斜体、下划线、中划线。经过一段时间的努力完成了功能,现在对开发过程进行记录。实现效果如下图:

IMG_20190428_105644.jpg
image

项目地址RichEditor
编辑器涉及到的功能点较多,所以将是一系列的文章。文章内容按照以下的关键点进行展开:

  1. 页面组成 分析(本篇)

  2. 粗体 斜体 下划线 中划线

  3. 字体大小 对齐方式(左对齐 居中 右对齐)

  4. 列表项 (多级列表)实现 以及样式的取消

  5. 引用项 实现以及样式的取消

  6. 行间距问题的处理(行高)

  7. 待办事项如何实现

  8. 图片如何插入 (todo ) 插入样式合并

  9. 图片等控件删除键 点击事件操作

  10. 各个样式 删除键与回退键 的处理

  11. 上传格式,生成HTML 样式片段

  12. Html 片段的解析 dom 解析 span的解析 (系统代码的修改)

  13. 关于长图生成

这篇文章先进行页面组成分析

界面构成

当时看到这个界面的时候一脸懵逼,整个界面的要求

  • 整体界面可滚动
  • 内容可编辑可以插入文字、图片、视频等
  • 图片、视频提供按钮操作。
  • 软键盘删除键可删除图片
  • 可插入待办事项,前方 CheckBox 可点击

根据界面要求作出以下分析

  1. 可插入图片、视频 界面不能用一个 EditText 来做,需要使用LinearLayout添加不同的控件
  2. 界面可滑动最外层使用ScrollView
  3. 可插入待办事项,单个编辑控件使用LinearLayout包裹
  4. 图片区域 包含可操作按钮,使用RelativeLayout进行包裹

最终实现的 布局结构如下图:

image

强烈建议在测试编辑器的时候 打开 开发者模式的显示布局边界

构建界面

经过以上分析,界面是由多个输入区域拼接而成,暂且把输入区域 称为 InputWeight

图片区域称为ImageWeight 可转为待办事项区域称为 TodoWeight

使用LinearLayout包含多个 InputWeight实现的难点:

  1. 记录当前的焦点区域
  2. 输入区域的删除键处理
  3. TodoWeight 输入的中间位置插入ImageWeight 样式的合并

输入区域

该部分会贴出 各个输入区域的 布局和部分代码,先了解整个布局的组成和一些基本的操作

最外层控件

ScrollView 内容区域为 标题 EditText 和正文编辑器

 <ScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_above="@+id/label_area"
        android:layout_below="@id/toolbar"
        android:fillViewport="true">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical">

            <EditText
                android:id="@+id/edt_title"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginTop="10dp"
                android:background="@null"
                android:hint="@string/input_title"
                android:maxLength="80"
                android:paddingLeft="25dp"
                android:paddingRight="25dp"
                android:textColor="@color/text_333"
                android:textColorHint="@color/text_999"
                android:textSize="22dp"
                android:textStyle="bold" />


            <com.scwen.editor.RichEditer
                android:id="@+id/editor_content"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginLeft="20dp"
                android:layout_marginTop="10dp"
                android:layout_marginRight="20dp"
                android:layout_marginBottom="20dp"
                android:paddingLeft="5dp"
                android:paddingRight="5dp"
                android:paddingBottom="5dp"></com.scwen.editor.RichEditer>


        </LinearLayout>
    </ScrollView>

布局很简单,其中 RichEditer是编辑器封装框架,封装了编辑区域

InputWeight
/**
 * Created by scwen on 2019/4/29.
 * QQ :811733738
 * 作用:输入控件基类
 */
public abstract class InputWeight {

    protected Context mContext;

    protected LayoutInflater mInflater;

    protected View mContentView;

    /**
     * 是否显示 待办事项
     */
    protected boolean isTodo;

    public boolean isTodo() {
        return isTodo;
    }

    public void setTodo(boolean todo) {
        isTodo = todo;
    }


    public InputWeight(Context context, ViewGroup parent) {
        this.mContext = context;
        this.mInflater = LayoutInflater.from(mContext);
        getView(parent);
    }


    public void getView(ViewGroup parent) {
        mContentView = mInflater.inflate(provideResId(), parent, false);
        initView();
    }

    public View getContentView() {
        return mContentView;
    }

    /**
     * 初始化 View
     */
    protected abstract void initView();

    /**
     * 输入区域内容转Html
     *
     * @return
     */
    public abstract String getHtml();

    abstract @LayoutRes
    int provideResId();

    public void showTodo() {
    }

    public void hideTodo() {

    }

    public void checkTodo() {

    }

    public void unCheckTodo() {

    }

    /**
     * 获取输入区域的 EditText
     *
     * @return
     */
    abstract public EditText getEditText();

    /**
     * 获取输入的文本
     *
     * @return
     */
    abstract public String getContent();

}


TodoWeight

包含待办事项的输入区域:

/**
 * Created by scwen on 2019/4/18.
 * QQ :811733738
 * 作用: 包含 待办事项的 输入区域
 */
public class TodoWeight extends InputWeight {


    private CheckBox cb_todo_state;
    private EditText et_input;

    public TodoWeight(Context context, ViewGroup parent) {
        super(context, parent);
     
    }

    @Override
    protected void initView() {
        cb_todo_state = mContentView.findViewById(R.id.cb_todo_state);
        et_input = mContentView.findViewById(R.id.et_input);
        Editable editable = et_input.getText();
        cb_todo_state.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
            @Override
            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                if (isChecked) {
                    //选中  表示已完成
                    Editable text = et_input.getText();
                    et_input.setTextColor(Color.parseColor("#cccccc"));
                } else {
                    uncheckStyle();
                }
            }
        });

    }

    @Override
    public void checkTodo() {
        cb_todo_state.setChecked(true);
    }

    @Override
    public void unCheckTodo() {
        uncheckStyle();
    }

    private void uncheckStyle() {
        //反选  表示未完成
        Editable text = et_input.getText();
        et_input.setTextColor(Color.parseColor("#333333"));
    }

    @Override
    public String getHtml() {
        if (TextUtils.isEmpty((et_input.getText()))) {
            return "";
        }
        return "";
    }

    @Override
    public String getContent() {
        String content = et_input.getText().toString().trim().replaceAll("\n", "");
        return content;
    }

    public String provideCheckBox() {
        String checked = "";
        if (cb_todo_state.isChecked()) {
            checked = "checked";
        }
        String regix = "<p><form><input type=\"checkbox\" disabled %s>%s</form></p>";
        return String.format(regix, checked, et_input.getText().toString());
    }


    @Override
    int provideResId() {
        return R.layout.note_input_todo;
    }


    @Override
    public EditText getEditText() {
        return et_input;
    }


    public boolean hasDone() {
        return cb_todo_state.isChecked();
    }

    @Override
    public void showTodo() {
        et_input.setHint("待办事项");
        cb_todo_state.setVisibility(View.VISIBLE);
        //执行样式清除
        setTodo(true);
    }

    @Override
    public void hideTodo() {
        cb_todo_state.setVisibility(View.GONE);
        et_input.setHint("");
        uncheckStyle();
        setTodo(false);
    }
}

内容比较简单,初始化控件、添加CheckBox 点击监听,提供了切换CheckBox显示方法

布局文件

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <CheckBox
        android:id="@+id/cb_todo_state"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:checked="true"
        android:padding="5dp"
        android:visibility="gone" />


    <EditText
        android:id="@+id/et_input"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@null"
        android:cursorVisible="true"
        android:textColor="#333333"
        android:textCursorDrawable="@drawable/cursor_drawable"
        android:textSize="16dp" />

</LinearLayout>

LinearLayout 包裹CheckBox 和 EditText ,CheckBox 默认隐藏,当切换为 待办事项时,显示CheckBox

ImageWeight
/**
 * Created by scwen on 2019/4/18.
 * QQ :811733738
 * 作用: 图片区域
 */
public class ImageWeight extends InputWeight implements View.OnClickListener {


    private ImageView iv_input_image;  //图片

    private LinearLayout ll_bottom_tools;  //底部控件
    private RelativeLayout rl_delete;  //删除
    private RelativeLayout rl_replace; //替换
    private RelativeLayout rl_full; //全屏

    private String path;  //图片 手机路径

    private String shortPath; //图片上传服务器 短路径

    public String getShortPath() {
        return shortPath == null ? "" : shortPath;
    }

    public void setShortPath(String shortPath) {
        this.shortPath = shortPath;
    }

    public String getPath() {
        return path == null ? "" : path;
    }

    public void setPath(String path) {
        this.path = path;
    }

    public void replacePath(String path) {
        this.path = path;
        loadImage(path);
    }

    public ImageWeight(Context context, ViewGroup parent, String path) {
        super(context, parent);
        this.path = path;
        loadImage(path);
    }

    public void loadImage(String path) {

        //Glide 加载图片
        RequestOptions options = new RequestOptions();
        options.placeholder(R.drawable.big_image_placeholder)
                .sizeMultiplier(0.5f)
                .diskCacheStrategy(DiskCacheStrategy.ALL)
                .transform(new EditerTranform(mContext, 45));

        Glide.with(mContext)
                .load(path)
                .apply(options)
                .listener(new RequestListener<Drawable>() {
                    @Override
                    public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<Drawable> target, boolean isFirstResource) {
                        return false;
                    }

                    @Override
                    public boolean onResourceReady(Drawable resource, Object model, Target<Drawable> target, DataSource dataSource, boolean isFirstResource) {
                        //记载图片完成后  设置控件的 高度
                        ViewGroup.LayoutParams layoutParams = mContentView.getLayoutParams();
                        int minimumHeight = resource.getMinimumHeight();
                        layoutParams.height = minimumHeight;
                        return false;
                    }
                })
                .into(iv_input_image);
    }


    private ImageActionListener mImageActionListener;

    public void setImageActionListener(ImageActionListener imageActionListener) {
        mImageActionListener = imageActionListener;
    }

    @Override
    public String getHtml() {
        return provideHtml(shortPath);
    }

    public String provideHtml(String path) {
        return String.format("<div class=\"image\"><img src=\"%s\"></img></div>", path);
    }

    @Override
    int provideResId() {
        return R.layout.note_input_image;
    }

    @Override
    public String getContent() {
        return "";
    }


    @Override
    public EditText getEditText() {
        return null;
    }

    private void initListener() {
        iv_input_image.setOnClickListener(this);
        rl_delete.setOnClickListener(this);
        rl_replace.setOnClickListener(this);
        rl_full.setOnClickListener(this);
    }

    @Override
    public void initView() {
        iv_input_image = mContentView.findViewById(R.id.iv_input_image);
        ll_bottom_tools = mContentView.findViewById(R.id.ll_bottom_tools);
        rl_delete = mContentView.findViewById(R.id.rl_delete);
        rl_replace = mContentView.findViewById(R.id.rl_replace);
        rl_full = mContentView.findViewById(R.id.rl_full);
        initListener();
    }

    @Override
    public void onClick(View v) {
        if (v.getId() == R.id.iv_input_image) {
            //点击图片 显示下方 按钮区域
            ll_bottom_tools.setVisibility(ll_bottom_tools.getVisibility() == View.VISIBLE ? View.GONE : View.VISIBLE);
        } else if (v.getId() == R.id.rl_delete) {
            //触发 删除图片监听
            if (mImageActionListener != null) {
                mImageActionListener.onAction(ImageActionListener.ACT_DELETE, this);
            }
        } else if (v.getId() == R.id.rl_replace) {
            //触发替换图片监听
            if (mImageActionListener != null) {
                mImageActionListener.onAction(ImageActionListener.ACT_REPLACE, this);
            }
        } else if (v.getId() == R.id.rl_full) {
            //触发预览图片监听
            if (mImageActionListener != null) {
                mImageActionListener.onAction(ImageActionListener.ACT_PREVIEW, this);
            }
        }

    }
}

ImageActionListener


/**
 * Created by scwen on 2019/4/23.
 * QQ :811733738
 * 作用:图片操作监听
 */
public interface ImageActionListener {
      /**
       * 删除图片
       */
      int  ACT_DELETE=0;
      /**
       * 替换图片
       */
      int  ACT_REPLACE=1;
      /**
       * 预览图片
       */
      int  ACT_PREVIEW=2;

      void onAction(int action, ImageWeight imageWeight);

}

布局文件

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    >


    <ImageView
        android:id="@+id/iv_input_image"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />


    <LinearLayout
        android:id="@+id/ll_bottom_tools"
        android:layout_width="match_parent"
        android:layout_height="44dp"
        android:layout_alignParentBottom="true"
        android:background="#66000000"
        android:orientation="horizontal"
        android:visibility="gone">

        <RelativeLayout
            android:id="@+id/rl_delete"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1">

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:layout_centerInParent="true"
                android:drawableLeft="@drawable/ic_delete_black_24dp"
                android:drawablePadding="5dp"
                android:gravity="center"
                android:text="@string/delete"
                android:textColor="#fff"
                android:textSize="12dp" />
        </RelativeLayout>

        <RelativeLayout
            android:id="@+id/rl_replace"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1">

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:layout_centerInParent="true"
                android:drawableLeft="@drawable/ic_image_white_24dp"
                android:drawablePadding="5dp"
                android:gravity="center"
                android:text="@string/replace"
                android:textColor="#fff"
                android:textSize="12dp" />
        </RelativeLayout>

        <RelativeLayout
            android:id="@+id/rl_full"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1">

            <ImageView
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:layout_centerInParent="true"
                android:gravity="center"
                android:src="@drawable/ic_fullscreen_black_24dp"
                android:text="@string/delete" />
        </RelativeLayout>
    </LinearLayout>
</RelativeLayout>

RelativeLayout包裹内容区域,ImageView 控件自适应高度,底部包含3个点击区域

创建控件测试

创建Editor1控件,继承自LinearLayout,并且设置 当前的方向为VERTICAL

提供创建ImageWeightTodoWeight方法添加到控件中

public class Editor1 extends LinearLayout {

    /**
     * 输入控件的集合
     */
    private List<InputWeight> inputWeights = new ArrayList<>();


    public Editor1(Context context) {
        this(context, null);
    }

    public Editor1(Context context, @Nullable AttributeSet attrs) {
        this(context, null, 0);
    }

    public Editor1(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }

    /**
     * 初始胡
     *
     * @param context
     */
    private void init(Context context) {
        //设置当前控件的方向为  VERTICAL
        setOrientation(VERTICAL);
        //默认需要创建 TodoWeight
        TodoWeight todoWeight = addTodoWeight();
        //默认第一个控件需要 Hint
        todoWeight.getEditText().setHint(R.string.input_content);
    }

    /**
     * 添加 EditText 控件
     *
     * @return
     */
    public TodoWeight addTodoWeight() {
        TodoWeight todoWeight = new TodoWeight(getContext(), this, null);
        inputWeights.add(todoWeight);
        addView(todoWeight.getContentView());
        return todoWeight;
    }

    /**
     * 添加Image 控件
     * @return
     */
    public ImageWeight addImageWeight() {
        ImageWeight imageWeight = new ImageWeight(getContext(), this, null);
        inputWeights.add(imageWeight);
        addView(imageWeight.getContentView());
        return imageWeight;
    }


}

Activity测试

Activity 中创建两个Button 测试添加输入区域

btn_add_edit.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        TodoWeight todoWeight = editor1.addTodoWeight();
        //测试显示 CheckBox
        todoWeight.showTodo();

    }
});

btn_add_image.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        editor1.addImageWeight();
    }
});

测试界面效果如下:

image

焦点EditText记录

当前的编辑器已经添加了多个InputWeight,现在的问题在于需要记录当前编辑的EditText,在应用样式的时候定位到输入的控件,在编辑器中添加如下变量:

   private EditText lastFocusEdit;  //当前正在编辑的EditText

如何监听当前的输入控件呢,这就用到了OnFocusChangeListener

  private OnFocusChangeListener focusListener; // 所有EditText的焦点监听listener

在init方法中,创建对象

 focusListener = new OnFocusChangeListener() {

            @Override
            public void onFocusChange(View v, boolean hasFocus) {
                if (hasFocus) {
                    lastFocusEdit = (EditText) v;
                }
            }
        };

改造刚刚的 addTodoWeight方法

/**
     * 添加 EditText 控件
     *
     * @return
     */
    public TodoWeight addTodoWeight() {
        TodoWeight todoWeight = new TodoWeight(getContext(), this, null);
        inputWeights.add(todoWeight);
        //
        todoWeight.getEditText().setOnFocusChangeListener(focusListener);
        todoWeight.getEditText().requestFocus();
        addView(todoWeight.getContentView());
        return todoWeight;
    }

注意上面的代码 ,一定要先setOnFocusChangeListener(focusListener)requestFocus

图片插入

上方测试的插入图片功能只是最简单的在最末尾加入图片控件,现在要增加在输入的文本中间插入功能

分为四种情况

  • 当前焦点EditText 内容为空
  • 当前输入光标在EditText已输入内容最前端
  • 当前输入光标在EditText已输入内容最末端
  • 当前输入光标在EditText已输入内容中间

判断四种情况代码:

 public ImageWeight insertImage() {
        //lastFocusEdit获取焦点的EditText
        Editable preContent = lastFocusEdit.getText();

        //获取控件位置
        int lastEditIndex = indexOfChild((View) lastFocusEdit.getParent());

        ImageWeight imageWeight = null;

        if (preContent.length() == 0) {
            //当前焦点EditText 内容为空
        } else {
            //获取光标所在位置
            int cursorIndex = lastFocusEdit.getSelectionStart();
            //获取光标前面的 内容
            CharSequence start = preContent.subSequence(0, cursorIndex);
            //获取光标后面内容
            CharSequence end = preContent.subSequence(cursorIndex, preContent.length());

            if (start.length() == 0) {
                //如果光标已经顶在了editText的最前面

            } else if (end.length() == 0) {
                // 如果光标已经顶在了editText的最末端

            } else {
                //如果光标已经顶在了editText的最中间,
              
            }
        }

        return imageWeight;
    }

针对以上四种情况的处理

  • 直接在EditText下方插入图片,插入新的EditText
  • 直接在EditText下方插入图片,并且插入新的EditText
  • 则需要添加新的imageView和EditText
  • 则需要分割字符串,分割成两个EditText,并在两个EditText中间插入图片

需要在指定位置插入TodoWeight 和ImageWeight,增加addTodoWeightAtIndexaddImageWeightAtIndex方法

最终改造的代码如下:

 public ImageWeight insertImage(String path) {
        //lastFocusEdit获取焦点的EditText
        Editable preContent = lastFocusEdit.getText();

        //获取控件位置
        int lastEditIndex = indexOfChild((View) lastFocusEdit.getParent());

        ImageWeight imageWeight = null;

        if (preContent.length() == 0) {
            //如果当前获取焦点的EditText为空,直接在EditText下方插入图片,并且插入空的EditText
            addTodoWeightAtIndex(lastEditIndex + 1, "");
            imageWeight = addImageWeightAtIndex(lastEditIndex + 1, path);
        } else {
            //获取光标所在位置
            int cursorIndex = lastFocusEdit.getSelectionStart();
            //获取光标前面的 内容
            CharSequence start = preContent.subSequence(0, cursorIndex);
            //获取光标后面内容
            CharSequence end = preContent.subSequence(cursorIndex, preContent.length());

            if (start.length() == 0) {
                //如果光标已经顶在了editText的最前面,则直接插入图片,并且EditText下移即可
                imageWeight = addImageWeightAtIndex(lastEditIndex, path);
                //同时插入一个空的EditText,防止插入多张图片无法写文字
                addTodoWeightAtIndex(lastEditIndex + 1, "");
            } else if (end.length() == 0) {
                // 如果光标已经顶在了editText的最末端,则需要添加新的imageView和EditText
                addTodoWeightAtIndex(lastEditIndex + 1, "");
                imageWeight = addImageWeightAtIndex(lastEditIndex + 1, path);
            } else {
                //如果光标已经顶在了editText的最中间,则需要分割字符串,分割成两个EditText,并在两个EditText中间插入图片
                //把光标前面的字符串保留,设置给当前获得焦点的EditText(此为分割出来的第一个EditText)
                lastFocusEdit.setText(start);
                //把光标后面的字符串放在新创建的EditText中(此为分割出来的第二个EditText)
                addTodoWeightAtIndex(lastEditIndex + 1, end);
                //在第二个EditText的位置插入一个空的EditText,以便连续插入多张图片时,有空间写文字,第二个EditText下移
                addTodoWeightAtIndex(lastEditIndex + 1, "");
                //在空的EditText的位置插入图片布局,空的EditText下移
                imageWeight = addImageWeightAtIndex(lastEditIndex + 1, path);
            }
        }

        return imageWeight;
    }

    public TodoWeight addTodoWeightAtIndex(int index, CharSequence sequence) {
        TodoWeight todoWeight = new TodoWeight(getContext(), this, focusListener);
        inputWeights.add(index, todoWeight);
        //设置 显示的内容
        if (sequence != null && sequence.length() > 0) {
            todoWeight.getEditText().setText(sequence);
        }
        todoWeight.getEditText().setOnFocusChangeListener(focusListener);
        addView(todoWeight.getContentView(), index);
        
        lastFocusEdit = todoWeight.getEditText();
        lastFocusEdit.requestFocus();
        lastFocusEdit.setSelection(sequence.length(), sequence.length());
        return todoWeight;
    }

    public ImageWeight addImageWeightAtIndex(int index, String path) {
        ImageWeight imageWeight = new ImageWeight(getContext(), this, path);
        inputWeights.add(index, imageWeight);
        addView(imageWeight.getContentView(), index);
        return imageWeight;
    }

删除键处理

到此已经能够创建 输入控件和图片控件了 ,可以随意的添加图片了。

现在我们开始处理下一个问题:删除

  • 监听删除键的点击
  • 当光标在EditText 输入中间,点击删除不进行处理正常删除
  • 当光标在EditText首端,判断前一个控件,如果是图片控件,删除图片控件,如果是输入控件,删除当前控件并将输入区域合并成一个输入区域
监听删除键
private OnKeyListener keyListener; //按键监听

// 主要用来处理点击回删按钮时,view合并操作
keyListener = new OnKeyListener() {

   @Override
   public boolean onKey(View v, int keyCode, KeyEvent event) {
          if (event.getAction() == KeyEvent.ACTION_DOWN) {
              if (event.getKeyCode() == KeyEvent.KEYCODE_DEL) {
                   onBackspacePress((EditText) v);
                    return false;
                }
          }
          return false;
    }
};

修改 addTodoWeight方法

 public TodoWeight addTodoWeightAtIndex(int index, CharSequence sequence) {
        TodoWeight todoWeight = new TodoWeight(getContext(), this, focusListener);
        inputWeights.add(index, todoWeight);

        if (sequence != null && sequence.length() > 0) {
            todoWeight.getEditText().setText(sequence);
        }
     //添加 键盘监听
        todoWeight.getEditText().setOnKeyListener(keyListener);
        todoWeight.getEditText().setOnFocusChangeListener(focusListener);
        addView(todoWeight.getContentView(), index);

        lastFocusEdit = todoWeight.getEditText();
        lastFocusEdit.requestFocus();
        lastFocusEdit.setSelection(sequence.length(), sequence.length());
        return todoWeight;
    }

处理删除键的代码:

 private void onBackspacePress(EditText editText) {
        int selectionStart = editText.getSelectionStart();
        //只有光标在 edit 区域的 最前方  判断 上一个 控件的类型
        if (selectionStart == 0) {
            int editIndex = indexOfChild((View) editText.getParent());
            //第一个控件 直接 返回
            if (editIndex == 0) {
                return;
            }
            //获取前一个 输入控件
            InputWeight baseInputWeight = inputWeights.get(editIndex - 1);
            //执行类型检查
            if (baseInputWeight instanceof ImageWeight) {
                //前一个 控件是  图片 控件 直接删除
                removeWeight(baseInputWeight);
            } else if (baseInputWeight instanceof TodoWeight) {
                //前一个控件是 edittext  进行 样式的合并
                //获取当前输入的 文本
                Editable currContent = editText.getText();
                //获取 前一个输入控件
                EditText preEdit = baseInputWeight.getEditText();
                //获取前一个控件的 内容
                Editable preEditContent = preEdit.getText();
                //-----------------------
                removeWeight(inputWeights.get(editIndex));
                //将当前 输入内容 添加到 前一个控件中
                preEditContent.insert(preEditContent.length(), currContent);
                //移动光标
                preEdit.setSelection(preEditContent.length(), preEditContent.length());
                //获取焦点
                preEdit.requestFocus();
                lastFocusEdit = preEdit;
            }

        }
    }

    /**
     * 移除控件
     * @param inputWeight
     */
    public void removeWeight(InputWeight inputWeight) {
        removeView(inputWeight.getContentView());
        inputWeights.remove(inputWeight);
    }

小结

本篇文章作为整个系列的第一篇,展示了编辑器的基本功能,分析了界面的组成,并且结合代码完成了创建输入控件、添加控件、插入图片控件、输入控件删除键功能。
项目地址RichEditor

下一篇预告:
Span实现基础样式 粗体 斜体 下划线 中划线 字体大小 对齐方式(左对齐 居中 右对齐)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,688评论 1 330
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,559评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,749评论 0 226
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,581评论 0 191
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,741评论 3 271
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,684评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,122评论 2 292
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,847评论 0 182
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,441评论 0 228
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,939评论 2 232
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,333评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,783评论 2 236
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,275评论 3 220
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,830评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,444评论 0 180
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,553评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,618评论 2 249