转载

打造高仿QQ的友盟反馈界面(MVP模式)

打造高仿QQ的友盟反馈界面(MVP模式)

什么是MVP呢,简单来说就是将view层和逻辑完全独立出来,让逻辑和显示完全独立。本例中就是采用了这种模式,让activity作为view层,activity中涉及了适配器,所以这里尝试让适配器作为P层来进行逻辑处理。以后可能要考虑用多个p来做逻辑处理。总之,我们先来分析下如何用MVP得思路来分析这个工程吧~

一、界面

界面这个环节有很多细节需要扣,之前我写过一篇文章就是讲这个界面实现的,推荐先去看看: http://www.cnblogs.com/tianzhijiexian/p/4295195.html

二、根据界面来思考逻辑

一般情况下我们从设计那里得到了一张图后就需要进行分析了,分析这个界面需要什么逻辑。所以我们再来看看界面是什么样的:

打造高仿QQ的友盟反馈界面(MVP模式) 打造高仿QQ的友盟反馈界面(MVP模式)

我们从上到下进行分析,分析时就需要写出接口了,等于把自然语言程序化。

1.顶部有退出按钮——finish()

2.顶部有刷新按钮——refresh();刷新成功后需要有回调——onRefreshSuccess()

3.界面有信息,需要适配器——setAdapter()

4.下方有+号,是发送图片的按钮——sendPhoto();因为是实时聊天界面,即使信息没发送成功,也需要添加到适配器中,显示一个没法送成功的标记就好。所以不需要做回调。只需要在适配器的getView()中根据list得item的标志来判断是否发送成功了,根据是否发送成功来显示没法送成功的感叹号。

5.有发送按钮,发送文字——addNewReply(String str);因为是实时聊天界面,即使信息没发送成功,也需要添加到适配器中,显示一个没法送成功的标记就好。需要在适配器的getView()中进行状态的回调,回调方式同上。

6.既然需要在getView中进行回调,那么activity中就要能有这个getView()的方法——onGetViewFromAdapter()

7.因为调用拍照的activity会在onActivityResult中回调,所以还需要:

getPhotoFromAlbum (int requestCode, int resultCode, android.content.Intent data)

三、Activity需要实现的接口

这样我们大概的接口就已经写好了,下面来看看最终的接口文档:

接口 IUMengFeedbackView(实现友盟反馈的activity需要实现这个接口)

打造高仿QQ的友盟反馈界面(MVP模式)

四、调用P层进行逻辑操作

我们假设我们的activity已经实现了这个接口,也已经做好了界面,那么是不是该调用p层来处理逻辑了呢?现在p层在哪里呢?别着急,p我已经写好了,而且对外提供了很多的方法让view可以随意调用。

打造高仿QQ的友盟反馈界面(MVP模式)

UMengFeedbackPresenter的源码:

