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實現基礎樣式 粗體 斜體 下劃線 中劃線 字體大小 對齊方式(左對齊 居中 右對齊)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。