什麼是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/