/* * The MIT License (MIT) * * Copyright (c) 2014-2015 Umeng, Inc * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package com.umeng.comm.ui.widgets; import java.util.Arrays; import java.util.Comparator; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import android.annotation.SuppressLint; import android.content.Context; import android.text.Editable; import android.text.SpannableStringBuilder; import android.text.TextUtils; import android.text.TextWatcher; import android.util.AttributeSet; import android.widget.EditText; import com.umeng.comm.core.beans.CommConfig; import com.umeng.comm.core.beans.CommUser; import com.umeng.comm.core.beans.Topic; import com.umeng.comm.core.utils.Log; import com.umeng.comm.core.utils.ToastMsg; import com.umeng.comm.ui.fragments.TopicPickerFragment.ResultListener; import com.umeng.comm.ui.utils.textspan.TextWrapperClickSpan; @SuppressLint("UseSparseArrays") /** * 发布Feed时的编辑视图 * TODO : 可以使用扫描法来简化插入话题、好友等操作. * */ public class FeedEditText extends EditText { /** * @好友的map */ public Map<Integer, CommUser> mAtMap = new ConcurrentHashMap<Integer, CommUser>(); /** * 话题map */ public Map<Integer, Topic> mTopicMap = new ConcurrentHashMap<Integer, Topic>(); private ResultListener<Topic> mTopicListener; boolean isMyOp = false; int lastTextLength = 0; int curTextLength = 0; public int mCursorIndex = 0; /** * @param context */ public FeedEditText(Context context) { this(context, null); } public FeedEditText(Context context, AttributeSet attrs) { this(context, attrs, 0); } /** * @param context * @param attrs * @param defStyle */ public FeedEditText(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); // 添加监听器 this.addTextChangedListener(new TextWatcher() { @Override public void onTextChanged(CharSequence s, int start, int before, int count) { Log.d(VIEW_LOG_TAG, "@@@ onTextChanged : start = " + start + ", before = " + before + ", count = " + count + ", text = " + s); if (isMyOp) { isMyOp = false; return; } // 最新的文本内容 curTextLength = s.length(); int charOffset = curTextLength - lastTextLength; if (charOffset > 0) { Log.d(VIEW_LOG_TAG, "### 插入 " + charOffset + " 个字符"); } else { Log.d(VIEW_LOG_TAG, "### 删除 " + charOffset + " 个字符"); // 删除字符, 最后一个字符 deleteFriendOrTopic(start + before - count); } if (charOffset != 0 && !isDecorating && isNeedUpdateIndex()) { updateMapIndex(start, charOffset); } } @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { Log.d(VIEW_LOG_TAG, "@@@ beforeTextChanged : start = " + start + ", after = " + after + ", count = " + count + ", text = " + s); mCursorIndex = getSelectionStart() + 1; lastTextLength = s.length(); } @Override public void afterTextChanged(Editable text) { Log.d(VIEW_LOG_TAG, "### text : " + text); Log.d(VIEW_LOG_TAG, "### 字符数 : " + getText().toString().length()); checkChars(); } }); } private void checkChars() { // int currentChars = // CommonUtils.getCharacterNums(getText().toString()); int MAX_CHARS = CommConfig.getConfig().mFeedLen; int currentChars = getText().toString().length(); int temp = currentChars - MAX_CHARS; if (temp > 0) { setText(getText().delete(MAX_CHARS, currentChars)); setSelection(MAX_CHARS); ToastMsg.showShortMsgByResName("umeng_comm_overflow_tips"); } } // 覆盖该方法的目的:避免用户在话题或者@好友文本中间插入自己输入的内容。 // 目前的处理策略是:在话题名or好友名中间插入文本,则光标自动移动到该话题名or好友名的末尾 @Override protected void onSelectionChanged(int selStart, int selEnd) { if (selStart == selEnd && selStart != 0) { boolean isMiddle = isTopicOrFriendMiddle(getSelectionStart()); if (isMiddle) { int newPos = getNextCursorPos(getSelectionStart()); if (newPos + 1 > getText().length()) { newPos = getText().length() - 1; } setSelection(newPos + 1); super.onSelectionChanged(newPos, newPos + (selEnd - selStart) + 1); } } else { super.onSelectionChanged(selStart, selEnd); } } /** * 获取当前cursor的最佳insert位置。最佳位置如下:<li>1:如果当前位置未在话题名or好友名的中间,则cursorStart为最佳位置; * <li>2:如果当前位置在话题名or好友名的中间,则最佳位置为该话题名or好友名的末尾</br> * * @param cursorStart 光标的当前位置 * @return 最佳的光标insert位置 */ private int getNextCursorPos(int cursorStart) { // 检查插入的字符是否在话题中间 Set<Entry<Integer, Topic>> entries = mTopicMap.entrySet(); for (Entry<Integer, Topic> entry : entries) { int start = entry.getKey(); int offset = entry.getValue().name.length(); if (cursorStart > start && cursorStart < start + offset) { return start + offset; } } // 检查插入的字符是否在@好友中间 Set<Entry<Integer, CommUser>> friendsEntries = mAtMap.entrySet(); for (Entry<Integer, CommUser> entry : friendsEntries) { int start = entry.getKey(); int offset = entry.getValue().name.length(); if (cursorStart > start && cursorStart < start + offset) { return start + offset; } } return cursorStart; } /** * 当前的位置{@link textStart}是否在某个话题名or好友名的中间</br> * * @param textStart 当前光标位置 * @return true,当前光标位置在话题名or好友名的中间;否则返回false */ private boolean isTopicOrFriendMiddle(int textStart) { // 检查插入的字符是否在话题中间 Set<Entry<Integer, Topic>> entries = mTopicMap.entrySet(); for (Entry<Integer, Topic> entry : entries) { int start = entry.getKey(); int offset = entry.getValue().name.length(); if (textStart > start && textStart < start + offset) { return true; } } // 检查插入的字符是否在@好友中间 Set<Entry<Integer, CommUser>> friendsEntries = mAtMap.entrySet(); for (Entry<Integer, CommUser> entry : friendsEntries) { int start = entry.getKey(); int offset = entry.getValue().name.length(); if (textStart > start && textStart < start + offset) { return true; } } return false; } /** * @param start * @param text */ public void deleteElement(int start, String text) { Editable editableText = getText(); // +1表示每个话题或者好友name后的一个空格 final int deleteLength = text.length() + 1; if (start + deleteLength > this.length()) { Log.d(VIEW_LOG_TAG, "### 删除的文字超过了原来的长度"); return; } isDecorating = true; // 删除这个区域的文本 Editable newEditable = editableText.delete(start, start + deleteLength); setText(newEditable); // 更新索引 updateMapIndex(start, -deleteLength); isDecorating = false; mCursorIndex = newEditable.length(); setSelection(mCursorIndex); } /** * @param topic */ public void removeTopic(final Topic topic) { Iterator<Integer> iterator = mTopicMap.keySet().iterator(); while (iterator.hasNext()) { Integer key = iterator.next(); Topic curTopic = mTopicMap.get(key); if (topic != null && curTopic != null && curTopic.equals(topic)) { iterator.remove(); // deleteElement(key, topic.name); break; } } // mTopicMap.values().remove(topic); } /** * @param start * @param charOffset 大于0代表插入数据, 后面的索引向后移动; 小于0,代表删掉字符,后面的索引向前移动. */ private void updateAtMap(int start, int charOffset) { // @好友 Object[] keysStrings = mAtMap.keySet().toArray(); // 对于key排序,升序排列,这样便于删除某个@时更新后面的数据索引 Arrays.sort(keysStrings, new Comparator<Object>() { @Override public int compare(Object lhs, Object rhs) { return ((Integer) lhs) - ((Integer) rhs); } }); int myStart = start; // 迭代索引 for (int i = 0; i < keysStrings.length; i++) { // key int keyIndex = (Integer) keysStrings[i]; // value CommUser item = mAtMap.get(keyIndex); // 后移 if (keyIndex >= start && (keyIndex + charOffset) <= getText().length()) { mAtMap.put(keyIndex + charOffset, item); mAtMap.remove(keyIndex); } // log Log.d(VIEW_LOG_TAG, "### updateAtMap的item, keyIndex = " + keyIndex + ", item = " + item + ", myStart = " + myStart + ", charOffset = " + charOffset); } // end for Log.d(VIEW_LOG_TAG, "@@@ @好友的map : " + mAtMap); } /** * @param start * @param before * @param count */ private void updateTopicMap(int start, int charOffset) { // 话题 Object[] keysStrings = mTopicMap.keySet().toArray(); Arrays.sort(keysStrings); int myStart = start; // 迭代索引 for (int i = 0; i < keysStrings.length; i++) { // key int keyIndex = (Integer) keysStrings[i]; // value Topic item = mTopicMap.get(keyIndex); int newIndex = keyIndex + charOffset; // 更新索引 if (keyIndex >= start && newIndex >= 0) { mTopicMap.remove(keyIndex); mTopicMap.put(newIndex, item); } // log Log.d(VIEW_LOG_TAG, "### updateTopicMap的item, keyIndex = " + keyIndex + ", item = " + item + ", myStart = " + myStart + ", charOffset = " + charOffset); } // end for Log.d(VIEW_LOG_TAG, "@@@ 话题的map : " + mTopicMap); } /** * @param start * @param charOffser */ private void updateMapIndex(int start, int charOffset) { try { updateAtMap(start, charOffset); updateTopicMap(start, charOffset); } catch (Exception e) { e.printStackTrace(); } } /** * @param start * @param end */ private void deleteText(int start, int end) { Editable originText = getText(); int originLength = originText.length(); if (originLength >= end - 1) { originText.delete(start, end - 1); setText(originText); setSelection(start); } } /** * @param last */ private void deleteFriendOrTopic(int last) { Log.d(VIEW_LOG_TAG, "### 删除字符 last " + last); Set<Entry<Integer, CommUser>> atKeys = mAtMap.entrySet(); // Iterator<Entry<Integer, CommUser>> iterator = atKeys.iterator(); while (iterator.hasNext()) { Map.Entry<java.lang.Integer, CommUser> entry = iterator .next(); int temp = entry.getKey() + entry.getValue().name.length(); Log.d(VIEW_LOG_TAG, "### index = " + temp + ", edit last = " + last); // 找到以后,删除好友名字 if (temp + 1 == last) { iterator.remove(); isMyOp = true; // 删除文本 deleteText(entry.getKey(), last); // updateTopicMap(start, charOffset); mFriendsListener.onRemove(entry.getValue()); return; } } Log.d(VIEW_LOG_TAG, "### topic map : " + mTopicMap); // topic Set<Entry<Integer, Topic>> topicKeys = mTopicMap.entrySet(); // Iterator<Entry<Integer, Topic>> topicIterator = topicKeys.iterator(); // while (topicIterator.hasNext()) { Map.Entry<java.lang.Integer, Topic> entry = topicIterator .next(); int temp = entry.getKey() + entry.getValue().name.length(); Log.d(VIEW_LOG_TAG, "### topic index = " + temp + ", edit last = " + last); // 找到以后,删除话题 if (temp == last) { topicIterator.remove(); isMyOp = true; // 删除文本 deleteText(entry.getKey(), last); if (mTopicListener != null) { mTopicListener.onRemove(entry.getValue()); } return; } } } /** * 是否需要更新索引。由于insert话题时有一个空格,此处在检查时做-1处理 * * @return */ private boolean isNeedUpdateIndex() { int start = getSelectionStart(); Log.d(VIEW_LOG_TAG, "#### isNeedUpdateIndex, start = " + start); Set<Integer> atKeys = mAtMap.keySet(); for (Integer integer : atKeys) { Log.d(VIEW_LOG_TAG, "#### isNeedUpdateIndex,at index = " + integer); if (integer >= start - 1) { return true; } } Set<Integer> topicKeys = mTopicMap.keySet(); for (Integer integer : topicKeys) { Log.d(VIEW_LOG_TAG, "#### isNeedUpdateIndex,topic index = " + integer); if (integer >= start - 1) { return true; } } return false; } boolean isDecorating = false; /** * */ private void decorateText() { isDecorating = true; // SpannableStringBuilder ssb = new SpannableStringBuilder(getText()); // 已经存在的话题先包装 Set<Integer> topicKeySet = mTopicMap.keySet(); for (Integer atIndex : topicKeySet) { Topic topic = mTopicMap.get(atIndex); if (topic != null) { // ViewUtils.wrapString(ssb, atIndex, topic.name); ssb.setSpan(new TextWrapperClickSpan(), atIndex, atIndex + topic.name.length(), 0); } } // @好友的wrap Set<Integer> keySet = mAtMap.keySet(); for (Integer atIndex : keySet) { CommUser atUser = mAtMap.get(atIndex); if (atUser != null && !TextUtils.isEmpty(atUser.name)) { // ViewUtils.wrapString(ssb, atIndex, "@" + atUser.name + " "); String atName = "@" + atUser.name + " "; ssb.setSpan(new TextWrapperClickSpan(), atIndex, atIndex + atName.length(), 0); } } setText(ssb); } /** * 封装赞的TextView * * @param str * @return */ public void atFriends(List<CommUser> friends) { if (friends == null) { return; } // removeNonexistFriend(friends); // 光标的位置 int editSection = getCursorPos(); Log.d("", "### atFriends, start = " + editSection); SpannableStringBuilder ssb = new SpannableStringBuilder(getText()); int count = friends.size(); for (int i = 0; i < count; i++) { final CommUser user = friends.get(i); // 如果已经有了该好友,则直接返回 if (mAtMap.containsValue(user)) { continue; } isDecorating = true; final String name = "@" + user.name + " "; ssb.insert(editSection, name); setText(ssb); // 更新map索引 updateMapIndex(editSection, name.length()); // 将所有at的位置存储在map中 mAtMap.put(editSection, user); // 更新起始点 editSection += name.length(); } // 包装文本 decorateText(); isDecorating = false; // 此时将光标移动到文本末尾 setSelection(getText().length()); mCursorIndex = editSection; } // end of atFriends /** * 获取光标位置</br> * * @return */ private int getCursorPos() { int editSection = getSelectionStart(); // 判断当前光标的位置是否在一个好友name中间。如果处于name中间,则将光标移动至name的末尾再insert好友name Set<Entry<Integer, CommUser>> entries = mAtMap.entrySet(); for (Entry<Integer, CommUser> entry : entries) { int start = entry.getKey(); int offset = entry.getValue().name.length() + 1; if (editSection > start && editSection < start + offset) { editSection = start + offset; break; } } return editSection; } /** * 移除不存在的好友。在用户选择@好友后,再次选择时删除了某些好友的情况</br> * * @param newFriends */ // private void removeNonexistFriend( List<CommUser> newFriends){ // Collection<CommUser> oldUsers = mAtMap.values(); // Iterator<CommUser> iterator = oldUsers.iterator(); // while (iterator.hasNext() ) { // CommUser user = iterator.next(); // String context = getText().toString(); // if ( !newFriends.contains(user) ) { // String name = "@" + user.name ; // int start = context.indexOf(name); // if ( start >= 0 ) { // iterator.remove(); // deleteElement(start, name); // } // } // } // } /** * 封topic的TextView * * @param str * @return */ public void insertTopics(List<Topic> topics) { if (topics == null || topics.size() == 0) { return; } // 光标的位置 int editSection = getSelectionStart(); Log.d("", "### insertTopicText, start = " + editSection); SpannableStringBuilder ssb = new SpannableStringBuilder(getText()); int count = topics.size(); for (int i = 0; i < count; i++) { final Topic topicItem = topics.get(i); final String name = topicItem.name + " "; // 如果已经有了该好友,则直接返回 if (mTopicMap.containsValue(topicItem)) { continue; } isDecorating = true; int ssbLength = ssb.length(); if (editSection > ssbLength) { editSection = ssbLength; } ssb.insert(editSection, name); setText(ssb); // 更新map索引 updateMapIndex(editSection, name.length()); Log.d("#####", "#####put start : " + editSection); // 将所有at的位置存储在map中 mTopicMap.put(editSection, topicItem); // 更新起始点 editSection += name.length(); } // 包装文本 decorateText(); isDecorating = false; setSelection(editSection); mCursorIndex = editSection; } /** * @param listener */ public void setTopicListener(ResultListener<Topic> listener) { mTopicListener = listener; } public ResultListener<CommUser> mFriendsListener; public void setResultListener(ResultListener<CommUser> listener) { mFriendsListener = listener; } }