概述
在使用微博的時候,我們都會發現這兩個功能
- EditText可輸入
@好友
,#話題#
和鏈接links
- 動態中可展示
@好友
,#話題#
和鏈接links
很容易想到是通過Span
和 正則表達式
來實現的,但是其中還涉及到了一些細節需要處理
EditText
輸入部分的細節功能
- 關鍵詞變色高亮
- 刪除的時候要選中整體刪除
- 焦點及光標不可以落在關鍵詞中間
- 一般都會帶有附加信息
對於最後一點,是基於以下考慮,比如
用戶名可相同,id不相同
一般都是類似這樣的處理,
<name id="xxx">@楊冪</name>
或者(@楊冪,id=xxx)
根據不同的id,跳轉界面,比如用戶詳情頁(
獲取詳細用戶信息
)。
參考實現:
優化實現
最後一點針對MentionEditText
做了一些優化,源碼:Mentions中的EditText部分。
抽象其功能主要涉及到這幾個方面:
- 界面顯示的CharSequence
- 帶有附加字段的CharSequence
- 高亮顏色
先來看一下用法,再看實現吧:
- User
public class User implements InsertData{
//...
@Override public CharSequence charSequence() {
return "@"+userName; //provide the CharSequence insert to edittext
}
@Override public FormatRange.FormatData formatData() {
return new UserConvert(this);//provide the formater for the insert data
}
@Override public int color() {
return Color.MAGENTA;//provide the range color
}
private class UserConvert implements FormatRange.FormatData {
public static final String USER_FORMART = "(@%s,id=%s)";
private final User user;
public UserConvert(User user) {
this.user = user;
}
@Override public CharSequence formatCharSequence() {//format
return String.format(USER_FORMART, user.getUserName(), user.getUserId());
}
}
}
- Activity
public class MainActivity extends AppCompatActivity{
@BindView(R.id.mentionedittext) MentionEditText mMentionedittext;
@Override protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (resultCode == Activity.RESULT_OK && null != data) {
switch (requestCode) {
case REQUEST_USER_APPEND:
User user = (User) data.getSerializableExtra(UserList.RESULT_USER);
mMentionedittext.insert(user);//insert data to edittext
break;
//...
}
}
super.onActivityResult(requestCode, resultCode, data);
}
}
- 獲取發送到服務器的數據
CharSequence convertMetionString = mMentionedittext.getFormatCharSequence();// 按照上面的format格式,這裏會得到 (@xxx,id=xxx-xx-x)
接下來看實現
!. 我們首先提供一個接口,定義插入的數據,如下:
public interface InsertData {
CharSequence charSequence(); //提供界面顯示的CharSequence
FormatRange.FormatData formatData();//提供CharSequence的轉換器
int color();//提供高亮顯示的顏色
}
!!. 爲什麼定義這兒接口,先看一下字符的插入過程
// 僞代碼
public void insert(InsertData insertData) {
//1.插入需要顯示在界面上的CharSequence
//2.將插入的CharSequence及其屬性用一個類管理起來
//3.將字符串變色
}
}
如上,第二步中可能需要的信息可能包括: 字符的起始位置,字符,轉換後的字符…
但是後面發現,字符接口已經提供
、、、而轉換後的字符
(可能需要經過不同的轉換),還是提供一個轉換器,讓用戶自己實現。
!!!. 因此有了轉換接口,如下:
public interface FormatData {
CharSequence formatCharSequence();//轉換爲帶有特殊字段的CharSequence
}
!!!!. 那麼上面的插入代碼即可採用如下方式寫:
public void insert(InsertData insertData) {
if (null != insertData) {
CharSequence charSequence = insertData.charSequence();
Editable editable = getText();
int start = getSelectionStart();//獲取插入的開始位置
int end = start + charSequence.length();//獲取文本長度
editable.insert(start, charSequence);//插入
FormatRange.FormatData format = insertData.formatData();
Range range = new FormatRange(start, end, format);
mRangeManager.add(range);//將相關信息存儲起來,用於獲取轉換信息
int color = insertData.color();
editable.setSpan(new ForegroundColorSpan(color), start, end,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);//關鍵字變色
}
}
!!!!!. 那麼我們看一下,上面這些東西如何組合成我們想要的最終信息,上面將Range
信息存儲起來了,這裏將其連接起來即可構成我們想要的信息,如下:
// mRangeManager中有一個ArrayList<Range>
public CharSequence getFormatCharSequence(String text) {
if (isEmpty()) {
return text;
}
int lastRangeTo = 0;
ArrayList<? extends Range> ranges = get();
Collections.sort(ranges);//Range 實現了Comparable,並且按照 start 排序
StringBuilder builder = new StringBuilder("");
CharSequence newChar;
for (Range range : ranges) {
if (range instanceof FormatRange) {
FormatRange formatRange = (FormatRange) range;
FormatRange.FormatData convert = formatRange.getConvert();
newChar = convert.formatCharSequence();
builder.append(text.substring(lastRangeTo, range.getFrom()));//將第一個 `Range` 之前的 字符縣存入
builder.append(newChar); // 將 轉換後的字符 存入
lastRangeTo = range.getTo();
}
}
builder.append(text.substring(lastRangeTo));//存入最後一個 `Range` 之後的字符
return builder.toString();
}
如上,即實現了功能。
TextView
因爲發出去的數據結構做了一些改變,展示的時候也需要作相應的處理
- 界面不能
將附加信息
顯示出來,可以仿照Html
類的實現 - 點擊
Span
的時候,獲取附加信息,如獲取xml
的Attribute
信息 - 點擊
Span
之外的地方,響應相應事件,這裏有坑,LinkMovementMethod
會攔截事件 - 支持圖文混排
emoji
,支持ellipse
,使用SpanableString
時,ellipse
會失效。
參考實現:
優化實現:
其實相比前面的EditText
,這個相對簡單多了。
!. 首先定義接口:
public interface ParserConverter {
Spanned convert(CharSequence source);//將CharSequence轉化爲需要顯示的Spanned,類似 Html.fromHtml()
}
!!. MentionTextView
繼承 TextView
,通過接口轉換
public class MentionTextView extends TextView {
//...
@Override public void setText(CharSequence text, BufferType type) {
if (!TextUtils.isEmpty(text) && null != mParserConverter) {
text = mParserConverter.convert(text);
}
super.setText(text, type);
setMovementMethod(new LinkMovementMethod());
}
//...
}
!!!. 使用
mMentiontextview.setParserConverter(mUserParser);
CharSequence convertMetionString = mMentionedittext.getFormatCharSequence();
mMentiontextview.setText(convertMetionString);
!!! Parser實現
先看一下,我們的Parser
需要實現的功能有如下幾個:
- @功能
- #tag#功能
- links功能:如微博將
http鏈接
替換爲網頁鏈接
,並可以跳轉
步驟1.
public class LinkUtil {
// 獲取網頁鏈接,動態替換
private static final Pattern URL_PATTERN = Pattern.compile(
"((http|https|ftp|ftps):\\/\\/)?([a-zA-Z0-9-]+\\.){1,5}(com|cn|net|org|hk|tw)((\\/(\\w|-)+(\\.([a-zA-Z]+))?)+)?(\\/)?(\\??([\\.%:a-zA-Z0-9_-]+=[#\\.%:a-zA-Z0-9_-]+(&)?)+)?");
public static String replaceUrl(String source) {
Matcher matcher = URL_PATTERN.matcher(source);
if (matcher.find()) {
String url = matcher.group();
source = source.replace(url, "<a href=" + "\'" + url + "\'" + ">網頁鏈接</a>");
}
return source;
}
}
步驟2:
public class Parser implements ParserConverter {
public Parser() {
}
@Override public Spanned convert(CharSequence source) {
if (TextUtils.isEmpty(source)) return new SpannableString("");
String sourceString = source.toString();
sourceString = LinkUtil.replaceUrl(sourceString);
return Html.fromHtml(sourceString, null, new HtmlTagHandler());
}
}
如上,功能實現,詳細代碼見: Mentions