(第二届 Google 暑期大学生博客分享大赛 - 2011 Android 成长篇)
做过web开发的人应该都知道,在HTML里支持<a>标签在文本里插入一个链接,点击后跳转;并且有<img>标签可以插入图片。Android开发是否也支持呢?带着这个疑问,我们去APIDemos探索一下。OK,在com.example.android.apis.text.link这个类里,官方演示了TextView支持的一些链接,上个图:
看来TextView是支持链接跳转的,不过做Android开发的应该都知道,android的View载体是Activity,能不能支持activity跳转呢,很遗憾,不支持。
不过无所谓,Android很有爱,开源的,理解了原理后我们自己去做,这也是我写本篇文章的主要目的,"授之以鱼,不如授之以渔",希望大家在遇到相似问题时能像我这样去分析源码,然后找出解决办法(或者大家可以提出更好的方法),另外,文中如有不妥的地方,也欢迎大家批评指正。先上效果图:点击左边的链接后跳转到右边。
现在我们开始开发吧!第一步,研究相关的源代码吧。通过跟踪TextView的源码,我们发现TextView支持的链接是由android.text.style.URLSpan这个类实现的,它重写了一个onClick方法:
public void onClick(View widget) {
Uri uri = Uri.parse(getURL());
Context context = widget.getContext();
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
intent.putExtra(Browser.EXTRA_APPLICATION_ID, context.getPackageName());
context.startActivity(intent);
}
大家看到了吧startActivity,多么熟悉的方法。既然它能实现,为什么我们不能呢,答案是可以的。我们接着跟踪代码,可以看到URLSpan其实继承的是android.text.style.ClickableSpan,我们来看一下他的源码:
public abstract class ClickableSpan extends CharacterStyle implements UpdateAppearance {
/**
* Performs the click action associated with this span.
*/
public abstract void onClick(View widget);
/**
* Makes the text underlined and in the link color.
*/
@Override
public void updateDrawState(TextPaint ds) {
ds.setColor(ds.linkColor);
ds.setUnderlineText(true);
}
}
是不是有点眉目了,我们直接继承这个类,重写他的方法不就可以了吗?大胆假设,小心求证,我们新建一个类:
import android.content.Context;
import android.content.Intent;
import android.text.TextPaint;
import android.text.style.ClickableSpan;
import android.view.View;
/**
* If an object of this type is attached to the text of a TextView with a
* movement method of LinkMovementMethod, the affected spans of text can be
* selected. If clicked, the {@link #onClick} method will be called.
*
* @author 张宁
*/
public class MyClickableSpan extends ClickableSpan {
int color = -1;
private Context context;
private Intent intent;
public MyClickableSpan(Context context, Intent intent) {
this(-1, context, intent);
}
/**
* constructor
* @param color the link color
* @param context
* @param intent
*/
public MyClickableSpan(int color, Context context, Intent intent) {
if (color!=-1) {
this.color = color;
}
this.context = context;
this.intent = intent;
}
/**
* Performs the click action associated with this span.
*/
public void onClick(View widget){
context.startActivity(intent);
};
/**
* Makes the text without underline.
*/
@Override
public void updateDrawState(TextPaint ds) {
if (color == -1) {
ds.setColor(ds.linkColor);
} else {
ds.setColor(color);
}
ds.setUnderlineText(false);
}
}
在这个类里,我们重写了onClick事件,实现了Activity的跳转,并且去掉了下划线。Ok,第一个目的就达到了,下面我们来看一下如何在TextView里加入表情。
这个就比较复杂了,因为TextView只能在其上下左右方向加入图片,是由Drawables这个类实现的,而我们想要的效果是在中间也可以插入,看来这次TextView插入图片源码帮不了我们了。不过我们可以去android.text这个包里去找别的类,大家可以看到在这个包里有一个Html类,做过web开发的应该可以想到什么吧?在文章开头已经提到了Html的<img>标签可以插入图片,那这个类是否提供这个功能呢?带着这个疑问我们可以进去看看,其中有个接口:
/**
* Retrieves images for HTML <img> tags.
*/
public static interface ImageGetter {
/**
* This methos is called when the HTML parser encounters an
* <img> tag. The <code>source</code> argument is the
* string from the "src" attribute; the return value should be
* a Drawable representation of the image or <code>null</code>
* for a generic replacement image. Make sure you call
* setBounds() on your Drawable if it doesn't already have
* its bounds set.
*/
public Drawable getDrawable(String source);
}
看到<code>source</code>这个没,熟悉吧,结合URLSpan的用法,我们是否可以配合Spanned实现一个
ImageSpan呢?OK,上代码:
import java.util.Map;
import java.util.Set;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.text.Html;
import android.text.Spanned;
import android.text.Html.ImageGetter;
/**
* this is a class which defining a spanned with image
* @author 张宁
*
*/
public class ImageSpan {
/**
* the map of face.
*/
private Map<String, String> faceMap;
private Context context;
public ImageSpan(Context context, Map<String, String> faceMap){
this.context = context;
this.faceMap = faceMap;
}
/**
* get the image by the given key
*/
private ImageGetter imageGetter = new Html.ImageGetter() {
@Override
public Drawable getDrawable(String source) {
Drawable drawable = null;
String sourceName = context.getPackageName() + ":drawable/"
+ source;
int id = context.getResources().getIdentifier(sourceName, null, null);
if (id != 0) {
drawable = context.getResources().getDrawable(id);
if (drawable != null) {
drawable.setBounds(0, 0, drawable.getIntrinsicWidth(),
drawable.getIntrinsicHeight());
}
}
return drawable;
}
};
/**
* return a {@link Spanned} with image
* @param text
* @return
*/
public Spanned getImageSpan(CharSequence text){
String cs = text.toString();
if (faceMap != null) {
Set<String> keys = faceMap.keySet();
for (String key : keys) {
if (cs.contains(key)) {
cs = cs.replace(key, "<img src='" + faceMap.get(key) + "'>");
}
}
}
return Html.fromHtml(cs, imageGetter, null);
}
}
到目前为止可以说关键代码都已经实现了,但是会有人问,我该如何使用这两个类呢?下面,我们在实现一个工具类来封装这两个类的方法,以方便调用:
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import android.content.Context;
import android.content.Intent;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.method.LinkMovementMethod;
import android.widget.EditText;
import android.widget.TextView;
/**
* TextView with intent that can redirect to a new activity
*
* @author 张宁
*
*/
public class CustomTextView {
private static Map<String, String> faceMap;
static {
faceMap = new HashMap<String, String>();
faceMap.put("[哭]", "face_1");
faceMap.put("[怒]", "face_2");
}
/**
* make textview a clickable textview<br>
* Note: make true the order of textList and intentList are mapped
*
* @param context
* @param textView
* @param textList
* the text should be set to this textview,not null
* @param intentList
* the intent map to the text, if the text have no intent mapped
* to, please set a null value.Or it will happen some unknown
* error.<br>
* not null
*/
public static void setClickableTextView(Context context, TextView textView,
List<String> textList, List<Intent> intentList) {
if (textList == null || intentList == null) {
return;
}
SpannableStringBuilder builder = new SpannableStringBuilder();
int end = -1, length = -1;
int size = textList.size();
Intent intent;
for (int i = 0; i < size; i++) {
String text = textList.get(i);
if (TextUtils.isEmpty(text)) {
continue;
}
builder.append(textList.get(i));
if ((intent = intentList.get(i)) != null) {
end = builder.length();
length = textList.get(i).length();
builder.setSpan(getClickableSpan(context, intent),
end - length, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
builder.append(" ");
}
textView.setText(builder);
textView.setFocusable(true);
textView.setMovementMethod(LinkMovementMethod.getInstance());
}
/**
* make textview a clickable textview<br>
* Note: make true the order of textList and intentList are mapped
* @param context
* @param textView
* @param text
* @param intent
*/
public static void setClickableTextView(Context context, TextView textView,
String text, Intent intent) {
SpannableStringBuilder builder = new SpannableStringBuilder(text);
builder.setSpan(getClickableSpan(context, intent), 0, text.length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
textView.setText(builder);
textView.setMovementMethod(LinkMovementMethod.getInstance());
}
/**
* make TextView a View with image at any index
* @param context
* @param textView
* @param textList
*/
public static void setImgTextView(Context context, TextView textView,
List<String> textList) {
StringBuilder builder = new StringBuilder();
for (int i = 0; i < textList.size(); i++) {
builder.append(textList.get(i)).append(" ");
}
setImgTextView(context, textView, builder.toString());
}
/**
* make TextView a View with image at any index
* @param context
* @param textView
* @param text
*/
public static void setImgTextView(Context context, TextView textView,
String text) {
ImageSpan imageSpan = new ImageSpan(context, faceMap);
Spanned spanned = imageSpan.getImageSpan(text);
textView.setText(spanned);
}
/**
* make EditText a View with image at any index
* @param context
* @param EditText
* @param text
*/
public static void setImgTextView(Context context, EditText editText,
String text) {
ImageSpan imageSpan = new ImageSpan(context, faceMap);
Spanned spanned = imageSpan.getImageSpan(text);
editText.setText(spanned);
}
/**
* return a custom ClickableSpan
*
* @param context
* @param intent
* @return
*/
public static MyClickableSpan getClickableSpan(Context context,
Intent intent) {
return new MyClickableSpan(context, intent);
}
/**
* make textview a clickable textview with image<br>
* Note: make true the order of textList and intentList are mapped
*
* @param context
* not null
* @param haveImg
* whether this is image in the text,not null
* @param textView
* not null
* @param textList
* the text should be set to this textview,not null
* @param intentList
* the intent map to the text, if the text have no intent mapped
* to, please set a null value.Or it will happen some unknown
* error.<br>
* allow null
*/
public static void setCustomText(Context context, Boolean haveImg,
TextView textView, List<String> textList, List<Intent> intentList) {
SpannableStringBuilder builder = new SpannableStringBuilder();
int end = -1, length = -1;
if (intentList != null) {
int size = textList.size();
Intent intent;
for (int i = 0; i < size; i++) {
String text = textList.get(i);
if (TextUtils.isEmpty(text)) {
continue;
}
builder.append(textList.get(i));
if ((intent = intentList.get(i)) != null) {
end = builder.length();
length = textList.get(i).length();
builder.setSpan(getClickableSpan(context, intent), end
- length, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
builder.append(" ");
}
} else {
for (String text : textList) {
builder.append(text).append(" ");
}
}
if (haveImg) {
ImageSpan imageSpan = new ImageSpan(context, faceMap);
Spanned spanned = imageSpan.getImageSpan(builder);
textView.setText(spanned);
} else {
textView.setText(builder);
}
textView.setMovementMethod(LinkMovementMethod.getInstance());
}
}
有了这个类,我们就可以方便的实现在TextView中插入Intent和表情了,甚至不用管底层是怎样实现的,也降低了代码的耦合度。但是又回到我写这篇文章的目的:希望大家能得到“渔”而不仅仅是“鱼”。
Ok,任务完成。源码奉上。
注:原创作品,转载请标明出处:
http://zhangning290.iteye.com/blog/1134286