打造高仿QQ的友盟反馈界面(MVP模式)
package com.kale.umenglib; import com.umeng.fb.FeedbackAgent; import com.umeng.fb.SyncListener; import com.umeng.fb.model.Conversation; import com.umeng.fb.model.Reply; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.os.Handler; import android.os.Message; import android.provider.MediaStore; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; import java.util.List; import java.util.UUID; /**  * @author:Jack Tony  * 定义回话界面的adapter  * @date :2015年2月9日  */ public class UMengFeedbackPresenter extends BaseAdapter {  private final String TAG = getClass().getSimpleName();  /**   * 表示是一对一聊天,有两个类型的信息   */  private static final int VIEW_TYPE_COUNT = 2;  /**   * 用户的标识   */  private static final int VIEW_TYPE_USER = 0;  /**   * 开发者的标识   */  private static final int VIEW_TYPE_DEV = 1;  /**   * 一次性加载多少条数据,默认10条   */  private int mLoadDataNum = 10; // default  /**   * 当前显示的数据条数   */  private int mCurrentMsgCount = 10;  private Context mContext;  /**   * 负责显示umeng反馈界面信息的activity   */  private IUMengFeedbackView mFeedbackView;  /**   * 反馈系统的回话对象   */  private Conversation mConversation;  public UMengFeedbackPresenter(IUMengFeedbackView activity) {   mContext = (Context) activity;   mFeedbackView = activity;   mConversation = new FeedbackAgent(mContext).getDefaultConversation();   mConversation.setOnChangeListener(new Conversation.OnChangeListener() {    @Override    public void onChange() {     // 发送消息后会自动调用此方法,在这里更新下发送状态     notifyDataSetChanged();    }   });  }  /**   * 得到当前adapt中的数据条数   * @return 当前adapt中的数据条数    */  @Override  public int getCount() {   // 如果开始时的数目小于一次性显示的数目,就按照当前的数目显示,否则会数组越界   int totalCount = mConversation.getReplyList().size();   if (totalCount < mCurrentMsgCount) {    mCurrentMsgCount = totalCount;   }   return mCurrentMsgCount;  }  /**   * @return 当前的position   * 重要方法,计算出当前的position   */  private int getCurrentPosition(int position) {   int totalCount = mConversation.getReplyList().size();   if (totalCount < mCurrentMsgCount) {    mCurrentMsgCount = totalCount;   }   return totalCount - mCurrentMsgCount + position;  }  @Override  public Object getItem(int position) {   position = getCurrentPosition(position);   return mConversation.getReplyList().get(position);  }  @Override  public long getItemId(int position) {   return getCurrentPosition(position);  }  @Override  public int getViewTypeCount() {   // 这里是一对一聊天,所以是两种类型   return VIEW_TYPE_COUNT;  }  @Override  public int getItemViewType(int position) {   position = getCurrentPosition(position);   // 获取单条回复   Reply reply = mConversation.getReplyList().get(position);   if (reply.type.equals(Reply.TYPE_DEV_REPLY)) {    // 开发者回复Item布局    return VIEW_TYPE_DEV;   } else if (reply.type.equals(Reply.TYPE_USER_REPLY)) {    // 用户反馈、回复Item布局    return VIEW_TYPE_USER;   } else {    return 0;   }  }  @Override  public View getView(int position, View convertView, ViewGroup parent) {   position = getCurrentPosition(position);   // 得到当前位置的reply对象   Reply reply = mConversation.getReplyList().get(position);   //Log.d(TAG, "reply type = " + reply.type);   if (convertView == null) {    LayoutInflater inflater = LayoutInflater.from(mContext);    // 根据Type的类型来加载不同的Item布局    switch (reply.type) {     case Reply.TYPE_DEV_REPLY:      // 如果是开发者回复的,那么就加载开发者回复的布局      convertView = inflater.inflate(mFeedbackView.getDevReplyLayoutId(), null);      break;     case Reply.TYPE_NEW_FEEDBACK:      //break;     case Reply.TYPE_USER_REPLY:      convertView =  inflater.inflate(mFeedbackView.getUserReplyLayoutId(), null);      break;     default:    }   }   if (reply.type.equals(Reply.TYPE_USER_REPLY) || reply.type.equals(Reply.TYPE_NEW_FEEDBACK)) {    mFeedbackView.setUserReplyView(convertView, reply);   } else if (reply.type.equals(Reply.TYPE_DEV_REPLY)) {    mFeedbackView.setDevReplyView(convertView, reply);   }   Reply nextReply = null;   if ((position + 1) < mConversation.getReplyList().size()) {    nextReply = mConversation.getReplyList().get(position + 1);   }   mFeedbackView.onGetViewFromAdapter(convertView, reply, nextReply);   return convertView;  }  /**   * 加载之前的聊天信息   */  public void loadOldData() {   int loadDataNum = mLoadDataNum;   int totalCount = mConversation.getReplyList().size();   if (loadDataNum + mCurrentMsgCount >= totalCount) {    // 如果要加载的数据超过了数据的总量,算出实际加载的数据条数    loadDataNum = totalCount - mCurrentMsgCount;   }   mCurrentMsgCount += loadDataNum;   notifyDataSetChanged();   mFeedbackView.onLoadOldDataSuccess(loadDataNum);  }  private static final int REQUEST_CODE = 1;  /**   * 发送图片给开发者   */  public void sendPhotoToDev() {   Intent intent = new Intent("android.intent.action.PICK", MediaStore.Images.Media.EXTERNAL_CONTENT_URI);   ((Activity) mContext).startActivityForResult(intent, REQUEST_CODE);  }  /**   * 当用户发送图片信息时,在Activity的onActivityResult中调用此方法来处理上传图片等后续操作   */  public void getPhotoFromAlbum(int requestCode, int resultCode, Intent data) {   if (resultCode == -1 && requestCode == REQUEST_CODE && data != null) {    //Log.e(TAG, "data.getDataString -- " + data.getDataString());    if (UMengB.a(mContext, data.getData())) {     UMengB.a(mContext, data.getData(), "R" + UUID.randomUUID().toString(), new Handler() {      @Override      public void handleMessage(Message msg) {       super.handleMessage(msg);       sendMsgToDev((String) msg.obj, Reply.CONTENT_TYPE_IMAGE_REPLY);      }     });    }   }  }  /**   * 用户发送了一条新的信息后调用此方法   *   * @param replyMsg 信息的内容   * @param type  Reply.CONTENT_TYPE_TEXT_REPLY或者Reply.CONTENT_TYPE_IMAGE_REPLY   */  public void sendMsgToDev(String replyMsg, String type) {   if (type.equals(Reply.CONTENT_TYPE_TEXT_REPLY)) {    mConversation.addUserReply(replyMsg);   } else if (type.equals(Reply.CONTENT_TYPE_IMAGE_REPLY)) {    mConversation.addUserReply("", replyMsg, "image_reply", -1.0F);   } else if (type.equals(Reply.CONTENT_TYPE_AUDIO_REPLY)) {   }   mCurrentMsgCount++;   syncToUmeng();  }  /**   * 将数据和服务器同步   * TODO:这里有两种写法,可以考虑换个实现方式。   */  public void syncToUmeng() {   //new FeedbackAgent(mContext).sync();// 第一种写法   // 第二种写法↓   mConversation.sync(new SyncListener() {    @Override    public void onSendUserReply(List<Reply> replyList) {     Log.d(TAG, "onSendUserReply");     if (replyList == null || replyList.size() < 1) {      Log.d(TAG, "user 用户没有发送新的消息");     } else {      notifyDataSetChanged();     }    }    @Override    public void onReceiveDevReply(List<Reply> replyList) {     Log.d(TAG, "onReceiveDevReply");     if (replyList == null || replyList.size() < 1) {      // 没有开发者新的回复      Log.d(TAG, "dev 开发者没有新的回复");     } else {      notifyDataSetChanged();     }    }   });  }  /**   * 设置界面一开始显示多少条数据,默认显示最近的十条信息   */  public void setReplyMsgCount(int count) {   mCurrentMsgCount = count;  }  /**   * 设置调用loadOldData()时,一次性加载多少条数据,默认10条   */  public void setLoadDataNum(int number) {   mLoadDataNum = number;  }  /**   * 得到适配器对象   */  public BaseAdapter getAdapter() {   return this;  } } 
shark0017

五、Demo

下面的demo通过activity来调用了presenter的各个方法,完全自定义的友盟的反馈界面,而且只用关系界面的view,不用考虑任何逻辑处理。这就是mvp的特点,分工明确,而且方便扩展~

UMengFeedbackActivity源码:

package com.example.jack.umengfeedback; import com.kale.lib.ViewHolder; import com.kale.lib.activity.KaleBaseActivity; import com.kale.lib.utils.EasyToast; import com.kale.lib.utils.InputUtil; import com.kale.umenglib.IUMengFeedbackView; import com.kale.umenglib.UMengFeedbackPresenter; import com.umeng.fb.model.Reply; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.support.v4.widget.SwipeRefreshLayout; import android.support.v4.widget.SwipeRefreshLayout.OnRefreshListener; import android.text.Editable; import android.text.TextUtils; import android.text.TextWatcher; import android.util.DisplayMetrics; import android.util.Log; import android.view.KeyEvent; import android.view.View; import android.view.View.OnClickListener; import android.view.View.OnKeyListener; import android.view.ViewStub; import android.view.WindowManager; import android.widget.AbsListView; import android.widget.Button; import android.widget.EditText; import android.widget.ImageView; import android.widget.ListView; import android.widget.ProgressBar; import android.widget.TextView; import java.text.SimpleDateFormat; import java.util.Date; public class UMengFeedbackActivity extends KaleBaseActivity implements IUMengFeedbackView {  /**   * 退出的文字按钮   */  private TextView exitTv;  /**   * 刷新新的信息的图片按钮   */  private ImageView refreshIv;  /**   * 会话界面的下拉刷新控件   */  private SwipeRefreshLayout swipeRefreshLayout;  /**   * 对话的listView   */  private ListView conversationLv;  /**   * 输入信息的文本框   */  private EditText inputBoxEt;  /**   * 发送信息的按钮   */  private Button sendMsgBtn;  private ImageView sendPhotoIv;  /**   * 友盟反馈界面的聊天信息适配器   */  private UMengFeedbackPresenter mUMengFeedbackPresenter;  @Override  protected void beforeSetContentView() {   super.beforeSetContentView();  }  @Override  protected int getContentViewId() {   return R.layout.umeng_feedback_main;  }  @Override  protected void onCreate(Bundle savedInstanceState) {   super.onCreate(savedInstanceState);  }  /**   * 当这个activity在最上方时不重复启动activity, 如果调用了startActivity,那么就更新下视图   */  @Override  protected void onNewIntent(Intent intent) {   super.onNewIntent(intent);   Log.d(TAG, "on new intent");  }  @Override  protected void findViews() {   refreshIv = getView(R.id.refresh_imageView);   exitTv = getView(R.id.exit_textView);   swipeRefreshLayout = getView(R.id.swipe_container);   conversationLv = getView(R.id.fb_conversation_listView);   inputBoxEt = getView(R.id.inputBox_editText);   sendMsgBtn = getView(R.id.sendMsg_button);   sendPhotoIv = getView(R.id.sendPhoto_imageView);  }  @Override  protected void beforeSetViews() {   mUMengFeedbackPresenter = new UMengFeedbackPresenter(this);  }  @Override  protected void setViews() {   refreshIv.setOnClickListener(new OnClickListener() {    @Override    public void onClick(View v) {     // TODO:修复bug:只有少数信息时,刷新开发者的回复后listView会跳到顶端     mUMengFeedbackPresenter.syncToUmeng();     EasyToast.makeText(mContext, "刷新成功~");    }   });   exitTv.setOnClickListener(new OnClickListener() {    @Override    public void onClick(View v) {     finish();    }   });   setSwipeLayout();   setConversationListView();   setInputBoxEditText();   /**    * 设置发送按钮的事件    */   sendMsgBtn.setEnabled(false);   sendMsgBtn.setOnClickListener(new OnClickListener() {    @Override    public void onClick(View v) {     sendMessage();    }   });   /**    * 设置添加图片按钮的事件    */   sendPhotoIv.setOnClickListener(new OnClickListener() {    @Override    public void onClick(View v) {     mUMengFeedbackPresenter.sendPhotoToDev();     conversationLv.setSelection(mUMengFeedbackPresenter.getAdapter().getCount());    }   });  }  public void onActivityResult(int requestCode, int resultCode, Intent data) {   super.onActivityResult(requestCode, resultCode, data);   mUMengFeedbackPresenter.getPhotoFromAlbum(requestCode, resultCode, data);  }  /**   * 设置下拉刷新的控件   */  private void setSwipeLayout() {   swipeRefreshLayout.setSize(SwipeRefreshLayout.DEFAULT);   // 设置下拉圆圈上的颜色,蓝色、绿色、橙色、红色   swipeRefreshLayout.setColorSchemeResources(     android.R.color.holo_blue_bright,     android.R.color.holo_green_light,     android.R.color.holo_orange_light,     android.R.color.holo_red_light);   swipeRefreshLayout.setOnRefreshListener(new OnRefreshListener() {    @Override    public void onRefresh() {     mUMengFeedbackPresenter.loadOldData();     conversationLv.setTranscriptMode(AbsListView.TRANSCRIPT_MODE_NORMAL);    }   });  }  /**   * 当加载旧的数据完成后的回调方法   *   * @param loadDataNum 加载了多少个旧的数据   */  @Override  public void onLoadOldDataSuccess(int loadDataNum) {   swipeRefreshLayout.setRefreshing(false);   // 加载完毕旧的数据,跳到刷新出来数据的位置   if (loadDataNum - 1 >= 0) {    conversationLv.setSelection(loadDataNum - 1);   } else {    EasyToast.makeText(mContext, "已经到头了");    conversationLv.setSelection(0);   }   conversationLv.setTranscriptMode(AbsListView.TRANSCRIPT_MODE_ALWAYS_SCROLL);  }  /**   * 设置listView,list不显示分割线,设置滚动监听器,设置适配器   */  private void setConversationListView() {   conversationLv.setDivider(null);   conversationLv.setAdapter(mUMengFeedbackPresenter.getAdapter());   // 不显示滚动到顶部/底部的阴影(减少绘制)   conversationLv.setOverScrollMode(View.OVER_SCROLL_NEVER);   // 监听listView的滑动状态,如果到了顶部就刷新数据,向上滑动就隐藏输入法   conversationLv.setOnScrollListener(new AbsListView.OnScrollListener() {    @Override    public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {}    @Override    public void onScrollStateChanged(AbsListView view, int scrollState) {     switch (scrollState) {      case AbsListView.OnScrollListener.SCROLL_STATE_IDLE:       // 滚动停止       if (view.getLastVisiblePosition() == (view.getCount() - 1)) {        // 如果滚动到底部       } else if (view.getFirstVisiblePosition() == 0) {        // 滚动到顶部       }       break;      case AbsListView.OnScrollListener.SCROLL_STATE_FLING:       // 开始滚动       break;      case AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL:       // 正在滚动       InputUtil.getInstance((Activity) mContext).hide();       break;     }    }   });  }  /**   * 设置发送消息的按钮和输入框 按下回车键,发送消息   */  private void setInputBoxEditText() {   inputBoxEt.setOnKeyListener(new OnKeyListener() {    @Override    public boolean onKey(View v, int keyCode, KeyEvent event) {     // 按下回车发送消息     // 这两个条件必须同时成立,如果仅仅用了enter判断,就会执行两次     if (keyCode == KeyEvent.KEYCODE_ENTER && event.getAction() == KeyEvent.ACTION_DOWN) {      sendMessage();      return true;     }     return false;    }   });   // 给editText添加监听器   inputBoxEt.addTextChangedListener(new TextWatcher() {    // 输入过程中,还在内存里,没到屏幕上    @Override    public void onTextChanged(CharSequence s, int start, int before, int count) {    }    // 在输入之前会触发的    @Override    public void beforeTextChanged(CharSequence s, int start, int count, int after) {    }    @Override    public void afterTextChanged(Editable s) {     // 输入完将要显示到屏幕上时会触发     boolean isEmpty = s.toString().isEmpty();     sendMsgBtn.setEnabled(!isEmpty);     sendMsgBtn.setTextColor(isEmpty ? 0xffa1a2a5 : 0xffffffff);    }   });  }  /**   * 发送消息   */  private void sendMessage() {   String replyMsg = inputBoxEt.getText().toString();   inputBoxEt.getText().clear();   if (!TextUtils.isEmpty(replyMsg)) {    mUMengFeedbackPresenter.sendMsgToDev(replyMsg, Reply.CONTENT_TYPE_TEXT_REPLY);    conversationLv.setSelection(mUMengFeedbackPresenter.getAdapter().getCount());   }  }  @Override  public int getUserReplyLayoutId() {   return R.layout.umeng_feedback_user_reply;  }  /**   * 设置用户的list item view   * 这里的提示信息有进度条和感叹号两种。如果正在发送就显示进度条,如果发送失败就显示感叹号   */  @Override  public void setUserReplyView(View convertView, Reply reply) {   // 利用viewHolder进行了优化   TextView textMsgTv = ViewHolder.get(convertView, R.id.textMsg_textView);   ImageView photoMsgIv = ViewHolder.get(convertView, R.id.photoMsg_imageView);   ImageView msgErrorIv = ViewHolder.get(convertView, R.id.msg_error_imageView);   ProgressBar msgSendingPb = ViewHolder.get(convertView, R.id.msg_progressBar);   // 放入消息   switch (reply.content_type) {    case Reply.CONTENT_TYPE_TEXT_REPLY:     photoMsgIv.setVisibility(View.GONE);     textMsgTv.setVisibility(View.VISIBLE);     textMsgTv.setText(reply.content);     break;    case Reply.CONTENT_TYPE_IMAGE_REPLY:     textMsgTv.setVisibility(View.GONE);     photoMsgIv.setVisibility(View.VISIBLE);     // 显示大图     //photoMsgIv.setImageBitmap(BitmapFactory.decodeFile(com.umeng.fb.util.c.b(mContext, reply.reply_id)));     // 显示小图     com.umeng.fb.image.a.a().a(com.umeng.fb.util.c.b(mContext, reply.reply_id), photoMsgIv, getPhotoSize(mContext));     break;    case Reply.CONTENT_TYPE_AUDIO_REPLY:     break;    default:   }   // 根据Reply的状态来设置replyStateFailed的状态,如果发送失败就显示提示图标   switch (reply.status) {    case Reply.STATUS_NOT_SENT:     msgSendingPb.setVisibility(View.GONE);     msgErrorIv.setVisibility(View.VISIBLE);     break;    case Reply.STATUS_SENDING:     //break;    case Reply.STATUS_WILL_SENT:     msgSendingPb.setVisibility(View.VISIBLE);     msgErrorIv.setVisibility(View.GONE);     break;    case Reply.STATUS_SENT:     msgSendingPb.setVisibility(View.GONE);     msgErrorIv.setVisibility(View.GONE);     break;    default:   }  }  @Override  public int getDevReplyLayoutId() {   return R.layout.umeng_feedback_dev_reply;  }  /**   * 设置开发者的list item view   */  @Override  public void setDevReplyView(View convertView, Reply reply) {   // 利用viewHolder进行了优化   TextView textMsgTv = ViewHolder.get(convertView, R.id.dev_textMsg_textView);   textMsgTv.setText(reply.content);  }  /**   * 如果两条信息间隔了TIME_RANGE秒,那么就显示上一条信息的发送时间   */  public static final int TIME_RANGE = 2 * 60;  @Override  public void onGetViewFromAdapter(View convertView, Reply reply, Reply nextReply) {     /* Log.d(TAG, "context_type = " + reply.content_type);   Log.d(TAG, "context = " + reply.content);   Log.d(TAG, "reply_id = " + reply.reply_id);*/   // 显示消息的时间   if (nextReply != null) {    ViewStub timeView = ViewHolder.get(convertView, R.id.msg_time_viewStub);    // 当两条回复相差TIME_RANGE秒时显示时间    if (nextReply.created_at - reply.created_at > TIME_RANGE * 1000) {     timeView.setVisibility(View.VISIBLE);     TextView timeTv = ViewHolder.get(convertView, R.id.msg_Time_TextView);     Date replyTime = new Date(reply.created_at);     SimpleDateFormat sdf = new SimpleDateFormat("HH:mm");     timeTv.setText(sdf.format(replyTime));    } else {     timeView.setVisibility(View.GONE);    }   }  }  private int getPhotoSize(Context context) {   DisplayMetrics metrics = new DisplayMetrics();   WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);   windowManager.getDefaultDisplay().getMetrics(metrics);   return metrics.widthPixels > metrics.heightPixels ? metrics.heightPixels : metrics.widthPixels;  }  @Override  public void finish() {   super.finish();   InputUtil.getInstance((Activity) mContext).hide();  } } 

源码下载:http://download.csdn.net/detail/shark0017/8683945

btw:源码引用了kaleLibrary这个库:https://github.com/tianzhijiexian/KaleLibrary/

正文到此结束
Loading...