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