一个简洁优雅的意见反馈 - Uphie/ONE-Unofficial GitHub Wiki
注:以下贴出的代码均摘自本开源项目ONE-Unofficial,读者可自行查阅。
以前做意见反馈的时候常常是两个输入框(意见内容、联系方式)和一个按钮(确定),需要再加后台接口、管理端反馈查看功能。实际效果也不好,不常有人用,开发成本也大。后来转投sdk,找到了友盟,然而友盟的意见反馈界面不(bu)太(ren)好(zhi)看(shi),又没找到定制的方法,一度放弃友盟。后来找到了方法,加上了自己的一点设计,重新定制了反馈界面功能,使用了会话式的交互方式,增强了开发者(或运营者)和用户的沟通。
先看效果图:
友盟的意见反馈,其实是一个conversation,里面是reply对象,包括用户的回复和开发者的回复,那么关键只要向conversation中增加用户reply和刷新获取开发者reply即可。此文也可为即时聊天开发做一些参考。
-
集成Umeng 意见反馈SDK(ONE删除了部分不需要的资源文件和AndroidManifest.xml配置,洁癖=_=)。
-
初始化设置
//同步数据 final FeedbackAgent agent = new FeedbackAgent(this); agent.sync(); UserInfo u = agent.getUserInfo(); Map<String, String> contact = new HashMap<>(); contact.put("昵称", nickname); contact.put("手机", phone); u.setContact(contact); agent.setUserInfo(u); new Thread(new Runnable() { @Override public void run() { agent.updateUserInfo(); } }).start();
在程序入口处同步反馈数据。其中,contact 内填写用户的联系方式或者唯一标识用户的东西,在友盟管理控制台可以看到。
-
布局文件,这个很简单
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/content_gray" android:orientation="vertical"> <include layout="@layout/actionbar" /> <com.handmark.pulltorefresh.library.PullToRefreshListView android:id="@+id/listView" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" android:background="@android:color/transparent" android:cacheColorHint="@android:color/transparent" android:divider="@null" /> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@drawable/bg_top_line" android:paddingBottom="5dp" android:paddingLeft="10dp" android:paddingRight="10dp" android:paddingTop="5dp"> <studio.uphie.one.widgets.ClearEditText android:id="@+id/edt_feedback" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:background="@drawable/selector_edittext" android:imeOptions="actionSend" android:inputType="textMultiLine" android:lineSpacingExtra="4dp" android:padding="5dp" android:singleLine="false" android:textColor="@color/text_dark_gray" android:textSize="14sp" /> <TextView android:id="@+id/text_send" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="10dp" android:background="@drawable/bg_frame_white" android:enabled="false" android:padding="10dp" android:layout_gravity="bottom" android:text="@string/label_send" android:textColor="@color/disable_gray" /> </LinearLayout> </LinearLayout>
-
适配器,可以说最重要的部分。消息有三种状态,成功、失败和发送中,并需要提供失败消息重发功能,此外的一个细节是时间显示,不同间隔的时间不同的处理方法。
private class MessageAdapter extends BaseAdapter { private static final int TYPE_USER = 0; private static final int TYPE_ONE = 1; private Conversation conversation; public MessageAdapter(Conversation conversation) { this.conversation = conversation; } @Override public int getCount() { return conversation.getReplyList().size(); } @Override public int getViewTypeCount() { return 2; } @Override public int getItemViewType(int position) { return getItem(position).type.equals(Reply.TYPE_DEV_REPLY) ? TYPE_ONE : TYPE_USER; } @Override public Reply getItem(int position) { return conversation.getReplyList().get(position); } @Override public long getItemId(int position) { return position; } @Override public View getView(int position, View convertView, ViewGroup parent) { Reply reply = getItem(position); ViewHolder holder; int type = getItemViewType(position); if (convertView == null) { if (type == TYPE_ONE) { convertView = View.inflate(FeedbackActivity.this, R.layout.list_item_conversion_left, null); } else { convertView = View.inflate(FeedbackActivity.this, R.layout.list_item_conversion_right, null); } holder = new ViewHolder(); holder.iv_avatar = (ImageView) convertView.findViewById(R.id.list_item_conversion_avatar); holder.iv_msg_fail = (ImageView) convertView.findViewById(R.id.list_item_conversion_fail); holder.text_msg = (TextView) convertView.findViewById(R.id.list_item_conversion_message); holder.text_time = (TextView) convertView.findViewById(R.id.list_item_conversion_time); holder.pg_msg = (ProgressBar) convertView.findViewById(R.id.list_item_conversion_prog); holder.iv_msg_fail.setOnClickListener(new OnResendListener(reply)); convertView.setTag(holder); } else { holder = (ViewHolder) convertView.getTag(); } //头像 if (type == TYPE_ONE) { holder.iv_avatar.setImageResource(R.drawable.av_author); } //消息 holder.text_msg.setText(reply.content); //消息状态 switch (reply.status) { case Reply.STATUS_NOT_SENT: //发送失败 holder.pg_msg.setVisibility(View.GONE); holder.iv_msg_fail.setVisibility(View.VISIBLE); break; case Reply.STATUS_SENDING: //发送中 holder.pg_msg.setVisibility(View.VISIBLE); holder.iv_msg_fail.setVisibility(View.GONE); break; default: //发送成功 holder.pg_msg.setVisibility(View.GONE); holder.iv_msg_fail.setVisibility(View.GONE); break; } if (position != 0) { //不是第一条 Reply lastReply = getItem(position - 1); Date date = new Date(reply.created_at); if (reply.created_at - lastReply.created_at > 5 * 60 * 1000 && reply.created_at - lastReply.created_at < 24 * 60 * 60 * 1000) { //如果两条消息时间相差大于5分钟,小于一天,显示15:40形式时间 holder.text_time.setVisibility(View.VISIBLE); holder.text_time.setText(TimeUtil.toSimpleTime(date)); } else if (reply.created_at - lastReply.created_at > 24 * 60 * 60 * 1000) { //如果两条消息时间相差大于于一天,显示2015年1月27日 10:00:00形式时间 holder.text_time.setVisibility(View.VISIBLE); holder.text_time.setText(TimeUtil.toCommonTime(date)); } else { holder.text_time.setVisibility(View.GONE); } } return convertView; } private class OnResendListener implements View.OnClickListener { private Reply reply; public OnResendListener(Reply reply) { this.reply = reply; } @Override public void onClick(View v) { if (reply.type.equals(Reply.TYPE_USER_REPLY)) { //用户回复的,并且发送失败的状态 sync(); } } } private class ViewHolder { ImageView iv_avatar; TextView text_msg; TextView text_time; ProgressBar pg_msg; ImageView iv_msg_fail; } }
-
发送消息 ,除了发送按钮发送消息外,还有一个软键盘发送方式,此处输入框也提供了清空内容的功能。注意一个细节:发送后需关闭软键盘。
edt_feedback.setOnEditorActionListener(new TextView.OnEditorActionListener() { @Override public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { if (actionId == EditorInfo.IME_ACTION_SEND) { onClick(); return true; } return false; } }); @OnClick(R.id.text_send) public void onClick() { closeInputMethod(); String msg = edt_feedback.getText().toString().trim(); edt_feedback.setText(""); conversation.addUserReply(msg); adapter.notifyDataSetChanged(); sync(); } private void sync() { conversation.sync(new SyncListener() { @Override public void onSendUserReply(List<Reply> replyList) { } @Override public void onReceiveDevReply(List<Reply> replyList) { // SwipeRefreshLayout停止刷新 listView.onRefreshComplete(); // 刷新ListView adapter.notifyDataSetChanged(); //滚动到底部 listView.getRefreshableView().setSelection(adapter.getCount() - 1); } }); }
-
listview的一些设置,下拉刷新、初始显示最新消息。
//去除按item时的效果 listView.getRefreshableView().setSelector(new BitmapDrawable()); //顶部下拉刷新 listView.setMode(PullToRefreshBase.Mode.PULL_FROM_START); listView.setOnRefreshListener(new PullToRefreshBase.OnRefreshListener<ListView>() { @Override public void onRefresh(PullToRefreshBase<ListView> refreshView) { sync(); } }); adapter = new MessageAdapter(conversation); listView.setAdapter(adapter); if (adapter.getCount() != 0) { //滚动到底部,即显示到最新一条消息 listView.getRefreshableView().setSelection(adapter.getCount() - 1); }
最后,意见反馈功能就完成了。 此示例有两个不足,1:不能及时在开发者回复后通知用户,笔者曾经添加过友盟推送来实现,但在小米手机上会有bug(Google快来统一下中国的ROM吧一 一+),反馈给友盟未获回复。日后可行的话会加入此功能。2:没有发送图片和语音的功能,官方demo中有此功能,暂未研究,日后若可行会加。