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

一、界面

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

二、根据界面来思考逻辑

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

 

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

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

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

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

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

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

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

三、Activity需要实现的接口

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

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

四、调用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;
} }

shark0017

因为有发送图片的功能所以需要从系统相册中得到图片,这就要建立一个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();
}
}

五、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/

打造高仿QQ的友盟反馈界面(MVP模式)的更多相关文章

  1. iOS高仿app源码:纯代码打造高仿优质《内涵段子》

    iOS高仿app源码:纯代码打造高仿优质<内涵段子>收藏下来 字数1950 阅读4999 评论173 喜欢133 Github 地址 https://github.com/Charlesy ...

  2. 高通AR和友盟SDK的AndroidManifest.xml合并

    高通AR和友盟SDK的AndroidManifest.xml合并 因为高通的AR在android中一开始就要启动,所有主Activity要设置为高通的Activity,即android:name=&q ...

  3. 史上最简单,一步集成侧滑(删除)菜单,高仿QQ、IOS。

    重要的话 开头说,not for the RecyclerView or ListView, for the Any ViewGroup. 本控件不依赖任何父布局,不是针对 RecyclerView. ...

  4. Android实现高仿QQ附近的人搜索展示

    本文主要实现了高仿QQ附近的人搜索展示,用到了自定义控件的方法 最终效果如下 1.下面展示列表我们可以使用ViewPager来实现(当然如果你不觉得麻烦,你也可以用HorizontalScrollVi ...

  5. 高仿QQ的即时通讯应用带服务端软件安装

    Android 基于xmpp协议,smack包,openfire服务端(在下面)的高仿QQ的即时通讯实现.实现了注册,登录,读取好友列表,搜索好友,添加分组,添加好友,删除好友,修改心情,两个客户端之 ...

  6. 高仿QQ即时聊天软件开发系列之三登录窗口用户选择下拉框

    上一篇高仿QQ即时聊天软件开发系列之二登录窗口界面写了一个大概的布局和原理 这一篇详细说下拉框的实现原理 先上最终效果图 一开始其实只是想给下拉框加一个placeholder效果,让下拉框在未选择未输 ...

  7. 高仿QQ即时聊天软件开发系列之二登录窗口界面

    继上一篇高仿QQ即时聊天软件开发系列之一开端之后,开始做登录窗口 废话不多说,先看效果,只有界面 可能还有一些细节地方没有做,例如那个LOGO嘛,不要在意这些细节 GIF虽短,可是这做起来真难,好吧因 ...

  8. 高仿QQ即时聊天软件开发系列之一开端

    前段时间在园子里看到一个大神做了一个GG2014IM软件,仿QQ的,那感觉···,赶快下载源码过来试试,还真能直接跑起来,效果也不错.但一看源码,全都给封装到了ESFramework里面了,音视频那部 ...

  9. 高仿qq聊天界面

    高仿qq聊天界面,给有需要的人,界面效果如下: 真心觉得做界面非常痛苦,给有需要的朋友. chat.xml <?xml version="1.0" encoding=&quo ...

随机推荐

  1. HIVE: UDF应用实例

    数据文件内容 TEST DATA HERE Good to Go 我们准备写一个函数,把所有字符变为小写. 1.开发UDF package MyTestPackage; import org.apac ...

  2. [转]VS2005/2008过期之后简单实用的升级方法

    网络上有不少key,但是用了之后没效果,发现了一个好方法可以解决.  把\vs\setup\下面的 setup.sdb文件用文本编辑器打开,然后改动其最后的一行([Product Key] 下面的一行 ...

  3. ubuntu14.04编译安装Git2.7

    在开源中国看文章, 随意之间, 在软件资讯栏看到git 2.7的信息. 一直在使用在git 1.9.1, 心中突感, 这个git 2.7是个什么东西, 怎么git的版本更新有如此快么. 印象里, 老外 ...

  4. [Code::Blocks] Install wxWidgets & openCV

    The open source, cross platform, free C++ IDE. Code::Blocks is a free C++ IDE built to meet the most ...

  5. solrcloud使用中遇到的问题及解决方式

    首先声明,我们团队在使用solrcloud过程中踩了一些坑,同事(晓磊和首富)进行了总结,我列到我的博客上做记录用: Q:为什么Solr里面的时间比数据库里面早8小时? Solr默认采用的时区是UTC ...

  6. [Node.js] 也说this

    原文地址:http://www.moye.me/2014/11/21/也说this/ 引子 Any sufficiently advanced technology is indistinguisha ...

  7. iOS-动态计算Label的高度

    一. 要求 1.根据网络请求的回来的字符串内容,动态计算Label的高度. 二. 注意点 1. 要注意设置label 的 numberOfLines 为0; 2. MAXFLOAT 的作用. 设置高度 ...

  8. MySQL多实例,主从同步

    由于背景原因,所做的主从同步还是要基于MySQL 5.1的版本,主从同步主要是一个数据库读写访问原来的数据库热度过大,需要做到使用从库对读分压. MySQL主从同步介绍     MySQL 支持单双向 ...

  9. [Architect] Abp 框架原理解析(5) UnitOfWork

    本节目录 介绍 分析Abp源码 实现UOW 介绍 UOW(全称UnitOfWork)是指工作单元. 在Abp中,工作单元对于仓储和应用服务方法默认开启.并在一次请求中,共享同一个工作单元. 同时在Ab ...

  10. “康园圈--互联网+校园平台“项目之sprint2

    一.sprint2任务列表 1.部署框架,并上传代码到github. 2.原型设计 * 设计首页界面原型(包括功能公告.快速通道等展示栏) * 设计店铺浏览页面原型 * 设计店内浏览页面原型 * 设计 ...