这个需求,比较少见,但是我遇到了。
刚开始觉得很简单,毕竟 Android 的 TextView 还是很强大。直接 Html.fromHtml()
不就行了。
骚年,你想的太简单了。首先图片就不行,其次你也不知道服务端会给你什么标签,好吧。关于图片不显示我在网上查了点资料,重写ImageGetter.代码贴上
/**
* 重写图片加载接口
*/
private class HtmlImageGetter implements Html.ImageGetter {
private TextView textView;
public HtmlImageGetter(TextView v) {
textView = v;
}
/**
* 获取图片
*/
@Override
public Drawable getDrawable(String source) {
URLDrawable d = new URLDrawable(textView.getContext());
LoadImageAsyncTask loadImageAsyncTask = new LoadImageAsyncTask(textView, d);
loadImageAsyncTask.execute(source);
return d;
}
class LoadImageAsyncTask extends AsyncTask {
private URLDrawable mDrawable;
private TextView textView;
private final Context context;
public LoadImageAsyncTask(TextView v, URLDrawable drawable) {
this.textView = v;
context = textView.getContext();
mDrawable = drawable;
}
@Override
protected Drawable doInBackground(String... params) {
String source = params[0];
return fetchDrawable(source);
}
//预定图片宽高比例为 4:3
@SuppressWarnings("deprecation")
public Rect getDefaultImageBounds(Context context) {
Display display = ((Activity) context).getWindowManager().getDefaultDisplay();
int width = display.getWidth();
int height = (width * 3 / 4);
return new Rect(0, 0, width, height);
}
public Drawable fetchDrawable(String s) {
Drawable drawable = null;
URL url;
try {
url = new URL(s);
URLConnection conn = url.openConnection();
conn.connect();
InputStream in;
in = conn.getInputStream();
drawable = Drawable.createFromStream(in, SystemClock.currentThreadTimeMillis()+".jpg");
LogUtils.d("pcx", "drawable "+ drawable);
} catch (Exception e) {
return null;
}
return drawable;
}
/**
* 图片下载完成后执行
*/
@Override
protected void onPostExecute(Drawable drawable) {
LogUtils.d("pcx", "drawable != null && mDrawable != null" + (drawable != null && mDrawable != null));
if (drawable != null && mDrawable != null) {
mDrawable.drawable = drawable;
textView.invalidate();
textView.requestLayout();
// CharSequence t = textView.getText();
// textView.setText(t);
}
}
}
}
private class URLDrawable extends BitmapDrawable {
protected Drawable drawable;
@SuppressWarnings("deprecation")
public URLDrawable(Context context) {
this.setBounds(getDefaultImageBounds(context));
drawable = context.getResources().getDrawable(R.drawable.defaultimg);
drawable.setBounds(getDefaultImageBounds(context));
}
@Override
public void draw(Canvas canvas) {
if (drawable != null) {
drawable.draw(canvas);
}
}
@SuppressWarnings("deprecation")
public Rect getDefaultImageBounds(Context context) {
Display display = ((Activity) context).getWindowManager().getDefaultDisplay();
int width = display.getWidth();
int height = (int) (width * 3 / 4);
Rect bounds = new Rect(0, 0, width, height);
return bounds;
}
}
是的,确实可以实现,但是图片的宽高并不是想象中那么好控制,其次我这边图片加载太慢了,同条件下用Fresco 早就加载出来了,这边还没有出来。
我猜测可能这个是 Android 早期的方式,那个时候耗时操作还有在 MainThread.但是现在不可以了,所以用了古老的AsyncTask去处理,在速度方面太不理想了。
必杀技
直接去解析好了。
我很开心的写了如下代码。
if (!TextUtils.isEmpty(product.htmlStr)) {
Html.TagHandler handler = new Html.TagHandler() {
int contentIndex = 0;
/**
* opening : 是否为开始标签
* tag: 标签名称
* output:输出信息,用来保存处理后的信息
* xmlReader: 读取当前标签的信息,如属性等
*/
@Override
public void handleTag(boolean opening, String tag, Editable output, XMLReader xmlReader) {
LogUtils.d("PCX ", "handleTag--------- opening:" + opening + ",tag:" + tag);
if (!("html".equalsIgnoreCase(tag) || "body".equalsIgnoreCase(tag))) {
if ("img".equalsIgnoreCase(tag)) {
if (opening) {//获取当前标签的内容开始位置
lists.add(true);
LogUtils.d("PCX ", " tag:" + tag + ", lists.add(true);");
// try {
// final String imgUrl = (String) xmlReader.getProperty("src");
// LogUtils.d("pcx", " imgUrl------- " + imgUrl);
// } catch (Exception e) {
// LogUtils.e("pcx", " Exception------- " + e.toString());
// }
}
} else {
if (opening) {//获取当前标签的内容开始位置
contentIndex = output.length();
lists.add(false);
LogUtils.d("PCX ", " tag:" + tag + ", lists.add(false);");
Field elementField ;
try {
// get the private attributes of the xmlReader by reflection by rekire
//http://stackoverflow.com/questions/6952243/how-to-get-an-attribute-from-an-xmlreader?rq=1
elementField = xmlReader.getClass().getDeclaredField("theNewElement");
elementField.setAccessible(true);
Object element = elementField.get(xmlReader);
Field attsField = element.getClass().getDeclaredField("theAtts");
attsField.setAccessible(true);
Object atts = attsField.get(element);
Field dataField = atts.getClass().getDeclaredField("data");
dataField.setAccessible(true);
String[] data = (String[]) dataField.get(atts);
Field lengthField = atts.getClass().getDeclaredField("length");
lengthField.setAccessible(true);
int len = (Integer) lengthField.get(atts);
for (int i = 0; i < len; i++) {
//这边的src和type换成你自己的属性名就可以了
// if("src".equals(data[i * 5 + 1])) {
// myAttributeA = data[i * 5 + 4];
// } else if("type".equals(data[i * 5 + 1])) {
// myAttributeB = data[i * 5 + 4];
// }
LogUtils.i("log", data[i * 5 + 1] + " : " + data[i * 5 + 4]);
}
} catch (Exception e) {
e.printStackTrace();
}
} else {
final int length = output.length();
String content = output.subSequence(contentIndex, length).toString();
// SpannableString spanStr = new SpannableString(content);
// spanStr.setSpan(new ForegroundColorSpan(Color.GREEN), 0, content.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
LogUtils.d("pcx", " content------- " + content);
// output.replace(contentIndex, length, spanStr);
stringLists.add(content);
}
}
}
}
};
// //这里面的resource就是fromhtml函数的第一个参数里面的含有的url
Html.ImageGetter imgGetter = new Html.ImageGetter() {
public Drawable getDrawable(String source) {
LogUtils.d("pcx", " source------- " + source);
imgLists.add(source);
return null;
}
};
// TextView textView = new TextView(this);
Spanned spanned = Html.fromHtml(product.htmlStr, null, handler);
LogUtils.d("pcx", "------- " + spanned.toString());
没毛病啊,感觉马上成功了,但是。
当我发觉不行的时候看下这个注释就知道问题在哪里了
/**
* Is notified when HTML tags are encountered that the parser does
* not know how to interpret.
*/
public static interface TagHandler {
/**
* This method will be called whenn the HTML parser encounters
* a tag that it does not know how to interpret.
*/
public void handleTag(boolean opening, String tag,
Editable output, XMLReader xmlReader);
}
不服气的我去吧唧了下源码,问题在这里,我们 Android 解析 HTML 标签在这里解析的。然后不能识别的才回调,好吧,我的小九九破灭了。OOOOM
private void handleStartTag(String tag, Attributes attributes) {
if (tag.equalsIgnoreCase("br")) {
// We don't need to handle this. TagSoup will ensure that there's a for each
// so we can safely emite the linebreaks when we handle the close tag.
} else if (tag.equalsIgnoreCase("p")) {
handleP(mSpannableStringBuilder);
} else if (tag.equalsIgnoreCase("div")) {
handleP(mSpannableStringBuilder);
} else if (tag.equalsIgnoreCase("strong")) {
start(mSpannableStringBuilder, new Bold());
} else if (tag.equalsIgnoreCase("b")) {
start(mSpannableStringBuilder, new Bold());
} else if (tag.equalsIgnoreCase("em")) {
start(mSpannableStringBuilder, new Italic());
} else if (tag.equalsIgnoreCase("cite")) {
start(mSpannableStringBuilder, new Italic());
} else if (tag.equalsIgnoreCase("dfn")) {
start(mSpannableStringBuilder, new Italic());
} else if (tag.equalsIgnoreCase("i")) {
start(mSpannableStringBuilder, new Italic());
} else if (tag.equalsIgnoreCase("big")) {
start(mSpannableStringBuilder, new Big());
} else if (tag.equalsIgnoreCase("small")) {
start(mSpannableStringBuilder, new Small());
} else if (tag.equalsIgnoreCase("font")) {
startFont(mSpannableStringBuilder, attributes);
} else if (tag.equalsIgnoreCase("blockquote")) {
handleP(mSpannableStringBuilder);
start(mSpannableStringBuilder, new Blockquote());
} else if (tag.equalsIgnoreCase("tt")) {
start(mSpannableStringBuilder, new Monospace());
} else if (tag.equalsIgnoreCase("a")) {
startA(mSpannableStringBuilder, attributes);
} else if (tag.equalsIgnoreCase("u")) {
start(mSpannableStringBuilder, new Underline());
} else if (tag.equalsIgnoreCase("sup")) {
start(mSpannableStringBuilder, new Super());
} else if (tag.equalsIgnoreCase("sub")) {
start(mSpannableStringBuilder, new Sub());
} else if (tag.length() == 2 &&
Character.toLowerCase(tag.charAt(0)) == 'h' &&
tag.charAt(1) >= '1' && tag.charAt(1) <= '6') {
handleP(mSpannableStringBuilder);
start(mSpannableStringBuilder, new Header(tag.charAt(1) - '1'));
} else if (tag.equalsIgnoreCase("img")) {
startImg(mSpannableStringBuilder, attributes, mImageGetter);
} else if (mTagHandler != null) {
mTagHandler.handleTag(true, tag, mSpannableStringBuilder, mReader);
}
}
没辙了,解析吧,我尝试了 PULL SAX 解析,但是由于有一些标签,比如空标签。
经过一个小时奋斗,放弃了。
终于我还是用了第三方的,通常情况下我能不用就不用第三方,这次算我输。
用的什么框架?
Jsoup
jsoup 是一款Java 的HTML解析器,可直接解析某个URL地址、HTML文本内容。它提供了一套非常省力的API,可通过DOM,CSS以及类似于jQuery的操作方法来取出和操作数据。
我们 Android 当然也有。
中文文档
第一步
加入到我们项目
compile 'org.jsoup:jsoup:1.9.2'
第二步
算了还是直接上代码吧,一步一步写感觉怪怪的。
private void loadHtml() {
htmlList = new ArrayList<>();
if (!TextUtils.isEmpty(product.htmlStr)) {
Document parse = Jsoup.parse(product.htmlStr);
parseHtml(parse.getAllElements());
}
HtmlItemAdapter adapter = new HtmlItemAdapter(this, htmlList);
LinearLayoutManager mamager = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);
recyclerViewHtml.setLayoutManager(mamager);
recyclerViewHtml.setAdapter(adapter);
recyclerViewHtml.setNestedScrollingEnabled(false);
}
private void parseHtml(Elements allElements) {
for (int i = 0; i < allElements.size(); i++) {
Element element = allElements.get(i);
if (!(element.tagName().equalsIgnoreCase("#root") || element.tagName().equalsIgnoreCase("html") || element.tagName().equalsIgnoreCase("body") || element.tagName().equalsIgnoreCase("head"))) {
if (element.tagName().equalsIgnoreCase("img")) {
String src = element.attr("src");
htmlList.add(src);
LogUtils.d("pcx", src);
} else {
StringBuilder sb = new StringBuilder();
sb.append("");
sb.append(element.text());
sb.append("
");
htmlList.add(sb.toString());
LogUtils.d("pcx text", sb.toString());
}
}
}
}
简单来说 就是把图片拿出来,
剩下的标签都保留原来的样式再给 TextView。
HtmlItemAdapter 代码
ublic class HtmlItemAdapter extends RecyclerView.Adapter {
private int imgPosition = 0;
private final ArrayList mData;
private final Context mContext;
private ArrayList imgList = new ArrayList<>();
private enum ITEM_TYPE {
ITEM_TYPE_IMAGE,
ITEM_TYPE_TEXT
}
public HtmlItemAdapter(Context context, ArrayList aList) {
mData = aList;
mContext = context;
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (viewType == ITEM_TYPE.ITEM_TYPE_IMAGE.ordinal()) {
return new ImageViewHolder(new SimpleDraweeView(mContext));
} else {
return new TextViewHolder(new TextView(mContext));
}
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
if (holder instanceof TextViewHolder) {
((TextViewHolder) holder).mTextView.setText(Html.fromHtml(mData.get(position)));
} else if (holder instanceof ImageViewHolder) {
String s = mData.get(position);
imgList.add(s);
EzbuyImageLoaderUtil.loadImageWrapContent(s, ((ImageViewHolder) holder).mImageView);
((ImageViewHolder) holder).mImageView.setTag(imgPosition++);
}
}
@Override
public int getItemViewType(int position) {
String s = mData.get(position);
if (s.startsWith("http")) {
return ITEM_TYPE.ITEM_TYPE_IMAGE.ordinal();
} else {
return ITEM_TYPE.ITEM_TYPE_TEXT.ordinal();
}
}
@Override
public int getItemCount() {
return mData == null ? 0 : mData.size();
}
private class TextViewHolder extends RecyclerView.ViewHolder {
TextView mTextView;
TextViewHolder(View view) {
super(view);
this.mTextView = (TextView) view;
}
}
private class ImageViewHolder extends RecyclerView.ViewHolder {
SimpleDraweeView mImageView;
ImageViewHolder(View view) {
super(view);
this.mImageView = (SimpleDraweeView) view;
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
mImageView.setLayoutParams(lp);
view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String[] urls = new String[imgList.size()];
imgList.toArray(urls);
Intent intent = new Intent(mContext, ScanPictureActivity.class);
mContext.startActivity(intent.putExtras(ScanPictureActivity.setArguments(urls, (int) v.getTag())));
}
});
}
}
}
之前我们是用 LinearLayout ,我觉得这个效率太差,所以换成了RecyclerView.
由于我们是嵌套在ScrollView里面所以滑动不是很流畅。
加这个代码就行。
recyclerViewHtml.setNestedScrollingEnabled(false);
原来的 html
![](http://img1.imgtn.bdimg.com/it/u=1582593178,3329696341&fm=23&gp=0.jpg)
![](http://upload-images.jianshu.io/upload_images/1432234-9679194a4ecf8a8a.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)qfrwtfews
0.6
100
9000
jilhu
其实这个有 bug
在复杂布局下是实现不了的,百思不得姐后。
我决定再暴力点,鉴于 TextView 的强大兼容性,对不完整的 html 也可以解析显示。
我决定用 img 标签为分割线去切割了。
代码如下:
if (!TextUtils.isEmpty(product.htmlStr)) {
Document parse = Jsoup.parse(product.htmlStr);
Elements img = parse.getElementsByTag("img");
for (Element ele : img) {
String s = ele.outerHtml();
String[] split = product.htmlStr.split(s);
htmlList.add(split[0]);
htmlList.add(ele.attr("src"));
product.htmlStr = product.htmlStr.substring(s.length()+split[0].length());
}
htmlList.add(product.htmlStr);
HtmlItemAdapter adapter = new HtmlItemAdapter(this, htmlList);
LinearLayoutManager mamager = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);
recyclerViewHtml.setLayoutManager(mamager);
recyclerViewHtml.setAdapter(adapter);
recyclerViewHtml.setNestedScrollingEnabled(false);
}
好了就这几行代码。
可能的问题
img 后的样式可能会丢失。
但能接受。