Android仿新浪微博加#话题的EditText实现

需求

产品上线了,项目差不多算是稳定下来了,接下来就是一个个的版本迭代了。这周又增加了几个新功能,其中一个就是题目中讲的,要仿新浪微博(如下图)的输入框里的文字效果。作为整体的字体两边是井号,并且包括井号要变色,删除的时候,当光标到右侧的井号,再次点击删除的时候,会将井号包裹的内容作为一个整体删除掉,同时焦点不能在变色的字符之间,也就是说当点击变色的字符时,光标会自动地落在井号两侧。

拆分问题

看上去是一个挺简单合理的需求,随便一想,这不就是字符串匹配嘛!可是实际行动起来,就会发现各种坑爹问题,远不是想象中的那么简单。我这做下来的感受是,必须要有一个明确清晰的思路,总结下来,其实可以分为下面几个步骤。

  • 首先,新浪微博这个功能的入口是,点击井号,进入到新的界面里选择字符串,然后自动添加到 EditText 框里,我们要实现这样一个 Demo ,也没必要进入新的界面,直接做一个按钮,点击添加即可;
  • 然后,需要明确的一点是,在 Android 提供的原生的 API 里,删除动作,一次只能删掉一个字符,而无法删除批量的字符,一次只能删除一个 letter ,而不是一个 word ,删除动作是我们这个课题的关键所在;
  • 处理完了增加和删除字符串功能,接着就是匹配符合规则的字符串做变色处理了;
  • 再接着就是处理点击字符串,光标的所处位置的问题;
  • 完善需要补充和修改的细节。

通过以上五个步骤将课题拆分为四个小问题,然后再去一一解决即可。

添加字符串

这一步基本上算是最简单的一步了。我们只需要做到点击一个 button ,将字符串 append 到 EditText 里就可以了。当然,为了让我们的操作更加有广泛的适用性,单纯地增加字符串就有点太不够看了,而实际应用里,这样做基本上也没有什么意义。

我们要做的就是,创建一个实体类,而这个字符串作为一个值,所对应的变量就是这个实体类的成员变量,我们通过不断往实体类集合里添加 new 出来的对象,来往 EditText 里追加字符串。同时,这样做还有一个好处就是,实际应用里,往往伴随字符串使用的可能还有其他的变量,要处理与字符串一一对应的其他变量,最好的方法就是在增加和删除字符串的同时,将字符串对应变量所在实体从实体集合中移除掉。

下面就是我的实体类:

/** 实体类 */
public class BookEntity implements Serializable {
	private static final long serialVersionUID = 1L;
	private String bookId;
	private String bookName;
    public BookEntity(String bookName, int bookId) {
		super();
		this.bookName = bookName;
		this.bookId = bookId;
	}
	public String getBookId() {
		return bookId;
	}
	public void setBookId(String bookId) {
		this.bookId = bookId;
	}
	public String getBookName() {
		return bookName;
	}
	public void setBookName(String bookName) {
		this.bookName = bookName;
	}
}

添加字符操作:

/** 执行增加字符串的操作 */
private Button button;
private ArrayList<BookEntity> mList = new ArrayList<BookEntity>();
private EditText editText;
View view = inflater.inflate(R.layout.fragment_main, container, false);
editText = (EditText) view.findViewById(R.id.edit_text);
button = (Button) view.findViewById(R.id.bt);
buttont.setOnClickListener(new OnClickListener() {
	@Override
	public void onClick(View v) {
		int nextInt = new Random().nextInt(100);
		String str = "#测试测试" + nextInt + "# ";
		editText.setText(editText.getText());
		editText.append(str);
		editText.setSelection(editText.getText().toString().length());
		mList.add(new BookEntity(str, nextInt));
	}
});

这样我们增加字符串操作就做完了。

删除符合条件的字符串

