如何简单的实现一个富文本,图文混排编辑器

如何简单的实现一个android图文混排,据我所知,android有很多种现成的方式可以实现图文混排

  • WebView + JavaScript
  • EditText + Span
  • scrollview + view

上面几种方法是比较常见的实现图文混排+富文本的办法。


  • WebView + JavaScript

    在使用webview实现富文本真是太简单了,也就是html+css+js嘛,想怎么搞就怎么搞,不过这种的难点就是在手机客户端中的编辑问题,毕竟是webview和android view的转化问题,实现起来还是很多坑,不符合我的需求,略过

  • EditText + Span
    这个虽然可以很好的实现简单富文本的编辑,但是在图文混排,以及各种主要自定义的组件面前就显得捉襟见肘,顾忽略

  • scrollview + view
    这才是我想介绍的实现方式,这个的优点是可以实现各种各样的view,想什么组件自定义就行,而且实现比较简单,简单几句就可以实现文本插入编辑。


scrollview + view:

先上一个简单的效果图

这里写图片描述

首先,我先定义一个组件的接口

 //富文本组件都要实现该接口 
    public interface IEditView {

    //下面的方法根据具体的组件自己增加删除
    //上传文件返回的id
     String getUploadId();

    /**
     * 获取view类型
     */
    Enum getViewType();

    /**
     * 获取文件本地路径
     * @return
     */
    String getFilePath();

    /**
     * 获取具体实现的view
     * @return
     */
    View getView();

    /**
     * 设置点击组件下面的空白回调事件
     * @param listener
     */
    void setOnClickViewListener(IClickCallBack listener);

    /**
     * 获取显示的文本
     * @return
     */
    String getContent();

    Holder getHolder();

    //这里定个了多个组件类型
    enum Type{
        IMAGE,FILE,VOICE,LOCATION,CONTENT,TITLE,UNKOWN
    }

    class Holder implements Serializable{
        public  String uploadId;
        public String filePath;
        public String fileName;
        public Enum viewType;
        public String content;

        @Override
        public String toString() {
            return "ViewHolder{" +
                    "uploadId='" + uploadId + '\'' +
                    ", filePath='" + filePath + '\'' +
                    ", fileName='" + fileName + '\'' +
                    ", viewType=" + viewType +
                    ", content='" + content + '\'' +
                    '}';
        }
    }

还有一个组件的点击接口,可根据自己的组件自己选择实现的方法

 public interface IClickCallBack {

    /**
     * 点击view下面的空白处回调事件,可在此实现插入edittext,在组件下面留一条空白又好看又可以点击
     * @param v 点击的view
     * @param widget 当前的组件
     */
    void onBlankViewClick(View v, View widget);
    /**
     * 点击view里面的删除图标回调事件,部分类型的view里面没有删除图标
     * @param v 点击的view
     * @param widget 当前的组件
     */
    void onDeleteIconClick(View v, View widget);

    /**
     * 组件的点击事件
     * @param v
     * @param widget
     */
    void onContentClick(View v, View widget);
}

然后定义两个简单的组件 RichEditText 和RichImageView

//实现一个简单的文本框组件
    public class RichEditText extends FrameLayout implements IEditView {



    private LayoutInflater mInflater;
    private Context mContext;


    private EditText mEditText;

    private IClickCallBack clickCallBack;

    public Holder holder;

    @Override
    public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
        return new DeleteInputConnection(super.onCreateInputConnection(outAttrs),
                true);
    }
    //处理软键盘回删按钮backSpace时回调OnKeyListener
    private class DeleteInputConnection extends InputConnectionWrapper {

        public DeleteInputConnection(InputConnection target, boolean mutable) {
            super(target, mutable);
        }

        @Override
        public boolean sendKeyEvent(KeyEvent event) {
            return super.sendKeyEvent(event);
        }

        @Override
        public boolean deleteSurroundingText(int beforeLength, int afterLength) {
            if (beforeLength == 1 && afterLength == 0) {
                return sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN,
                        KeyEvent.KEYCODE_DEL))
                        && sendKeyEvent(new KeyEvent(KeyEvent.ACTION_UP,
                        KeyEvent.KEYCODE_DEL));
            }

            return super.deleteSurroundingText(beforeLength, afterLength);
        }

    }

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

    public RichEditText(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public RichEditText(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);

        mContext = context;
        mInflater = LayoutInflater.from(context);
        mInflater.inflate(R.layout.item_rich_edit,this);
        holder = new Holder();
        holder.viewType = Type.CONTENT;
        init();
    }

    private void init() {
        mEditText = (EditText) findViewById(R.id.et_rich);
        findViewById(R.id.blank_view).setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                if(clickCallBack != null)
                    clickCallBack.onBlankViewClick(v, RichEditText.this);
            }
        });
        mEditText.setOnTouchListener(new OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                if(clickCallBack != null)
                    clickCallBack.onContentClick(v, RichEditText.this);
                return false;
            }
        });

    }

     public void setContent(String content){
         mEditText.setText(content);
     }

    public EditText getEditText(){
        return mEditText;
    }

    public int getSelectionStart(){
        return mEditText.getSelectionStart();
    }

    public void setText(String text){
        mEditText.setText(text);
    }

    public void setSelection(int start,int stop){
        mEditText.setSelection(start,stop);
    }

    public void reqFocus(){
        mEditText.requestFocus();
    }

    @Override
    public String getUploadId() {
        return null;
    }

    @Override
    public Enum getViewType() {
        return Type.CONTENT;
    }

    @Override
    public String getFilePath() {
        return null;
    }

    @Override
    public View getView() {
        return this;
    }

    @Override
    public void setOnClickViewListener(IClickCallBack listener) {
        this.clickCallBack = listener;
    }

    @Override
    public String getContent() {
        String s = mEditText.getText().toString();
        holder.content = s;
        return s;
    }

    @Override
    public Holder getHolder() {
        return holder;
    }
    }

