Android 自定義view-仿新浪微博#話題#插入EditText
不小心開啟新浪微博發微博頁面有個可以插入話題#…#的功能,看著挺好玩的。就照著實現了一下。
如果不知道怎麼樣效果的可以開啟微博看看。大概的功能是:
- 插入話題使用#特殊符號開頭和結尾
- 話題文字高亮顯示
- 刪除話題選中整個話題文字一次性刪除
- 可以插入多個話題
下面先看看我的實現效果圖
怎麼樣,看起來是不是很吊的樣子呢?又好像跟新浪微博的有些不一樣哦,話題匹配符號不一樣,沒錯,高仿微博但更勝於微博效果。
那麼使用我的自定義控制元件你可以有哪些功能呢?
1.自定義每一個話題匹配符號 (# $ ^ & * 等)*
2.自定義話題高亮顏色,選中話題背景色
3.允許插入多個話題,並且可以獲取物件列表,不僅僅是文字
4.點選話題游標自動移動至話題結束或開頭
OK,那麼這樣一個自定控制元件到底有多難呢?其實很簡單。下面來實現一下
強調一遍做事情思路一定要清晰,遇到困難不要緊,但是解決問題的思路一定要清晰,那麼我們這個控制元件要實現哪些功能呢?
1.在游標處插入話題
這或許是最簡單的問題了,我們直接繼承原生的EditText,獲取游標的位置,然後往EditText中insert文字。
這裡需要注意的是:插入話題的時候不僅僅是文字,而是設定一個物件(很多需求是你獲取輸入框文字的時候還需要拿到話題的id等其他屬性),同時需要將這個物件儲存在集合中,最終提供給開發者使用。
那麼我們自定一個類繼承EditText,然後提供一個方法 setObject(RObject object)
/**
* 插入/設定話題
*
* @param object話題物件
*/
public void setObject(RObject object) {
if (object == null)
return;
String objectRule = object.getObjectRule();
String objectText = object.getObjectText();
if (TextUtils.isEmpty(objectText) || TextUtils.isEmpty(objectRule))
return;
// 拼接字元# %s #,並儲存
objectText = objectRule + objectText + objectRule;
object.setObjectText(objectText);
/**
* 新增話題<br/>
* 1.將話題內容新增到資料集合中<br/>
* 2.將話題內容新增到EditText中展示
*/
/**
* 1.新增話題內容到資料集合
*/
mRObjectsList.add(object);
/**
* 2.將話題內容新增到EditText中展示
*/
int selectionStart = getSelectionStart();// 游標位置
Editable editable = getText();// 原先內容
if (selectionStart >= 0) {
editable.insert(selectionStart, objectText);// 在游標位置插入內容
editable.insert(getSelectionStart(), " ");// 話題後面插入空格,至關重要
setSelection(getSelectionStart());// 移動游標到新增的內容後面
}
}
RObject :話題物件,包含基本屬性{ 話題文字,話題匹配字元# }
public class RObject {
private String objectRule = "#";// 匹配規則
private String objectText;// 話題文字
public String getObjectRule() {
return objectRule;
}
public void setObjectRule(String objectRule) {
this.objectRule = objectRule;
}
public String getObjectText() {
return objectText;
}
public void setObjectText(String objectText) {
this.objectText = objectText;
}
}
如果開發者的話題物件還需要其他的屬性,那麼繼承 RObject 新增所需屬性,例如
/**
* 測試使用開發者話題實體,必須繼承RObject
*/
class MyTopic extends RObject {
private String id;
// 其他屬性...
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
}
第一段程式碼功能
1.將話題內容新增到資料集合中
2.將話題內容新增到EditText中展示
開發者傳遞一個RObject 物件過來,然後獲取 objectRule ,objectText 根據匹配規則拼接字串現在UI中,同時儲存到資料集合。關鍵方法獲取到當前游標位置然後呼叫insert方法插入文字
2.話題文字高亮顏色
第一步輕輕鬆鬆將內容插入到EditText,可以看到內容中已經有帶 # 的話題內容內容出現了,看起來有點像了哦,那麼現在就改變話題的文字顏色,關鍵方法
Editable editable = getText();
ForegroundColorSpan colorSpan = new ForegroundColorSpan(mForegroundColor);
editable.setSpan(colorSpan, findPosition, findPosition + objectText.length(),
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
通過Editable 方法setSpan設定高亮顏色,這裡需要獲取到變色的 起始位置 和 結束位置 ,我們都知道在使用者不斷輸入文字的時候起始位置和結束位置是在不停變化的,因此我們需要在EditText文字改變的時候重新整理UI重新設定話題的高亮顏色。
監聽文字變化,很簡單直接實現EditText的監聽介面
/**
* 輸入框內容變化監聽<br/>
* 1.當文字內容產生變化的時候實時更新UI
*/
this.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count,
int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before,
int count) {
}
@Override
public void afterTextChanged(Editable s) {
// 文字改變重新整理UI
refreshEditTextUI(s.toString());
}
});
重新整理UI,重繪高亮文字文字方法
/**
* EditText內容修改之後重新整理UI
*
* @param content輸入框內容
*/
private void refreshEditTextUI(String content) {
/**
* 內容變化時操作<br/>
* 1.查詢匹配所有話題內容 <br/>
* 2.設定話題內容特殊顏色
*/
if (mRObjectsList.size() == 0)
return;
if (TextUtils.isEmpty(content)) {
mRObjectsList.clear();
return;
}
/**
* 重新設定span
*/
Editable editable = getText();
int findPosition = 0;
for (int i = 0; i < mRObjectsList.size(); i++) {
final RObject object = mRObjectsList.get(i);
String objectText = object.getObjectText();// 文字
findPosition = content.indexOf(objectText);// 獲取文字開始下標
if (findPosition != -1) {// 設定話題內容前景色高亮
ForegroundColorSpan colorSpan = new ForegroundColorSpan(
mForegroundColor);
editable.setSpan(colorSpan, findPosition, findPosition
+ objectText.length(),
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
}
方法要做的事情很明確
1.查詢匹配所有話題內容,起始和結束位置
2.設定話題內容特殊顏色
這裡得到改變後EditText的所有內容,使用 indexOf(“目標文字”)查詢目標文字起始的位置,結束位置就簡單啦,起始位置+目標文字的長度。由於這裡支援多個話題插入,所以加多一個for迴圈遍歷一遍就好。soEasy
得到起始和結束位置就好辦了,使用Editable 的 setSpan() 方法設定高亮文字的起始和結束值。
到這裡,已經實現了類似微博話題輸入,並且可以高亮顏色展示的效果了。
3.實現話題選中刪除效果
我們都知道,刪除鍵是逐個刪除文字的,那麼我們需要做的就是,判斷當游標處於話題內容中,或者在話題結束位置的時候 第一次點選刪除實現選中整個文字效果,當用戶再次點選刪除鍵時再刪除這個話題內容,而其他普通內容則逐個刪除
我們通過監聽輸入板事件擷取使用者按下刪除鍵時的事件,當游標處於話題內容中,或者在話題後面時,第一次按下刪除鍵,實現選中文字效果,並且修改文字背景色,再次點選刪除話題文字
但是要注意的是,我們不能利用 activity 裡面的 onKeyDown() 和 onKeyUp() 兩個回撥,通過 log 發現文字變動和按鍵點選的回撥順序為 beforeTextChanged -> onTextChanged -> afterTextChanged -> onKeyDown -> onKeyUp .
這也說明了如果通過 攔截 onKeyDown() 和 onKeyUp() 兩個回撥時,文字是已經刪除之後的文字,並不能有效的達到我們要實現的目的,那麼有沒有是文字改變之前就能擷取到按鍵的方法呢?
其實我們可以通過監聽 EditText 的 setOnKeyListener() 方法來監聽按鍵( onKey -> beforeTextChanged -> onTextChanged -> afterTextChanged -> onKeyDown -> onKeyUp ):
/**
* 監聽刪除鍵 <br/>
* 1.游標在話題後面,將整個話題內容刪除 <br/>
* 2.游標在普通文字後面,刪除一個字元
*/
this.setOnKeyListener(new View.OnKeyListener() {
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_DEL
&& event.getAction() == KeyEvent.ACTION_DOWN) {
int selectionStart = getSelectionStart();
int selectionEnd = getSelectionEnd();
/**
* 如果游標起始和結束不在同一位置,刪除文字
*/
if (selectionStart != selectionEnd) {
// 查詢文字是否屬於目標物件,若是移除列表資料
String tagetText = getText().toString().substring(
selectionStart, selectionEnd);
for (int i = 0; i < mRObjectsList.size(); i++) {
RObject object = mRObjectsList.get(i);
if (tagetText.equals(object.getObjectText())) {
mRObjectsList.remove(object);
}
}
return false;
}
int lastPos = 0;
Editable editable = getText();
// 遍歷判斷游標的位置
for (int i = 0; i < mRObjectsList.size(); i++) {
String objectText = mRObjectsList.get(i)
.getObjectText();
lastPos = getText().toString().indexOf(objectText,
lastPos);
if (lastPos != -1) {
if (selectionStart != 0
&& selectionStart >= lastPos
&& selectionStart <= (lastPos + objectText
.length())) {
// 選中話題
setSelection(lastPos,
lastPos + objectText.length());
// 設定背景色
editable.setSpan(new BackgroundColorSpan(
mBackgroundColor), lastPos, lastPos
+ objectText.length(),
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
return true;
}
}
lastPos += objectText.length();
}
}
return false;
}
});
當滿足第一次點選刪除條件時設定游標 為 話題起始 到 話題結束
點選刪除按鍵時判斷游標起始位置和結束位置是否相等,如果不相等表示該文字已經選中,可以直接刪除
到這裡基本上完成了類似微博插入話題並且刪除的效果,於是再仔細看看發現微博的效果中,游標無論如何不會出現在話題內容中,我們第一時間應該想到EditText有沒有這類事件的監聽呢,一查,果然有,那就太輕鬆了,這就是直接繼承原生EditText的好處,很多方法直接用,我們只需要按照自己的需求稍作修改
我們重寫onSelectionChanged(int selStart, int selEnd)
監聽游標位置變化,如果游標位置處於話題內容中,直接將游標移動到話題最後。從而達到游標不會再話題內容中出現閃爍的效果,這樣好看多了
/**
* 監聽游標的位置,若游標處於話題內容中間則移動游標到話題結束位置
*/
@Override
protected void onSelectionChanged(int selStart, int selEnd) {
super.onSelectionChanged(selStart, selEnd);
if (mRObjectsList == null || mRObjectsList.size() == 0) {
return;
}
int startPosition = 0;
int endPosition = 0;
String objectText = "";
for (int i = 0; i < mRObjectsList.size(); i++) {
objectText = mRObjectsList.get(i).getObjectText();// 文字
startPosition = getText().toString().indexOf(objectText);// 獲取文字開始下標
endPosition = startPosition + objectText.length();
if (startPosition != -1 && selStart > startPosition
&& selStart <= endPosition) {// 若游標處於話題內容中間則移動游標到話題結束位置
setSelection(endPosition);
}
}
}
好了到這裡你就真的實現了高仿微博的實現效果了
但是我們知道實際開發中,我們不僅僅是在UI上展示話題文字高亮就可以了,在提交內容的時候我們希望可以拿到 id,text,以及其他的一些屬性。因此我在一個設計時候就要求開發者傳入一個RObject 物件,在需要提交到伺服器的時候可以獲取這些話題物件的集合,拿到想要的內容。
如果需要點選事件的時候也方便拿到話題物件的屬性。(這裡個人覺得在輸入框中沒有必要實現話題點選,就拿掉這部分功能了。如果要實現也很簡單,在第二步設定話題顏色中ForegroundColorSpan
修改為ClickableSpan
順便再新增一個回撥函式響應點選事件,就可以了)
最後提供一個方法給開發者獲取話題物件集合
/**
* 獲取object列表資料
*/
public List<RObject> getObjects() {
List<RObject> objectsList = new ArrayList<RObject>();
// 由於儲存時候文字內容添加了匹配字元#,此處去除,還原資料
if (mRObjectsList != null && mRObjectsList.size() > 0) {
for (int i = 0; i < mRObjectsList.size(); i++) {
RObject object = mRObjectsList.get(i);
String objectText = object.getObjectText();
String objectRule = object.getObjectRule();
object.setObjectText(objectText.replace(objectRule, ""));// 將匹配規則字元替換
objectsList.add(object);
}
}
return objectsList;
}
這裡需要注意的是,由於開發者的話題物件中文字是純文字,匹配規則是另外的屬性設定的,因此需要還原開發者本來的資料,再將整個話題物件返回給開發者
由於是分步驟講解,因此可能程式碼不是很直觀,那就貼一些整個自定義控制元件以及使用方法
1.自定義屬性
我們需要自定義兩個屬性設定高亮文字顏色,和選中文字的背景色,我們在 /value/attrs.xml 中這麼寫:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- REditText -->
<declare-styleable name="REditText">
<attr name="object_foreground_color" format="color" />
<attr name="object_background_color" format="color" />
</declare-styleable>
</resources>
2.自定義REditText控制元件類
重要的方法以及實現思路在前面已經講解,下面看整個類程式碼結構,結合註釋,我相信聰明的你肯定不會看不懂,當然有問題可以留言
/**
* 仿微博話題輸入控制元件
*
* @author Ruffian
*
*/
public class REditText extends EditText {
// 預設,話題文字高亮顏色
private static final int FOREGROUND_COLOR = Color.parseColor("#FF8C00");
// 預設,話題背景高亮顏色
private static final int BACKGROUND_COLOR = Color.parseColor("#FFDEAD");
/**
* 開發者可設定內容
*/
private int mForegroundColor = FOREGROUND_COLOR;// 話題文字高亮顏色
private int mBackgroundColor = BACKGROUND_COLOR;// 話題背景高亮顏色
private List<RObject> mRObjectsList = new ArrayList<RObject>();// object集合
public REditText(Context context) {
this(context, null);
}
public REditText(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray a = context.obtainStyledAttributes(attrs,
R.styleable.REditText);
mBackgroundColor = a
.getColor(R.styleable.REditText_object_background_color,
BACKGROUND_COLOR);
mForegroundColor = a
.getColor(R.styleable.REditText_object_foreground_color,
FOREGROUND_COLOR);
a.recycle();
// 初始化設定
initView();
}
/**
* 監聽游標的位置,若游標處於話題內容中間則移動游標到話題結束位置
*/
@Override
protected void onSelectionChanged(int selStart, int selEnd) {
super.onSelectionChanged(selStart, selEnd);
if (mRObjectsList == null || mRObjectsList.size() == 0) {
return;
}
int startPosition = 0;
int endPosition = 0;
String objectText = "";
for (int i = 0; i < mRObjectsList.size(); i++) {
objectText = mRObjectsList.get(i).getObjectText();// 文字
startPosition = getText().toString().indexOf(objectText);// 獲取文字開始下標
endPosition = startPosition + objectText.length();
if (startPosition != -1 && selStart > startPosition
&& selStart <= endPosition) {// 若游標處於話題內容中間則移動游標到話題結束位置
setSelection(endPosition);
}
}
}
/**
* 初始化控制元件,一些監聽
*/
private void initView() {
/**
* 輸入框內容變化監聽<br/>
* 1.當文字內容產生變化的時候實時更新UI
*/
this.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count,
int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before,
int count) {
}
@Override
public void afterTextChanged(Editable s) {
// 文字改變重新整理UI
refreshEditTextUI(s.toString());
}
});
/**
* 監聽刪除鍵 <br/>
* 1.游標在話題後面,將整個話題內容刪除 <br/>
* 2.游標在普通文字後面,刪除一個字元
*/
this.setOnKeyListener(new View.OnKeyListener() {
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_DEL
&& event.getAction() == KeyEvent.ACTION_DOWN) {
int selectionStart = getSelectionStart();
int selectionEnd = getSelectionEnd();
/**
* 如果游標起始和結束不在同一位置,刪除文字
*/
if (selectionStart != selectionEnd) {
// 查詢文字是否屬於目標物件,若是移除列表資料
String tagetText = getText().toString().substring(
selectionStart, selectionEnd);
for (int i = 0; i < mRObjectsList.size(); i++) {
RObject object = mRObjectsList.get(i);
if (tagetText.equals(object.getObjectText())) {
mRObjectsList.remove(object);
}
}
return false;
}
int lastPos = 0;
Editable editable = getText();
// 遍歷判斷游標的位置
for (int i = 0; i < mRObjectsList.size(); i++) {
String objectText = mRObjectsList.get(i)
.getObjectText();
lastPos = getText().toString().indexOf(objectText,
lastPos);
if (lastPos != -1) {
if (selectionStart != 0
&& selectionStart >= lastPos
&& selectionStart <= (lastPos + objectText
.length())) {
// 選中話題
setSelection(lastPos,
lastPos + objectText.length());
// 設定背景色
editable.setSpan(new BackgroundColorSpan(
mBackgroundColor), lastPos, lastPos
+ objectText.length(),
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
return true;
}
}
lastPos += objectText.length();
}
}
return false;
}
});
}
/**
* EditText內容修改之後重新整理UI
*
* @param content輸入框內容
*/
private void refreshEditTextUI(String content) {
/**
* 內容變化時操作<br/>
* 1.查詢匹配所有話題內容 <br/>
* 2.設定話題內容特殊顏色
*/
if (mRObjectsList.size() == 0)
return;
if (TextUtils.isEmpty(content)) {
mRObjectsList.clear();
return;
}
/**
* 重新設定span
*/
Editable editable = getText();
int findPosition = 0;
for (int i = 0; i < mRObjectsList.size(); i++) {
final RObject object = mRObjectsList.get(i);
String objectText = object.getObjectText();// 文字
findPosition = content.indexOf(objectText);// 獲取文字開始下標
if (findPosition != -1) {// 設定話題內容前景色高亮
ForegroundColorSpan colorSpan = new ForegroundColorSpan(
mForegroundColor);
editable.setSpan(colorSpan, findPosition, findPosition
+ objectText.length(),
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
}
/**
* 插入/設定話題
*
* @param object話題物件
*/
public void setObject(RObject object) {
if (object == null)
return;
String objectRule = object.getObjectRule();
String objectText = object.getObjectText();
if (TextUtils.isEmpty(objectText) || TextUtils.isEmpty(objectRule))
return;
// 拼接字元# %s #,並儲存
objectText = objectRule + objectText + objectRule;
object.setObjectText(objectText);
/**
* 新增話題<br/>
* 1.將話題內容新增到資料集合中<br/>
* 2.將話題內容新增到EditText中展示
*/
/**
* 1.新增話題內容到資料集合
*/
mRObjectsList.add(object);
/**
* 2.將話題內容新增到EditText中展示
*/
int selectionStart = getSelectionStart();// 游標位置
Editable editable = getText();// 原先內容
if (selectionStart >= 0) {
editable.insert(selectionStart, objectText);// 在游標位置插入內容
editable.insert(getSelectionStart(), " ");// 話題後面插入空格,至關重要
setSelection(getSelectionStart());// 移動游標到新增的內容後面
}
}
/**
* 獲取object列表資料
*/
public List<RObject> getObjects() {
List<RObject> objectsList = new ArrayList<RObject>();
// 由於儲存時候文字內容添加了匹配字元#,此處去除,還原資料
if (mRObjectsList != null && mRObjectsList.size() > 0) {
for (int i = 0; i < mRObjectsList.size(); i++) {
RObject object = mRObjectsList.get(i);
String objectText = object.getObjectText();
String objectRule = object.getObjectRule();
object.setObjectText(objectText.replace(objectRule, ""));// 將匹配規則字元替換
objectsList.add(object);
}
}
return objectsList;
}
}
3.控制元件使用
我們在xml檔案中呼叫
<cn.r.topic.edittext.android.widget.REditText
xmlns:view="http://schemas.android.com/apk/res-auto"
android:id="@+id/edittext"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minLines="5"
view:object_background_color="#FFD0AAF2"
view:object_foreground_color="#ab86cc" />
在Avtivity中呼叫。有兩個方法
1.mREditText.setObject(RObject object);// 設定話題
2.List<RObject> list = mREditText.getObjects();// 獲取話題物件集合
開發者可以直接使用,可以繼承此類拓展所需屬性
public class RObject {
private String objectRule = "#";// 匹配規則
private String objectText;// 高亮文字
public String getObjectRule() {
return objectRule;
}
public void setObjectRule(String objectRule) {
this.objectRule = objectRule;
}
public String getObjectText() {
return objectText;
}
public void setObjectText(String objectText) {
this.objectText = objectText;
}
}
public class MainActivity extends Activity implements OnClickListener {
private REditText mREditText;
private TextView mResult;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initViews();
}
public void initViews() {
mREditText = (REditText) findViewById(R.id.edittext);
mResult = (TextView) findViewById(R.id.result);
findViewById(R.id.topic_iv).setOnClickListener(this);
findViewById(R.id.send).setOnClickListener(this);
}
@Override
public void onClick(View view) {
switch (view.getId()) {
case R.id.topic_iv:
// 設定話題
setTopic();
break;
case R.id.send:
// 展示話題物件內容
getTopicsData();
break;
}
}
/**
* 新增設定話題
*
* @author Ruffian
*/
private void setTopic() {
MyTopic topic = new MyTopic();
int id = (int) (Math.random() * 100);
topic.setId("No." + id);
topic.setObjectText("雙" + id + "狂歡");// 必須設定
switch (id % 3) {
case 0:
topic.setObjectRule("*");// 開發者設定,預設#
break;
case 1:
topic.setObjectRule("$");// 開發者設定,預設#
break;
case 2:
topic.setObjectRule("#");// 開發者設定,預設#
break;
}
mREditText.setObject(topic);// 設定話題
}
/**
* 獲取話題列表資料
*
* @author Ruffian
*/
private void getTopicsData() {
/**
* 獲取話題物件集合,遍歷
*/
List<RObject> list = mREditText.getObjects();// 獲取話題物件集合
if (list == null || list.size() == 0) {
mResult.setText("no data");
return;
}
MyTopic myTopic;
StringBuffer stringBuffer = new StringBuffer();
for (int i = 0; i < list.size(); i++) {
myTopic = (MyTopic) list.get(i);// 強制轉化為開發者topic型別
stringBuffer.append("id= " + myTopic.getId() + " text= "
+ myTopic.getObjectText() + "\n");
}
mResult.setText(stringBuffer.toString());
}
/**
* 測試使用開發者話題實體,必須繼承RObject
*/
class MyTopic extends RObject {
private String id;
// 其他屬性...
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
}
}
這裡我點選按鈕隨機產生一個話題物件,插入,點擊發送,展示話題物件列表內容
好了思路和程式碼都在這裡了,各位看客完全可以在這些基礎上定製更完善的控制元件。