1.概述
最近看到网上很多根据图灵聊天机器人的API实现在客户端实现的聊天程序,于是我也借鉴并做出了自己的聊天机器人,觉得挺有意思的,也是希望以后人工智能的发展会更加美好。正所谓no picture you say a jb! 好吧我贴上做好的效果图:
制作该程序我们需要用到图灵机器人为我们提供的API,所以我们要去官网注册下,注册好之后在个人中心里找到我们所需的API KEY这个我们稍后会用到。具体的调用方式我就不再这里叙述,可以去官网的平台接入查看具体的调用方式。
涉及内容:
1.网络API的调用
2.异步通信的机制
3.json的解析
4.ListVIew多视图的布局方式
2.异步获取数据
根据上面的步骤你应该已经完成了注册,剩下的就是如何根据这个现有的API,实现我们自己的聊天机器人了。
首先我们需要把注册好的appkey获取,在浏览器中输入:
http://www.tuling123.com/openapi/api?key=<你自己的APP KEY>&info=<对话的内容>
不出意外我们会看到形如:
{"code":100000,"text":"今天晴转多云 13~-1°C明天多云 10~1°C后天多云转晴 13~0°C"}
这样格式的json字符串,我们在浏览器中查看是为了对于将来要返回的json字符串有一个感性的认识。(ps:以上数据是我查询某地的天气情况获取的数据,当然我们也可以去官网体验聊天)
既然浏览器中获取没问题,那么接下来就是我们要在Android程序中获取这样的信息了,做过的Android的同学应该都知道,Android的网络访问是不允许在UI线程中的(ps:古老的版本可以,当然我们要与时俱进吗),这样我们就有几种方案可供选择了:
1.Handler + Thread,比较方便的实现方式效率也最高,直接new一个线程,然后用handler去处理消息。
2.asynctask,比较简单快捷的实现方式,很容易理解,我自己是很喜欢的一种方式也常用。
3.Handler + HandlerThread,结构比较复杂,但是结构功能清晰,用的较少。
这里因为聊天是不断的访问网络的,第一种方式和第二种方式都是一次性消费品,发送一次信息都会启动一个线程或是启动一个异步任务,接收到消息线程和异步任务就会销毁。为了能重复利用我们这里采用第三种方式,并实现自己的消息循环,这样我们就会有自己的聊天工作线程了。(ps:其实这里用asynctask或是new一个Thread也是很方便的,这里主要是本人对于消息处理有一种学习的态度,如果自己需要快速的实现还是建议用asynctask)
消息处理
/**
* 聊天工作线程
*/
public class ChatThread extends HandlerThread {
private static final String TAG = "ChatThread";
/**
* 下载
*/
private static final int MESSAGE_DOWNLOAD = 0;
/**
* 工作线程处理消息Handler
*/
private Handler mHandler;
/**
* 更新UI线程的Handler
*/
private Handler responseHandler;
/**
* 回调接口
*/
private MessageListener mListener;
/**
* 聊天信息回调接口
*/
public interface MessageListener{
void onChatMessage(String revMsg);
}
public void setListener(MessageListener listener) {
mListener = listener;
}
public ChatThread(Handler handler) {
super(TAG);
this.responseHandler = handler;
}
/**
* 添加发送消息
*/
public void queueMessage(String url){
Log.i(TAG, "发送url:" + url);
mHandler.obtainMessage(MESSAGE_DOWNLOAD,url).sendToTarget();
}
@SuppressLint("HandlerLeak") @Override
protected void onLooperPrepared() {
mHandler = new Handler(){
@Override
public void handleMessage(Message msg) {
if (msg.what==MESSAGE_DOWNLOAD) {
String sendMsg = (String) msg.obj;
handleRequest(sendMsg);
}
}
};
}
/**
* 处理请求
*/
private void handleRequest(String url){
try {
final String revString = HttpUtils.getUrlString(url);
responseHandler.post(new Runnable() {
@Override
public void run() {
mListener.onChatMessage(revString);
}
});
} catch (IOException e) {
e.printStackTrace();
Log.e(TAG, "Exception:",e);
}
}
/**
* 清空消息队列
*/
public void clearQueue() {
mHandler.removeMessages(MESSAGE_DOWNLOAD);
}
}
工作流程:
1.首先我们需要在主界面中启动线程
/**
* 设置聊天工作线程
*/
mChatThread = new ChatThread(new Handler());
mChatThread.setListener(new MessageListener() {
@Override
public void onChatMessage(String revMsg) {
}
});
mChatThread.start();
mChatThread.getLooper();
启动线程并获取Looper对象实例,让聊天线程拥有消息循环。
2.发送消息
/**
* 添加发送消息
*/
public void queueMessage(String url){
Log.i(TAG, "发送url:" + url);
mHandler.obtainMessage(MESSAGE_DOWNLOAD,url).sendToTarget();
}
该方法是主界面调用的入口,向工作线程添加一条消息,交由工作线程处理。
3.handler处理消息
@SuppressLint("HandlerLeak") @Override
protected void onLooperPrepared() {
mHandler = new Handler(){
@Override
public void handleMessage(Message msg) {
if (msg.what==MESSAGE_DOWNLOAD) {
String sendMsg = (String) msg.obj;
handleRequest(sendMsg);
}
}
};
}
处理消息并获取网络内容
4.通知UI线程数据返回
/**
* 处理请求
*/
private void handleRequest(String url){
try {
final String revString = HttpUtils.getUrlString(url);
responseHandler.post(new Runnable() {
@Override
public void run() {
mListener.onChatMessage(revString);
}
});
} catch (IOException e) {
e.printStackTrace();
Log.e(TAG, "Exception:",e);
}
}
这里需要注意:responseHandler是UI线程的handler,Handler.post(Runnable)是一个张贴Message的
便利方法,运行在UI线程,调用回调接口将数据传出。当然我们也可以当做消息将它发送给主线程,然后主线程再用handler来处理信息,但是这样就更加复杂了,所以这里采用回调接口的方式。
好了以上就是这个聊天工作线程的实现以及简单的讲解,我在这里也不去深入的说明handler、MessageQueue以及Looper的关系了,想要更加深入的了解这些机制还是去google吧,或者自己去看看源代码这样会更好,我们主要还是讲实际过程中的用法。
3.主界面的实现
网络操作以及数据的准备都弄好了接下来就是我们的主界面的实现过程了,实际上界面布局就是一个ListView剩下的我也不想多说了直接看源代码吧。
/**
* 聊天主界面
*/
public class MainActivity extends Activity {
private static final String TAG = "MainActivity";
private static final String ENDPOINT = "http://www.tuling123.com/openapi/api";
private static final String APPKEY = "d545d3de184f5ae82f1090b83f965571";
private static final String KEY = "key";
private static final String INFO = "info";
private static final String USERID = "userid";
List<ChatMessage> lists;
private ListView mListView;
private EditText mEditText;
private Button mButton;
private TextAdapter mTextAdapter;
private String[] welcome_array;
private double currentTime, oldTime = 0;
private ChatThread mChatThread;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
}
/**
* 初始化视图
*/
private void initView() {
mListView = (ListView) findViewById(R.id.lv);
mEditText = (EditText) findViewById(R.id.sendText);
mButton = (Button) findViewById(R.id.sendBtn);
lists = new ArrayList<ChatMessage>();
/**
* 设置聊天工作线程
*/
mChatThread = new ChatThread(new Handler());
mChatThread.setListener(new MessageListener() {
@Override
public void onChatMessage(String revMsg) {
parseJson(revMsg);
}
});
mChatThread.start();
mChatThread.getLooper();
// 为发送按钮设置监听器
mButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
getTime();
// 获取发送的内容
String content_text = formatContent(mEditText.getText()
.toString());
mEditText.setText("");
// 将用户发送的数据放入列表中
ChatMessage chatMessage = new ChatMessage(content_text, ChatMessage.SEND,
getTime());
lists.add(chatMessage);
/*
* 当lists数据超过30条移除十条记录
* 这里需要注意下List集合是顺序列表循环移除只需把第0项移除即可
*/
if (lists.size() > 30) {
for (int i = 0; i < 10; i++) {
lists.remove(0);
}
}
// 数据刷新
dataSetChanged();
// 将发送的消息传入消息工作线程,加入消息队列并等待返回
mChatThread.queueMessage(construcUrl(content_text));
}
});
// ListView绑定数据适配器
mTextAdapter = new TextAdapter();
mListView.setAdapter(mTextAdapter);
// 为lists添加欢迎语
lists.add(new ChatMessage(getRandomWelcomeTips(), ChatMessage.RECEIVER,
getTime()));
}
/**
* 获取显示时间
*/
private String getTime() {
currentTime = System.currentTimeMillis();
SimpleDateFormat format = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss");
Date curDate = new Date();
String curStr = format.format(curDate);
if (currentTime - oldTime > 1 * 60 * 1000) {
oldTime = currentTime;
return curStr;
} else {
oldTime = currentTime;
return null;
}
}
/**
* 去掉发送空格和回车符
*/
private String formatContent(String content) {
return content.replace(" ", "").replace("\n", "");
}
/**
* 欢迎语获取
*/
private String getRandomWelcomeTips() {
String welcome_tip = null;
welcome_array = this.getResources()
.getStringArray(R.array.welcome_tips);
int index = (int) (Math.random() * welcome_array.length);
welcome_tip = welcome_array[index];
return welcome_tip;
}
/**
* 解析json数据
*/
private void parseJson(String text) {
try {
JSONObject jsonObject = new JSONObject(text);
ChatMessage chatMessage;
chatMessage = new ChatMessage(jsonObject.getString("text"),
ChatMessage.RECEIVER, getTime());
lists.add(chatMessage);
// 数据刷新
dataSetChanged();
} catch (JSONException e) {
e.printStackTrace();
}
}
/**
* 更新ListView
*/
private void dataSetChanged() {
mTextAdapter.notifyDataSetChanged();
}
/**
* ListView数据适配器
*/
private class TextAdapter extends BaseAdapter {
@Override
public int getCount() {
return lists.size();
}
@Override
public Object getItem(int position) {
return lists.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public int getItemViewType(int position) {
ChatMessage ChatMessage = lists.get(position);
if (ChatMessage.getFlag()==ChatMessage.SEND) {
return 0;
}
return 1;
}
@Override
public int getViewTypeCount() {
// TODO Auto-generated method stub
return 2;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
LayoutInflater inflater = getLayoutInflater();
ViewHolder viewHolder = null;
// 判断是接收方还是发送方
if (null == convertView) {
if (getItemViewType(position) == ChatMessage.RECEIVER) {
convertView = inflater.inflate(R.layout.leftitem, null);
viewHolder = new ViewHolder();
viewHolder.textView = (TextView) convertView
.findViewById(R.id.textView);
viewHolder.timeView = (TextView) convertView
.findViewById(R.id.timeTextView);
} else if (getItemViewType(position) == ChatMessage.SEND) {
convertView = inflater.inflate(R.layout.rightitem, null);
viewHolder = new ViewHolder();
viewHolder.textView = (TextView) convertView
.findViewById(R.id.textView);
viewHolder.timeView = (TextView) convertView
.findViewById(R.id.timeTextView);
}
convertView.setTag(viewHolder);
} else {
viewHolder = (ViewHolder) convertView.getTag();
}
viewHolder.textView.setText(lists.get(position).getContent());
viewHolder.timeView.setText(lists.get(position).getTime());
return convertView;
}
}
/**
* ViewHolder复用视图
*/
private final class ViewHolder {
TextView textView;
TextView timeView;
}
/**
* 构造URL
*/
private String construcUrl(String content) {
String url = Uri.parse(ENDPOINT).buildUpon()
.appendQueryParameter(KEY, APPKEY)
.appendQueryParameter(USERID, "1")
.appendQueryParameter(INFO, content).build().toString();
return url;
}
@Override
protected void onDestroy() {
super.onDestroy();
mChatThread.clearQueue();
mChatThread.quit();
}
}
@Override
public int getItemViewType(int position) {
ChatMessage ChatMessage = lists.get(position);
if (ChatMessage.getFlag()==ChatMessage.SEND) {
return 0;
}
return 1;
}
结束
这样这个聊天程序就基本上大功告成了,我们可以输入一些内容来与机器人对话了,是不是觉得还挺有意思心里还是有点小激动啊。终于可以做出一个看起来还不算丑的Android小程序了。当然这个程序还有一些不完善的地方,比如:
网络连接断开的情况下没有相关的提示
对话的内容不能保存,聊天记录的读取(当然这个只是和机器人聊天,要聊天记录干嘛,哈哈哈,汗...不过就是一种设想吧)
等等...
总结
学习编程的时候我们总是急着要做出一些东西,往往在实现的过程的总是遇到各种难题。记得在上大学的时候学习C语言,整天非常枯燥听老师讲解C语言的语法,结果导致班里很多同学都感到很无趣以及厌烦。就算是上机也是一个小黑框,不是计算就是输出东西当时真的觉得编程难道就是干这个吗。那些优秀的软件是怎么做出来的呢?还好大二的时候遇到的带我们java的老师,也因此跟着老师学习了3年的JavaEE开发这是后话。做java web的时候确实不用接触小黑框了,可以做出web页面来交互这真的大大提高了学习的兴趣。但是做到后来发现还web也不过如此CRUD罢了(ps:当然还有很多东西值得研究),这个时候我就会去回顾像数据结构算法,以及会去研究他们实现的机制。
说了这么多其实我就是想表达一点,编程不需要把基础的东西都学过,当然也不是基础不重要。我更加倾向的一种方式是:
兴趣驱动的方式。只有在产生兴趣后我们才更有动力去学习深层次的东西。
最后奉上个人信仰,当然也是给所有码农的鼓舞:
IT改变世界!!!