实现一个简单的图片组件

public class RichImageView extends FrameLayout implements IEditView {

    private LayoutInflater mInflater;
    private Context mContext;

    private ImageView mEditImageView;
    private ImageView mImageClose;
    private View mBlankView;

    private IClickCallBack clickCallBack;

    private Holder holder;
    private int SCREEN_WIDTH;

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

    public RichImageView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public RichImageView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);

        mContext = context;
        mInflater = LayoutInflater.from(context);
        mInflater.inflate(R.layout.item_edit_imageview, this);
        holder = new Holder();
        holder.viewType = Type.IMAGE;
        DisplayMetrics dm = new DisplayMetrics();
        ((Activity)context).getWindowManager().getDefaultDisplay().getMetrics(dm);
        SCREEN_WIDTH = dm.widthPixels;
        init();
    }


    private void init() {
        mEditImageView = (ImageView) findViewById(R.id.edit_imageView);
        mImageClose = (ImageView) findViewById(R.id.image_close);
        mBlankView = findViewById(R.id.blank_view);
        //图片组件下面留一条空白为了和下面的组件有间隔,也可以点击空白时候插入一个文本框
        mBlankView.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                if (clickCallBack != null) {
                    clickCallBack.onBlankViewClick(v, RichImageView.this);
                }
            }
        });
        //图片组件右上角有一个删除按钮
        mImageClose.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                if (clickCallBack != null) {
                    clickCallBack.onDeleteIconClick(v, RichImageView.this);
                }
            }
        });
        //图片组件点击,调用组件点击事件
        mEditImageView.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                if (clickCallBack != null) {
                    clickCallBack.onContentClick(v, RichImageView.this);
                }
            }
        });

    }
    //设置图片路径,我这里随便写死了
    public void setEditImageView(final String imagePath) {
//        if (TextUtils.isEmpty(imagePath))
//            return;
        holder.filePath = imagePath;
        mEditImageView.getLayoutParams().width= SCREEN_WIDTH;
        BitmapFactory.Options opts = new BitmapFactory.Options();
        opts.inJustDecodeBounds = true;
        BitmapFactory.decodeFile(imagePath, opts);
        RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(SCREEN_WIDTH, SCREEN_WIDTH);
        mEditImageView.setLayoutParams(layoutParams);

        mEditImageView.setBackgroundResource(R.drawable.ceshi);

    }


    @Override
    public String getUploadId() {
        return holder.uploadId;
    }

    @Override
    public Enum getViewType() {
        return Type.IMAGE;
    }

    @Override
    public String getFilePath() {
        return holder.filePath;
    }

    @Override
    public View getView() {
        return this;
    }

    @Override
    public void setOnClickViewListener(IClickCallBack listener) {
        this.clickCallBack = listener;
    }

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

    @Override
    public Holder getHolder() {
        return holder;
    }

    }

