import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Parcel;
import android.support.annotation.IntDef;
import android.support.annotation.IntRange;
import android.support.annotation.Nullable;
import android.text.Html;
import android.text.Layout;
import android.text.Selection;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.method.LinkMovementMethod;
import android.text.method.Touch;
import android.text.style.ClickableSpan;
import android.text.style.ImageSpan;
import android.text.style.URLSpan;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.widget.TextView;
import com.bumptech.glide.load.resource.drawable.GlideDrawable;
import com.bumptech.glide.request.Request;
import com.bumptech.glide.request.animation.GlideAnimation;
import com.bumptech.glide.request.target.SimpleTarget;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import pl.droidsonroids.gif.GifDrawable;
public class RichTextView extends GifSpanTextView implements Drawable.Callback, View.OnAttachStateChangeListener {
private static Pattern IMAGE_TAG_PATTERN = Pattern.compile("\\<img(.*?)\\>");
private static Pattern IMAGE_WIDTH_PATTERN = Pattern.compile("width=\"(.*?)\"");
private static Pattern IMAGE_HEIGHT_PATTERN = Pattern.compile("height=\"(.*?)\"");
private static Pattern IMAGE_SRC_PATTERN = Pattern.compile("src=\"(.*?)\"");
private static Pattern IMAGE_BIG_SRC_PATTERN = Pattern.compile("bigsrc=\"(.*?)\"");
private static Pattern IMAGE_HREF_PATTERN = Pattern.compile("href=\"(.*?)\"");
private Drawable placeHolder, errorImage;//占位图,错误图
private OnImageClickListener onImageClickListener;//图片点击回调
private OnURLClickListener onURLClickListener;//超链接点击回调
// private HashSet<Target> targets;
// private HashSet targets;
private HashMap mImages;
private HashMap mBigImages;
private ImageFixListener mImageFixListener;
private int d_w = 200;
private int d_h = 100;
private boolean isScroll = true;
private String imageHost;
private boolean dontConsumeNonUrlClicks = true;
private boolean linkHit;
private Context mContext;
private GlideImageGeter glideImageGeter;
public RichTextView(Context context) {
this(context, null);
}
public RichTextView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public RichTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}
private void init(Context context, AttributeSet attrs) {
mContext = context;
// targets = new HashSet<>();
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.RichTextView);
placeHolder = typedArray.getDrawable(R.styleable.RichTextView_placeHolder);
errorImage = typedArray.getDrawable(R.styleable.RichTextView_errorImage);
d_w = typedArray.getDimensionPixelSize(R.styleable.RichTextView_defaultWidth, Utils.Dp2Px(context, d_w));
d_h = typedArray.getDimensionPixelSize(R.styleable.RichTextView_defaultHeight, Utils.Dp2Px(context, d_h));
isScroll = typedArray.getBoolean(R.styleable.RichTextView_isScroll, isScroll);
if (placeHolder == null) {
placeHolder = new ColorDrawable(Color.GRAY);
}
placeHolder.setBounds(0, 0, d_w, d_h);
if (errorImage == null) {
errorImage = new ColorDrawable(Color.GRAY);
}
errorImage.setBounds(0, 0, d_w, d_h);
typedArray.recycle();
}
/**
* 设置富文本
*
* @param text 富文本
*/
public void setRichText(String text) {
try {
glideImageGeter = new GlideImageGeter(getContext(), this);
// targets.clear();
matchImages(text);
if (text != null) {
text = text.replaceAll(" ", "\t"); //替换空格
}
Spanned spanned = Html.fromHtml(text, glideImageGeter, null);
SpannableStringBuilder spannableStringBuilder;
if (spanned instanceof SpannableStringBuilder) {
spannableStringBuilder = (SpannableStringBuilder) spanned;
} else {
spannableStringBuilder = new SpannableStringBuilder(spanned);
}
// 处理图片得点击事件
ImageSpan[] imageSpans = spannableStringBuilder.getSpans(0, spannableStringBuilder.length(), ImageSpan.class);
final List<String> imageUrls = new ArrayList<>();
for (int i = 0, size = imageSpans.length; i < size; i++) {
ImageSpan imageSpan = imageSpans[i];
String imageUrl = imageSpan.getSource();
String bigImageUrl = mBigImages.get(imageUrl);
if (!TextUtils.isEmpty(bigImageUrl)) {
imageUrl = bigImageUrl;
}
int start = spannableStringBuilder.getSpanStart(imageSpan);
int end = spannableStringBuilder.getSpanEnd(imageSpan);
imageUrl = imageUrl.startsWith("http") ? imageUrl : imageHost + imageUrl;
if (imageUrl.endsWith(".gif") || imageUrl.endsWith(".bmp") || imageUrl.endsWith(".jpg") || imageUrl.endsWith(".jpeg") || imageUrl.endsWith(".JPG") || imageUrl.endsWith(".JPEG") || imageUrl.endsWith(".png") || imageUrl.endsWith(".PNG")) {
imageUrls.add(imageUrl);
}
final int finalI = i;
ClickableSpan clickableSpan = new ClickableSpan() {
@Override
public void onClick(View widget) {
if (onImageClickListener != null) {
onImageClickListener.imageClicked(imageUrls, finalI);
}
}
};
ClickableSpan[] clickableSpans = spannableStringBuilder.getSpans(start, end, ClickableSpan.class);
if (clickableSpans != null && clickableSpans.length != 0) {
for (ClickableSpan cs : clickableSpans) {
spannableStringBuilder.removeSpan(cs);
}
}
spannableStringBuilder.setSpan(clickableSpan, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
// 处理超链接点击事件
URLSpan[] urlSpans = spannableStringBuilder.getSpans(0, spannableStringBuilder.length(), URLSpan.class);
for (int i = 0, size = urlSpans == null ? 0 : urlSpans.length; i < size; i++) {
URLSpan urlSpan = urlSpans[i];
int start = spannableStringBuilder.getSpanStart(urlSpan);
int end = spannableStringBuilder.getSpanEnd(urlSpan);
spannableStringBuilder.removeSpan(urlSpan);
spannableStringBuilder.setSpan(new CallableURLSpan(urlSpan.getURL(), onURLClickListener), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
//表情转义
final Matcher matcher = EmojiHelper.getInstance().getEmojiMatcher(spanned.toString());
while (matcher.find()) {
final String name = matcher.group(1);
final GifDrawable drawable = EmojiHelper.getInstance().getGifDrawable(name);
if (drawable != null) {
final ImageSpan gifSpan = new GifImageSpan(drawable);
spannableStringBuilder.setSpan(gifSpan, matcher.start(), matcher.end(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
super.setText(spanned);
if (isScroll) {
setMovementMethod(LocalLinkMovementMethod.getInstance()); //解决ListView里TextView设置LinkMovementMethod后导致其ItemClick失效的问题
}
setLongClickable(false); //屏蔽长按事件,防止部分手机Spannable长按时崩溃
} catch (Exception ex) {
ex.printStackTrace();
}
}
/*
private void addTarget(ImageTarget target) {
targets.add(target);
}
/
/*
* 从文本中拿到标签,并获取图片url和宽高
*/
private void matchImages(String text) {
mImages = new HashMap<>();
mBigImages = new HashMap<>();
ImageHolder holder;
Matcher imageMatcher, srcMatcher, bigSrcMatcher, widthMatcher, heightMatcher, hrefMatcher;
int position = 0;
hrefMatcher = IMAGE_HREF_PATTERN.matcher(text);
String hrefImageUrl = null;
if (hrefMatcher.find()) {
hrefImageUrl = getTextBetweenQuotation(hrefMatcher.group().trim().substring(4));
}
imageMatcher = IMAGE_TAG_PATTERN.matcher(text);
while (imageMatcher.find()) {
String image = imageMatcher.group().trim();
srcMatcher = IMAGE_SRC_PATTERN.matcher(image);
String src = null;
if (srcMatcher.find()) {
src = getTextBetweenQuotation(srcMatcher.group().trim().substring(4));
}
if (TextUtils.isEmpty(src)) {
continue;
}
bigSrcMatcher = IMAGE_BIG_SRC_PATTERN.matcher(image);
String bigSrc = null;
if (bigSrcMatcher.find()) {
bigSrc = getTextBetweenQuotation(bigSrcMatcher.group().trim().substring(4));
}
if (TextUtils.isEmpty(bigSrc)) {
if (!TextUtils.isEmpty(hrefImageUrl)) {
bigSrc = hrefImageUrl;
} else {
bigSrc = src;
}
}
holder = new ImageHolder(src, bigSrc, position);
widthMatcher = IMAGE_WIDTH_PATTERN.matcher(image);
if (widthMatcher.find()) {
holder.width = parseStringToInteger(getTextBetweenQuotation(widthMatcher.group().trim().substring(6)));
}
heightMatcher = IMAGE_HEIGHT_PATTERN.matcher(image);
if (heightMatcher.find()) {
holder.height = parseStringToInteger(getTextBetweenQuotation(heightMatcher.group().trim().substring(6)));
}
mImages.put(holder.src, holder);
mBigImages.put(holder.src, holder.bigSrc);
position++;
}
}
private int parseStringToInteger(String integerStr) {
int result = -1;
if (!TextUtils.isEmpty(integerStr)) {
try {
result = Integer.parseInt(integerStr);
} catch (Exception e) {
e.printStackTrace();
}
}
return result;
}
/**
* 从双引号之间取出字符串
*/
@Nullable
private static String getTextBetweenQuotation(String text) {
Pattern pattern = Pattern.compile("\"(.*?)\"");
Matcher matcher = pattern.matcher(text);
if (matcher.find()) {
return matcher.group(1);
}
return null;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
linkHit = false;
boolean res = super.onTouchEvent(event);
if (dontConsumeNonUrlClicks)
return linkHit;
return res;
}
public static class LocalLinkMovementMethod extends LinkMovementMethod {
static LocalLinkMovementMethod sInstance;
public static LocalLinkMovementMethod getInstance() {
if (sInstance == null)
sInstance = new LocalLinkMovementMethod();
return sInstance;
}
@Override
public boolean onTouchEvent(TextView widget,
Spannable buffer, MotionEvent event) {
int action = event.getAction();
if (action == MotionEvent.ACTION_UP ||
action == MotionEvent.ACTION_DOWN) {
int x = (int) event.getX();
int y = (int) event.getY();
x -= widget.getTotalPaddingLeft();
y -= widget.getTotalPaddingTop();
x += widget.getScrollX();
y += widget.getScrollY();
Layout layout = widget.getLayout();
int line = layout.getLineForVertical(y);
int off = layout.getOffsetForHorizontal(line, x);
ClickableSpan[] link = buffer.getSpans(
off, off, ClickableSpan.class);
if (link.length != 0) {
if (action == MotionEvent.ACTION_UP) {
link[0].onClick(widget);
} else if (action == MotionEvent.ACTION_DOWN) {
Selection.setSelection(buffer,
buffer.getSpanStart(link[0]),
buffer.getSpanEnd(link[0]));
}
if (widget instanceof RichTextView) {
((RichTextView) widget).linkHit = true;
}
return true;
} else {
Selection.removeSelection(buffer);
Touch.onTouchEvent(widget, buffer, event);
return false;
}
}
return Touch.onTouchEvent(widget, buffer, event);
}
}
private static final class URLDrawable extends Drawable {
private Drawable drawable;
@Override
public void setAlpha(@IntRange(from = 0, to = 255) int alpha) {
}
@Override
public void setColorFilter(@Nullable ColorFilter colorFilter) {
}
@Override
public int getOpacity() {
return 0;
}
@Override
public void draw(Canvas canvas) {
if (drawable != null)
drawable.draw(canvas);
}
public void setDrawable(Drawable drawable) {
this.drawable = drawable;
}
}
private static class CallableURLSpan extends URLSpan {
private OnURLClickListener onURLClickListener;
public CallableURLSpan(String url, OnURLClickListener onURLClickListener) {
super(url);
this.onURLClickListener = onURLClickListener;
}
@SuppressWarnings("unused")
public CallableURLSpan(Parcel src, OnURLClickListener onURLClickListener) {
super(src);
this.onURLClickListener = onURLClickListener;
}
@Override
public void onClick(View widget) {
if (onURLClickListener != null && onURLClickListener.urlClicked(getURL())) {
return;
}
super.onClick(widget);
}
}
public static class ImageHolder {
public static final int DEFAULT = 0;
public static final int CENTER_CROP = 1;
public static final int CENTER_INSIDE = 2;
@IntDef({DEFAULT, CENTER_CROP, CENTER_INSIDE})
public @interface ScaleType {
}
private final String src;
private final String bigSrc;
private final int position;
private int width = -1, height = -1;
private int scaleType = DEFAULT;
public ImageHolder(String src, String bigSrc, int position) {
this.src = src;
this.bigSrc = bigSrc;
this.position = position;
}
public String getSrcUrl() {
return src;
}
public String getBigSrcUrl() {
return bigSrc;
}
public int getHeight() {
return height;
}
public int getWidth() {
return width;
}
public void setWidth(int width) {
this.width = width;
}
public void setHeight(int height) {
this.height = height;
}
@ScaleType
public int getScaleType() {
return scaleType;
}
public void setScaleType(@ScaleType int scaleType) {
this.scaleType = scaleType;
}
}
@SuppressWarnings("unused")
public ImageHolder getImageHolder(String url) {
return mImages.get(url);
}
@SuppressWarnings("unused")
public void setPlaceHolder(Drawable placeHolder) {
this.placeHolder = placeHolder;
this.placeHolder.setBounds(0, 0, d_w, d_h);
}
@SuppressWarnings("unused")
public void setErrorImage(Drawable errorImage) {
this.errorImage = errorImage;
this.errorImage.setBounds(0, 0, d_w, d_h);
}
public void setOnImageClickListener(OnImageClickListener onImageClickListener) {
this.onImageClickListener = onImageClickListener;
}
public void setImageFixListener(ImageFixListener mImageFixListener) {
this.mImageFixListener = mImageFixListener;
}
/**
* 设置超链接点击回调事件(需在setRichText方法之前调用)
*
* @param onURLClickListener 回调
*/
public void setOnURLClickListener(OnURLClickListener onURLClickListener) {
this.onURLClickListener = onURLClickListener;
}
@Override
public void onViewAttachedToWindow(View v) {
}
@Override
public void onViewDetachedFromWindow(View v) {
glideImageGeter.recycle();
}
public interface OnImageClickListener {
/**
* 图片被点击后的回调方法
*
* @param imageUrls 本篇富文本内容里的全部图片
* @param position 点击处图片在imageUrls中的位置
*/
void imageClicked(List<String> imageUrls, int position);
}
public interface OnURLClickListener {
/**
* 超链接点击得回调方法
*
* @param url 点击得url
* @return true:已处理,false:未处理(会进行默认处理)
*/
boolean urlClicked(String url);
}
public interface ImageFixListener {
/**
* 修复图片尺寸的方法
*
* @param holder ImageHolder对象
*/
void onFix(ImageHolder holder);
}
public void configure(String imageHostPrefix) {
imageHost = imageHostPrefix;
}
}