一个简洁优雅的意见反馈 - Uphie/ONE-Unofficial GitHub Wiki

##一个简洁优雅的意见反馈界面

注:以下贴出的代码均摘自本开源项目ONE-Unofficial,读者可自行查阅。

以前做意见反馈的时候常常是两个输入框(意见内容、联系方式)和一个按钮(确定),需要再加后台接口、管理端反馈查看功能。实际效果也不好,不常有人用,开发成本也大。后来转投sdk,找到了友盟,然而友盟的意见反馈界面不(bu)太(ren)好(zhi)看(shi),又没找到定制的方法,一度放弃友盟。后来找到了方法,加上了自己的一点设计,重新定制了反馈界面功能,使用了会话式的交互方式,增强了开发者(或运营者)和用户的沟通。

先看效果图:

feedback

友盟的意见反馈,其实是一个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中有此功能,暂未研究,日后若可行会加。

⚠️ **GitHub.com Fallback** ⚠️