定义了两个简单的组件之后,接下来就是最后的组件管理器RichSrcollView,对组件的增删其实也是最基本的addview和removeview.
管理器实现了组件的点击事件,键盘的回退删除,组件的插入方法等待。

   /**
     * 富文本内容编辑组件
     * 文本编辑内容组件每次都会自动添加,你只需要添加各种其他组件就行了
     */
    public class RichSrcollView extends ScrollView {

    public static final String KEY_TITLE = "title";
    public static final String KEY_CONTENT = "content";

    private LinearLayout allLayout; // 这个是所有子view的容器,scrollView内部的唯一一个ViewGroup
    private OnKeyListener keyListener; // 所有EditText的软键盘监听器
    private OnFocusChangeListener focusListener; // 所有EditText的焦点监听listener
    public RichEditText lastFocusView; // 最近被聚焦的view
    private LayoutTransition mTransitioner; // 只在图片View添加或remove时,触发transition动画
    private Context mContext;

    private boolean hasTitle = false;

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

    public RichSrcollView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public RichSrcollView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        this.mContext = context;

        // 初始化allLayout,用来存放所有富文本组件
        allLayout = new LinearLayout(context);
        allLayout.setOrientation(LinearLayout.VERTICAL);
        allLayout.setBackgroundColor(Color.WHITE);
        setupLayoutTransitions();
        LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT,
                LayoutParams.WRAP_CONTENT);
        addView(allLayout, layoutParams);

        // 键盘退格监听
        // 主要用来处理点击回删按钮时,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) {
                    RichEditText richEditText = (RichEditText) v.getParent().getParent();
                    onBackspacePress(richEditText);
                }
                return false;
            }
        };

        //定一个焦点改变监听器,用来知道最后的焦点在哪个组件,这样插入新组件的话就会插入到那个组件的后面
        focusListener = new OnFocusChangeListener() {

            @Override
            public void onFocusChange(View v, boolean hasFocus) {
                if (hasFocus) {
                    lastFocusView = (RichEditText) v.getParent().getParent();
                }
            }
        };

        //初始化生成一个编辑文本框
        LinearLayout.LayoutParams firstEditParam = new LinearLayout.LayoutParams(
                LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
        RichEditText view = createEditText();
        allLayout.addView(view, firstEditParam);
        lastFocusView = view;
    }

    public void removeAllIEditView() {
        if (allLayout != null) {
            allLayout.removeAllViews();
        }
    }

    /**
     * 处理软键盘backSpace回退事件
     * 回退时是否在文本上回退,在文本上时是否还有数据,有就删除数据,没有就上次上一个组件,当前焦点还是在这个文本框,这样才有一种富文本编辑器的感觉
     *
     * @param
     */
    private void onBackspacePress(RichEditText curView) {
        int startSelection = curView.getEditText().getSelectionStart();
        // 只有在光标已经顶到文本输入框的最前方,在判定是否删除之前的组件,或两个View合并
        if (startSelection == 0) {
            //表示一个文本框,这种情况回退不能删除组件
            if (allLayout.getChildCount() <= 1) {
                return;
            }
            int editIndex = allLayout.indexOfChild(curView);
            View preView = allLayout.getChildAt(editIndex - 1);
            // 则返回的是null
            if (null != preView) {
                if (preView instanceof RichEditText) {
                    // 光标EditText的上一个view对应的还是文本框EditText
                    String str1 = curView.getEditText().getText().toString();
                    EditText preEdit = ((RichEditText) preView).getEditText();
                    String str2 = preEdit.getText().toString();

                    // 合并文本view时,不需要transition动画
                    allLayout.setLayoutTransition(null);
                    allLayout.removeView(curView);
                    allLayout.setLayoutTransition(mTransitioner); // 恢复transition动画

                    // 文本合并
                    preEdit.setText(str2 + str1);
                    preEdit.requestFocus();
                    preEdit.setSelection(str2.length(), str2.length());
                    lastFocusView = (RichEditText) preView;
                } else if (preView instanceof IEditView) {
                    // 光标EditText的上一个view对应的是组件
                    onEditViewCloseClick(preView);
                }

            }
        }
    }

    /**
     * 处理组件关闭图标的点击事件
     *
     * @param view 整个image对应的relativeLayout view
     */
    private void onEditViewCloseClick(View view) {
        if (!mTransitioner.isRunning()) {
            allLayout.removeView(view);
        }
    }

    /**
     * 生成文本输入框
     */
    private RichEditText createEditText() {
        RichEditText richEditText = new RichEditText(mContext);
        richEditText.getEditText().setOnKeyListener(keyListener);
        if (haveEditText())
            richEditText.getEditText().setHint("");
        richEditText.getEditText().setOnFocusChangeListener(focusListener);
        return richEditText;
    }


    private boolean haveEditText() {
        int childCount = allLayout.getChildCount();
        for (int i = 0; i < childCount; i++) {
            IEditView iEditView = (IEditView) allLayout.getChildAt(i);
            if (iEditView.getViewType().ordinal() == IEditView.Type.CONTENT.ordinal()) {
                return true;
            }
        }
        return false;
    }

    private void setEditViewListener(IEditView editView) {
        //删除按钮设置监听器
        editView.setOnClickViewListener(new IClickCallBack() {
            @Override
            public void onBlankViewClick(View v, View widget) {
                //点击组件下面的空白,如果当前组件和上下组件都不是文本框,则创建一个文本框
                int childCount = allLayout.getChildCount();
                for (int i = 0; i < childCount; i++) {
                    if (allLayout.getChildAt(i) == widget) {
                        View curView = allLayout.getChildAt(i);
                        View nextView = allLayout.getChildAt(i + 1);
                        if (!(curView instanceof RichEditText) && (nextView == null || !(nextView instanceof RichEditText))) {
                            addEditTextAtIndex(i + 1, "");
                            break;
                        }
                    }
                }
            }

            @Override
            public void onDeleteIconClick(View v, View widget) {
    //              Toast.makeText(mContext,"点击删除",Toast.LENGTH_SHORT).show();
                onEditViewCloseClick(widget);
                if (lastFocusView != null)
                    lastFocusView.reqFocus();
            }

            @Override
            public void onContentClick(View v, View widget) {

            }
        });
    }


    /**
     * 在特定位置插入EditText
     *
     * @param index   位置
     * @param editStr EditText显示的文字
     */
    private void addEditTextAtIndex(final int index, String editStr) {
        RichEditText view = createEditText();
        EditText editText2 = (EditText) view.findViewById(R.id.et_rich);
        editText2.setText(editStr);
        lastFocusView = view;
        view.reqFocus();
        // 请注意此处,EditText添加、或删除不触动Transition动画
        allLayout.setLayoutTransition(null);
        allLayout.addView(view, index);
        allLayout.setLayoutTransition(mTransitioner); // remove之后恢复transition动画
    }

    /**
     * 在特定位置添加一个编辑组件
     */
    private void addEditViewAtIndexAnimation(final int index, final IEditView editView) {
        postDelayed(new Runnable() {
            @Override
            public void run() {
                allLayout.addView(editView.getView(), index);

            }
        }, 200);


    }

    private void srollToBottom() {
        postDelayed(new Runnable() {
            @Override
            public void run() {
                if (lastFocusView != null)
                    lastFocusView.reqFocus();
                fullScroll(ScrollView.FOCUS_DOWN);
            }
        }, 1000);
    }

    /**
     * 立即插入一个编辑组件,适用于编辑话题,有延时会导致顺序错乱
     * 代价是没有动画
     *
     * @param index    显示位置
     * @param editView 组件
     */
    private void addEditViewAtIndexImmediate(final int index, final IEditView editView) {

        allLayout.addView(editView.getView(), index);
        postDelayed(new Runnable() {
            @Override
            public void run() {
                if (lastFocusView != null)
                    lastFocusView.reqFocus();
                fullScroll(ScrollView.FOCUS_DOWN);
            }
        }, 1000);

    }

    /**
     * 初始化transition动画
     */
    private void setupLayoutTransitions() {
        mTransitioner = new LayoutTransition();
        allLayout.setLayoutTransition(mTransitioner);
        mTransitioner.setDuration(300);
    }


    /**
     * 获取当前焦点的Edittext
     *
     * @return
     */
    public EditText getCurFousEditText() {
        if (lastFocusView != null)
            return lastFocusView.getEditText();
        return null;
    }

    public void setLastEditTextFocus() {
        int childCount = allLayout.getChildCount();
        for (int i = childCount - 1; i >= 0; i--) {
            View childAt = allLayout.getChildAt(i);
            if (childAt instanceof RichEditText) {
                ((RichEditText) childAt).reqFocus();
                showKeyBoard(((RichEditText) childAt).getEditText());
                return;
            }
        }
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {

        if (ev.getY() > allLayout.getBottom()) {
            setLastEditTextFocus();
            return true;
        }


        return super.dispatchTouchEvent(ev);
    }

    /**
     * 隐藏小键盘
     */
    public void hideKeyBoard() {
        InputMethodManager imm = (InputMethodManager) getContext()
                .getSystemService(Context.INPUT_METHOD_SERVICE);
        imm.hideSoftInputFromWindow(lastFocusView.getWindowToken(), 0);
    }

    public void showKeyBoard(EditText view) {
        InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
        view.setSelection(0);
        view.setFocusable(true);
        view.setFocusableInTouchMode(true);
        view.requestFocus();
        imm.showSoftInput(view, 0);
    }

    /**
     * 插入一个编辑组件,根据焦点的不同而位置不同
     */
    public void insertEditView(IEditView editView) {
        setEditViewListener(editView);

        String lastEditStr = lastFocusView.getContent();
        lastFocusView.reqFocus();
        int cursorIndex = lastFocusView.getSelectionStart();
        int lastEditIndex = allLayout.indexOfChild(lastFocusView);
        if (cursorIndex >= 0) {
            String editStr1 = lastEditStr.substring(0, cursorIndex).trim();

            if (lastEditStr.length() == 0 || editStr1.length() == 0) {
                // 如果EditText为空,或者光标已经顶在了editText的最前面,则直接插入组件,并且EditText下移即可
                addEditViewAtIndexAnimation(lastEditIndex, editView);
            } else {
                // 如果EditText非空且光标不在最顶端,则需要添加新的imageView和EditText
                lastFocusView.setText(editStr1);
                String editStr2 = lastEditStr.substring(cursorIndex).trim();
                if (allLayout.getChildCount() - 1 == lastEditIndex
                        || editStr2.length() > 0) {
                    addEditTextAtIndex(lastEditIndex + 1, editStr2);
                }

                addEditViewAtIndexAnimation(lastEditIndex + 1, editView);
                lastFocusView.reqFocus();
                lastFocusView.setSelection(lastFocusView.getContent().length(), lastFocusView.getContent().length());
            }
            if (allLayout.indexOfChild(lastFocusView) >= allLayout.getChildCount() - 1) {
                srollToBottom();
            }
        } else {
            //出现失去焦点的情况,默认添加到最后面
            addEditViewAtIndexAnimation(allLayout.getChildCount() - 1, editView);
            srollToBottom();
        }

        hideKeyBoard();
    }

    /**
     * 获取全部数据集合
     */
    public List<IEditView> buildData() {
        List<IEditView> dataList = new ArrayList<IEditView>();
        int num = allLayout.getChildCount();
        for (int index = 0; index < num; index++) {
            IEditView itemView = (IEditView) allLayout.getChildAt(index);
            dataList.add(itemView);
        }
        return dataList;
    }

    }

大体的注释都有,而具体的引用很简单,我这里点击按钮的时候就新建一个图片组件,而文本框组件可以点击组件下面的空白条插入。

 Button button = (Button) findViewById(R.id.button);
        final RichSrcollView richSrcollVIew = (RichSrcollView) findViewById(R.id.scrollview);
        button.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                RichImageView richImageView = new RichImageView(MainActivity.this);
                //设置图片路径
                richImageView.setEditImageView("");
                //插入组件
                richSrcollVIew.insertEditView(richImageView);
            }
        });
  • 只需要在scrollview实现一些view的添加和删除,以及组件间的拼接,就可以实现一个很简单的可定制的富文本编辑器。

  • 然而有一个缺点就是,毕竟是scrollview,不像listview recycleview那样可以资源回收,这个插入太多图片有可能导致oom

代码查看 https://github.com/JadynChan/RichTextDemo

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值