删除操作是这个小程序的关键。做删除操作时,我们需要搞清楚这样几个问题:什么时候删除?从哪儿开始删?删到哪里算结束?删除结束之后,光标位置应该在哪里?接下来一个一个解决。

  1. 删除操作当然应该在按下软键盘的删除键时执行,这里有一个细节,在 Android 点击键盘按键的事件,其实是分开处理的,按下键盘时会出发动作,弹起键盘时同样会触发动作;
  2. 如果是不符合筛选条件(”#”+bookEntity.getBookName()+”#”),点击一次就删除一个字符;如果符合条件,就一次性将整个带左右井号的字符串都删掉;
  3. 而光标则应该一直保持在最末尾处。
/** 监听删除按键,执行删除动作 */
editText.setOnKeyListener(new OnKeyListener() {
	@Override
	public boolean onKey(View v, int keyCode, KeyEvent event) {
		if (keyCode == KeyEvent.KEYCODE_DEL && event.getAction() == KeyEvent.ACTION_DOWN) { //当为删除键并且是按下动作时执行
			int selectionStart = editText.getSelectionStart();
			int lastPos = 0;
			for (int i = 0; i < mList.size(); i++) { //循环遍历整个输入框的所有字符
				if ((lastPos = editText.getText().toString().indexOf(mList.get(i).getBookName(), lastPos)) != -1) {
					if (selectionStart != 0 && selectionStart >= lastPos && selectionStart <= (lastPos + mList.get(i).getBookName().length())) {
						String sss = editText.getText().toString();
						editText.setText(sss.substring(0, lastPos) + sss.substring(lastPos + mList.get(i).getBookName().length())); //字符串替换,删掉符合条件的字符串
						mList.remove(i); //删除对应实体
						editText.setSelection(lastPos); //设置光标位置
						return true;
					}
				} else {
						lastPos += ("#" + mList.get(i).getBookName() + "#").length();
				}
			}
		}
		return false;
	}
});

处理变色问题

变色,使符合条件的字符串颜色高亮,更多像是锦上添花的功能,给用户更直观的感知,却是整个小程序里实现起来最复杂的部分。由于 Android 自身的一些原因,无论是增加还是删除,或者是锁屏等事件,都会造成界面重绘的问题,因此其核心就在于要设置字符改变的监听状态,当字符改变时,剩余的字符的颜色随之发生变化。

具体的实现,我们需要一个 TextWatcher 的实现类,然后 new 出来一个对象,作为参数给控件 editText 设置的 addTextChangedListener 监听。

class MyTextWatcher implements TextWatcher {
	@Override
	public synchronized void afterTextChanged(Editable s) {
		AddNewArticleUI.this.etWriteDynamic.removeTextChangedListener(watcher);
		TEXT_CHANGE_LISTENER_FLAG = 0;
		int findPos = 0;
		int copyPos = 0;
		String sText = s.toString();
		List<Integer> spanIndexes = new ArrayList<Integer>();
		s.clear();
		for (int i = 0; i < bookList.size(); i++) {
			String tempBookName = "#" + bookList.get(i).getBookName() + "#";
			if ((findPos = sText.indexOf(tempBookName, findPos)) != -1) {
				spanIndexes.add(findPos);//bookName 的开始索引,键值为偶数,从0开始
				spanIndexes.add(findPos + tempBookName.length()); //bookName 的结束索引,键值为奇数,从1开始
			}
		}
		if (spanIndexes != null && spanIndexes.size() != 0) {
			for (int i = 0; i < spanIndexes.size(); i++) {
				if (i % 2 == 0) {
					s.append(sText.substring(copyPos, spanIndexes.get(i)));
				} else {
					Spanned htmlText = Html.fromHtml("<font color=‘blue‘>" + sText.substring(copyPos, spanIndexes.get(i)) + "</font>");
					s.append(htmlText);
				}
				copyPos = spanIndexes.get(i);
			}
			s.append(sText.substring(copyPos));
		} else {
			s.append(sText);
		}
	}

	@Override
	public void beforeTextChanged(CharSequence s, int start, int count, int after) {}

	@Override
	public void onTextChanged(CharSequence s, int start, int before, int count) {}
}

这里需要注意的是在 addTextChangedListener 的 afterTextChange 方法里,不能够操作 Editable 去给控件设置值,而且也不能在全局设置 TextChanged 监听,否则必然会因为循环调用 StackOverFlow 。也就是说我们只能在文字改变之前去调用,还要发屏幕解锁屏广播,在屏幕解锁时去调用。

private int TEXT_CHANGE_LISTENER_FLAG = 0;
/** 监听文字变化,并重新设置颜色 */
if (TEXT_CHANGE_LISTENER_FLAG == 0) {
	editText.addTextChangedListener(watcher);
	TEXT_CHANGE_LISTENER_FLAG = 1;
}

下面是解锁屏的广播(此段代码来自网络)

public class ScreenListener {
	private Context mContext;
	private ScreenBroadcastReceiver mScreenReceiver;
	private ScreenStateListener mScreenStateListener;

	public ScreenListener(Context context) {
		mContext = context;
		mScreenReceiver = new ScreenBroadcastReceiver();
	}

	/**
	 * screen状态广播接收者
	 */
	private class ScreenBroadcastReceiver extends BroadcastReceiver {
		private String action = null;

		@Override
		public void onReceive(Context context, Intent intent) {
			action = intent.getAction();
			if (Intent.ACTION_SCREEN_ON.equals(action)) { // 开屏
				mScreenStateListener.onScreenOn();
			} else if (Intent.ACTION_SCREEN_OFF.equals(action)) { // 锁屏
				mScreenStateListener.onScreenOff();
			} else if (Intent.ACTION_USER_PRESENT.equals(action)) { // 解锁
				mScreenStateListener.onUserPresent();
			}
		}
	}

	/**
	 * 开始监听screen状态
	 *
	 * @param listener
	 */
	public void begin(ScreenStateListener listener) {
		mScreenStateListener = listener;
		registerListener();
		getScreenState();
	}

	/**
	 * 获取screen状态
	 */
	private void getScreenState() {
		PowerManager manager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
		if (manager.isScreenOn()) {
			if (mScreenStateListener != null) {
				mScreenStateListener.onScreenOn();
			}
		} else {
			if (mScreenStateListener != null) {
				mScreenStateListener.onScreenOff();
			}
		}
	}

	/**
	 * 停止screen状态监听
	 */
	public void unregisterListener() {
		mContext.unregisterReceiver(mScreenReceiver);
	}

	/**
	 * 启动screen状态广播接收器
	 */
	private void registerListener() {
		IntentFilter filter = new IntentFilter();
		filter.addAction(Intent.ACTION_SCREEN_ON);
		filter.addAction(Intent.ACTION_SCREEN_OFF);
		filter.addAction(Intent.ACTION_USER_PRESENT);
		mContext.registerReceiver(mScreenReceiver, filter);
	}

	public interface ScreenStateListener {// 返回给调用者屏幕状态信息
		public void onScreenOn();

		public void onScreenOff();

		public void onUserPresent();
	}
}

注册广播

ScreenListener screenListener = new ScreenListener(this);
screenListener.begin(new ScreenStateListener() {

    @Override
    public void onUserPresent() {
        Log.e("onUserPresent", "onUserPresent");
    }

    @Override
    public void onScreenOn() {
        Log.e("onScreenOn", "onScreenOn");
        if (TEXT_CHANGE_LISTENER_FLAG == 0) {
			editText.addTextChangedListener(watcher);
			TEXT_CHANGE_LISTENER_FLAG = 1;
		}
    }

    @Override
    public void onScreenOff() {
        Log.e("onScreenOff", "onScreenOff");
    }
});

最后,不要忘记取消注册广播。

设置光标位置

最后就是在手指触摸到所匹配的字符时,设置光标的位置,其实跟上面的删除处理逻辑一样,只是不需要再去替换字符串,判断的逻辑都是一样的。

ditText.setOnClickListener(new OnClickListener() {
	@Override
	public void onClick(View v) {
		Log.i("TAG", ((EditText) v).getSelectionStart() + "");
		int selectionStart = ((EditText) v).getSelectionStart();
		int lastPos = 0;
		for (int i = 0; i < mList.size(); i++) {
			if ((lastPos = editText.getText().toString().indexOf(mList.get(i).getBookName(), lastPos)) != -1) {
				if (selectionStart >= lastPos && selectionStart <= (lastPos + mList.get(i).getBookName().length())) {
					editText.setSelection(lastPos + mList.get(i).getBookName().length());
				}
			} else {
					lastPos += ("#" + mList.get(i).getBookName() + "#").length();
			}
		}
	}
});

至此,一个类似新浪微博输入框的小程序就完成了。当然比起新浪微博,这里少了一步点击删除第一次选中被匹配到字符串,这个功能,新浪微博的实现是,点击删除,先选中字符串,再点击删除,就删掉字符串。具体情况,不再赘述。更多的技术文章请时时关注爱开发APP源码论坛

时间: 2024-08-09 06:29:25

Android仿新浪微博加#话题的EditText实现的相关文章

Android仿美团加载数据 小人奔跑进度动画对话框(附顺丰快递员奔跑效果)

我们都知道在Android中,常见的动画模式有两种:一种是帧动画(Frame Animation),一种是补间动画(Tween Animation).帧动画是提供了一种逐帧播放图片的动画方式,播放事先做好的图像,与gif图片原理类似,就像是在放电影一样.补间动画可以实现View组件的移动.放大.缩小以及渐变等效果. 今天我们主要来模仿一下美团中加载数据时小人奔跑动画的对话框效果,取个有趣的名字就是Running Man,奔跑吧,兄弟!话不多少,先上效果图,让各位大侠看看是不是你想要实现的效果,然

Android Studio 第八十期 - Android 仿微博加载中布局

代码已经整理好,效果如下图: 地址:https://github.com/geeklx/myapplication2018/tree/master/p024_shimmerlayout 原文地址:http://blog.51cto.com/liangxiao/2153326

android 仿小米icon处理,加阴影和边框

本人自己在做一个launcher,所以需要处理icon,加阴影和边框等.这只是一种处理方法,其他的处理方法类似. 源代码: https://github.com/com314159/LauncherIconMaskEffect 效果图: 核心思想: 1. 绘制边框时,先取原图片的颜色,再根据颜色绘制边框,加上了渐变效果 2.添加阴影其实非常简单,只是加了一张背景图片 android 仿小米icon处理,加阴影和边框,布布扣,bubuko.com

【Android自定义View实战】之仿百度加载动画,一种优雅的Loading方式

转载请注明出处:http://blog.csdn.net/linglongxin24/article/details/53470872 本文出自[DylanAndroid的博客] Android自定义View实战之仿百度加载动画一种优雅的Loading方式 第一个仿百度加载动画用ObjectAnimator属性动画操作ImageView的属性方法实现 第二个仿百度加载动画第二种实现方式用ValueAnimator原生的ondraw方法实现 第三个扔球动画-水平旋转动画 第四个扔球动画-垂直旋转动

ASP.NET仿新浪微博下拉加载更多数据瀑布流效果

闲来无事,琢磨着写点东西.貌似页面下拉加载数据,瀑布流的效果很火,各个网站都能见到各式各样的展示效果,原理大同小异.于是乎,决定自己写一写这个效果,希望能给比我还菜的菜鸟们一点参考价值. 在开始之前,先把实现的基本原理说一下.当夜幕下拉到底部的时候,js可以判断滚动条的位置,到达底部触发js方法,执行jquery的ajax方法,向后台一般处理程序夜幕ashx文件请求数据源,得到json格式的数据源.然后,遍历json数据源,拼接一个li标签,再填充到页面上去. 首先,我们来做个简单的html页面

Android自定义TabActivity(实现仿新浪微博底部菜单更新UI)

如今Android上很多应用都采用底部菜单控制更新的UI这种框架,例如新浪微博 点击底部菜单的选项可以更新界面.底部菜单可以使用TabHost来实现,不过用过TabHost的人都知道自定义TabHost究竟是有多麻烦的,原生TabHost的风格是不依附屏幕的底部的,要依附底部就要重写布局. TabHost设置的Container可以管理UI的显示,UI可以用LayoutInflater动态生成,也可以是Activity,但不好管理Activity的生命周期.然后用TabHost控制显示UI的显示

android开发新浪微博客户端 完整攻略 [新手必读]

开始接触学习android已经有3个礼拜了,一直都是对着android的sdk文档写Tutorials从Hello World到Notepad Tutorial算是初步入门了吧,刚好最近对微博感兴趣就打算开发个android版本的新浪微博客户端作为练手项目,并且以随笔的方式详细的记录开发的全过程.本人对java语言以及eclipse Ide都是初次应用基本上属于边学边用,做移动设备上的东西也是第一次,总的来说属于无基础.无经验.无天赋的纯三无人员,还请广大同学们多多给予指点. 开发第一件事情,那

android 仿ppt进入动画效果合集

EnterAnimation android 仿ppt进入动画效果合集, 百叶窗效果,擦除效果,盒状效果,阶梯效果,菱形效果,轮子效果,劈裂效果,棋盘效果, 切入效果,扇形展开效果,十字扩展效果,随机线条效果,向内溶解效果,圆形扩展效果, 适用于各种view和viewgroup,activity即用于页面根部viewgroup, 自定义viewgroup自动换行layout, 看效果图 Series of entrance animation effects just like ppt in A

jQuery练手:仿新浪微博图片文字列表淡进淡出上下滚动效果

1.效果及功能说明 仿新浪微博图片文字列表上下淡进淡出间歇上下滚动 2.实现原理 首先要设定div内只能显示4个图片那么多出来的图片会自动隐藏然后在给图片添加一个动画的事件让他们可以滚动的播放出来上下滚动效果播放就是li标签里面的内容图片和文字把每一个li看成一个整体在滚动播放的时候进入div内的显示出来在最后离开div的时候隐藏在给整个动画效果设定一个时间就可以完整的运行. 3.运行环境 IE6 IE7 IE8及以上 Firefox 和 Google Chrome游览器下都可实现 4.所有图片