如何简单的实现一个android图文混排,据我所知,android有很多种现成的方式可以实现图文混排
WebView + javaScript EditText + Spanscrollview + 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
新闻热点
疑难解答