什么是MVP呢,简单来讲就是将view层和逻辑彻底独立出来,让逻辑和显示彻底独立。本例中就是采用了这种模式,让activity做为view层,activity中涉及了适配器,因此这里尝试让适配器做为P层来进行逻辑处理。之后可能要考虑用多个p来作逻辑处理。总之,咱们先来分析下如何用MVP得思路来分析这个工程吧~html
1、界面java
界面这个环节有不少细节须要扣,以前我写过一篇文章就是讲这个界面实现的,推荐先去看看:http://www.cnblogs.com/tianzhijiexian/p/4295195.htmlandroid
2、根据界面来思考逻辑git
通常状况下咱们从设计那里获得了一张图后就须要进行分析了,分析这个界面须要什么逻辑。因此咱们再来看看界面是什么样的:github
咱们从上到下进行分析,分析时就须要写出接口了,等于把天然语言程序化。数组
1.顶部有退出按钮——finish()服务器
2.顶部有刷新按钮——refresh();刷新成功后须要有回调——onRefreshSuccess()app
3.界面有信息,须要适配器——setAdapter()
dom
4.下方有+号,是发送图片的按钮——sendPhoto();由于是实时聊天界面,即便信息没发送成功,也须要添加到适配器中,显示一个无法送成功的标记就好。因此不须要作回调。只须要在适配器的getView()中根据list得item的标志来判断是否发送成功了,根据是否发送成功来显示无法送成功的感叹号。
ide
5.有发送按钮,发送文字——addNewReply(String str);由于是实时聊天界面,即便信息没发送成功,也须要添加到适配器中,显示一个无法送成功的标记就好。须要在适配器的getView()中进行状态的回调,回调方式同上。
6.既然须要在getView中进行回调,那么activity中就要能有这个getView()的方法——onGetViewFromAdapter()
3、Activity须要实现的接口
这样咱们大概的接口就已经写好了,下面来看看最终的接口文档:
4、调用P层进行逻辑操做
咱们假设咱们的activity已经实现了这个接口,也已经作好了界面,那么是否是该调用p层来处理逻辑了呢?如今p层在哪里呢?别着急,p我已经写好了,并且对外提供了不少的方法让view能够随意调用。
UMengFeedbackPresenter的源码:
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.content.Context; import android.content.Intent; import android.os.Handler; import android.os.Message; 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; private static UMengFeedbackPresenter instance; private UMengFeedbackPresenter(IUMengFeedbackView view) { mContext = (Context) view; mFeedbackView = view; mConversation = new FeedbackAgent(mContext).getDefaultConversation(); mConversation.setOnChangeListener(new Conversation.OnChangeListener() { @Override public void onChange() { // 发送消息后会自动调用此方法,在这里更新下发送状态 notifyDataSetChanged(); } }); } public static UMengFeedbackPresenter getInstance(IUMengFeedbackView view) { if (instance == null) { instance = new UMengFeedbackPresenter(view); } return instance; } /** * 获得当前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); } /** * 发送图片给开发者 */ public void sendPhotoToDev() { Intent intent = new Intent(); intent.putExtra(UMengFeedbackPhotoActivity.KEY_UMENG_GET_PHOTO, UMengFeedbackPhotoActivity.VALUE_UMENG_GET_PHOTO); intent.setClass(mContext, UMengFeedbackPhotoActivity.class); mContext.startActivity(intent); } /** * 当用户发送图片信息时,在Activity的onActivityResult中调用此方法来处理上传图片等后续操做 */ protected void getPhotoFromAlbum(Intent data) { //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; } }
由于有发送图片的功能因此须要从系统相册中获得图片,这就要创建一个activity去获取图片:
package com.kale.umenglib; import android.app.Activity; import android.content.Intent; import android.os.Bundle; import android.provider.MediaStore; /** * @author Jack Tony * @date 2015/5/11 */ public class UMengFeedbackPhotoActivity extends Activity{ private static final int REQUEST_CODE = 1; public static final String KEY_UMENG_GET_PHOTO = "KEY_UMENG_GET_PHOTO"; public static final int VALUE_UMENG_GET_PHOTO = 1; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (getIntent() != null && getIntent().getIntExtra(KEY_UMENG_GET_PHOTO, 0) == VALUE_UMENG_GET_PHOTO) { Intent intent = new Intent("android.intent.action.PICK", MediaStore.Images.Media.EXTERNAL_CONTENT_URI); startActivityForResult(intent, REQUEST_CODE); } } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (resultCode == -1 && requestCode == REQUEST_CODE && data != null) { UMengFeedbackPresenter.getInstance(null).getPhotoFromAlbum(data); } finish(); } }
5、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 = UMengFeedbackPresenter.getInstance(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()); } }); } /** * 设置下拉刷新的控件 */ 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
最新源码(推荐):http://download.csdn.net/detail/shark0017/8686989
BTW:源码引用了kaleLibrary这个库:https://github.com/tianzhijiexian/KaleLibrary/