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

来源:互联网 发布:mac os x10.7.5下载 编辑:程序博客网 时间:2024/04/28 09:43

如何简单的实现一个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

0 0
原创粉丝点击