制作高仿QQ的聊天系统(下)—— Adapter & Activity

一、适配器

1.1 分页显示数据

因为聊天信息数目很多,所以adpter需要做分页处理,这里的分页处理是我自己实现的,如果有更好的办法欢迎在评论中告知。我们从友盟的反馈SDK中能得到聊天的list,我设定的是一次性显示10条数据,所以在适配器中传入和传出的position并不是listview的index,需要进行一定的计算。

下面是计算position的方法:

    /**
     * @description 重要方法,计算出当前的position
     *
     * @param position
     * @return 当前的position
     */
    private int getCurrentPosition(int position) {
        int totalCount = mConversation.getReplyList().size();
        if (totalCount < mCurrentCount) {
            mCurrentCount = totalCount;
        }
        return totalCount - mCurrentCount + position;
    }

通过

int totalCount = mConversation.getReplyList().size();

得到list的size,通过数据总条数(size)和当前一屏需要显示的条数(mCurrentCount)进比较,如果准备显示的数据条数大于数据的总条数,那么就进行一定的计算,如果不进行处理会出现数组越界异常。

 

同理,Adapter都需要设置一个存储数据的数目,在getCount()中我们也要进行处理。

    @Override
    public int getCount() {
        // 如果开始时的数目小于一次性显示的数目,就按照当前的数目显示,否则会数组越界
        int totalCount = mConversation.getReplyList().size();
        if (totalCount < mCurrentCount) {
            mCurrentCount = totalCount;
        }
        return mCurrentCount;
    }

1.2 设置Adapter中的数据种类

我们的聊天系统中发送的消息有来自用户和来自开发者的,所以有两种信息。在显示前adapter会通过getItemViewType(positon)得到当前position对应的view类型。

    // 表示是一对一聊天,有两个类型的信息
    private final int VIEW_TYPE_COUNT = 2;
    // 用户的标识
    private final int VIEW_TYPE_USER = 0;
    // 开发者的标识
    private final int VIEW_TYPE_DEV = 1; 
    /* 
     * @return 表示当前适配器中有两种类型的数据,也就是说item会加载两个布局
     */
    @Override
    public int getViewTypeCount() {
        // 这里是一对一聊天,所以是两种类型
        return VIEW_TYPE_COUNT;
    }

    /* 
     * 通过list中的反馈对象,判断对象的类型,然后返回当前position对应的数据类型
     * 
     * @param position
     * @return 
     */
    @Override
    public int getItemViewType(int position) {
        position = getCurrentPosition(position);
        // 获取单条回复
        Reply reply = mConversation.getReplyList().get(position);
        if (Reply.TYPE_DEV_REPLY.equals(reply.type)) {
            // 开发者回复Item布局
            return VIEW_TYPE_DEV;
        } else {
            // 用户反馈、回复Item布局
            return VIEW_TYPE_USER;
        }
    }

1.3 加载数据

当我们发送或者接收到了一条新数据的时候,需要告诉适配器,增加当前数据的显示条数。

    /**
     * @description 添加了一条新数据后调用此方法
     *
     */
    public void addOneCount() {
        mCurrentCount++;
    }

用户在下拉刷新后应该能加载一定条数的聊天记录

    /**
     * @description 加载之前的聊天信息
     *
     * @param dataCount    一次性加载的数据数目
     */
    public void loadOldData(int dataCount) {
        int totalCount = mConversation.getReplyList().size();
        if (mCurrentCount >= totalCount) {
            // 如果要加载的数据超过了数据的总量,算出实际加载的数据条数
            dataCount = dataCount - (mCurrentCount - totalCount);
            mCurrentCount = totalCount;
        }
        mCurrentCount += dataCount;
        /**
         * 下面的代码可以放在异步任务中执行,这里图省事就没写异步任务。 对于这种从磁盘读取之前数据的人物,用asynTask就行,不用loader
         */
        mActivity.onUpdateSuccess(dataCount);
    }

如果旧的数据比较多,可能需要用异步任务来做处理,加载完毕后需要通过onUpdateSuccess方法通知activity更新界面。

1.4 ViewHolder

package com.kale.mycmcc;

import android.util.SparseArray;
import android.view.View;

public class ViewHolder {
    // I added a generic return type to reduce the casting noise in client code
    @SuppressWarnings("unchecked")
    public static <T extends View> T get(View view, int id) {
        SparseArray<View> viewHolder = (SparseArray<View>) view.getTag();
        if (viewHolder == null) {
            viewHolder = new SparseArray<View>();
            view.setTag(viewHolder);
        }
        View childView = viewHolder.get(id);
        if (childView == null) {
            childView = view.findViewById(id);
            viewHolder.put(id, childView);
        }
        return (T) childView;
    }
}    

1.5 getView()

在getview()方法中我们做了很多重要的处理。首先是,根据position加载不同的布局文件,并且将消息添加到textview中去。

     // 计算出位置
        position = getCurrentPosition(position);
        // 得到当前位置的reply对象
        Reply reply = mConversation.getReplyList().get(position);
        // 通过converView来优化listview
        if (convertView == null) {
            LayoutInflater inflater = LayoutInflater.from((Activity) mActivity);
            // 根据Type的类型来加载不同的Item布局
            if (Reply.TYPE_DEV_REPLY.equals(reply.type)) {
                // 如果是开发者回复的,那么就加载开发者回复的布局
                convertView = inflater.inflate(R.layout.umeng_fb_dev_reply, null);
            } else {
                convertView = inflater.inflate(R.layout.umeng_fb_user_reply, null);
            }
        }
        // 放入消息
        TextView textView = ViewHolder.get(convertView, R.id.reply_textView);
        textView.setText(reply.content);

然后,处理消息发送的结果,如果正在发送就显示进度条,如果发送成功就不显示状态,如果发送失败就显示感叹号。

/**
         * 检查发送状态,如果发送失败就进行提示
         * 这里的提示信息有进度条和感叹号两种。如果正在发送就显示进度条,如果发送失败就显示感叹号
         */
        if (!Reply.TYPE_DEV_REPLY.equals(reply.type)) {
            //System.out.println("states = " + reply.status);
            ImageView msgErrorIv;
            ProgressBar msgSentingPb;
            // 根据Reply的状态来设置replyStateFailed的状态,如果发送失败就显示提示图标
            msgErrorIv = (ImageView) ViewHolder.get(convertView, R.id.msg_error_imageView);
            msgSentingPb = (ProgressBar) ViewHolder.get(convertView, R.id.msg_senting_progressBar);
            
            if (Reply.STATUS_NOT_SENT.equals(reply.status)) {
                msgSentingPb.setVisibility(View.GONE);
                msgErrorIv.setVisibility(View.VISIBLE);
            } else if (Reply.STATUS_SENDING.equals(reply.status) || Reply.STATUS_WILL_SENT.equals(reply.status)) {
                msgSentingPb.setVisibility(View.VISIBLE);
                msgErrorIv.setVisibility(View.GONE);
            } else {
                msgSentingPb.setVisibility(View.GONE);
                msgErrorIv.setVisibility(View.GONE);
            }
        }

接着,处理消息发送时间的问题。如果两条消息时间间隔较长,那么就显示消息发送的时间。

/**
         * 设置回复时间,两条Reply之间相差1分钟则展示时间
         */
        ViewStub timeView = ViewHolder.get(convertView, R.id.time_view_stub);
        if ((position + 1) < mConversation.getReplyList().size()) {
            Reply nextReply = mConversation.getReplyList().get(position + 1);
            // 当两条回复相差1分钟时显示时间
            if (nextReply.created_at - reply.created_at > 1 * 60 * 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);
            }
        }

最后,返回convertView。getView()的代码如下:

    /* 
     * 通过list中的反馈对象,判断对象的类型,然后返回当前position对应的数据类型
     * 
     * @param position
     * @return 
     */
    @Override
    public int getItemViewType(int position) {
        position = getCurrentPosition(position);
        // 获取单条回复
        Reply reply = mConversation.getReplyList().get(position);
        if (Reply.TYPE_DEV_REPLY.equals(reply.type)) {
            // 开发者回复Item布局
            return VIEW_TYPE_DEV;
        } else {
            // 用户反馈、回复Item布局
            return VIEW_TYPE_USER;
        }
    }
    
    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        // 计算出位置
        position = getCurrentPosition(position);
        // 得到当前位置的reply对象
        Reply reply = mConversation.getReplyList().get(position);
        // 通过converView来优化listview
        if (convertView == null) {
            LayoutInflater inflater = LayoutInflater.from((Activity) mActivity);
            // 根据Type的类型来加载不同的Item布局
            if (Reply.TYPE_DEV_REPLY.equals(reply.type)) {
                // 如果是开发者回复的,那么就加载开发者回复的布局
                convertView = inflater.inflate(R.layout.umeng_fb_dev_reply, null);
            } else {
                convertView = inflater.inflate(R.layout.umeng_fb_user_reply, null);
            }
        }
        // 放入消息
        TextView textView = ViewHolder.get(convertView, R.id.reply_textView);
        textView.setText(reply.content);

        /**
         * 检查发送状态,如果发送失败就进行提示
         * 这里的提示信息有进度条和感叹号两种。如果正在发送就显示进度条,如果发送失败就显示感叹号
         */
        if (!Reply.TYPE_DEV_REPLY.equals(reply.type)) {
            //System.out.println("states = " + reply.status);
            ImageView msgErrorIv;
            ProgressBar msgSentingPb;
            // 根据Reply的状态来设置replyStateFailed的状态,如果发送失败就显示提示图标
            msgErrorIv = (ImageView) ViewHolder.get(convertView, R.id.msg_error_imageView);
            msgSentingPb = (ProgressBar) ViewHolder.get(convertView, R.id.msg_senting_progressBar);
            
            if (Reply.STATUS_NOT_SENT.equals(reply.status)) {
                msgSentingPb.setVisibility(View.GONE);
                msgErrorIv.setVisibility(View.VISIBLE);
            } else if (Reply.STATUS_SENDING.equals(reply.status) || Reply.STATUS_WILL_SENT.equals(reply.status)) {
                msgSentingPb.setVisibility(View.VISIBLE);
                msgErrorIv.setVisibility(View.GONE);
            } else {
                msgSentingPb.setVisibility(View.GONE);
                msgErrorIv.setVisibility(View.GONE);
            }
        }

        /**
         * 设置回复时间,两条Reply之间相差1分钟则展示时间
         */
        ViewStub timeView = ViewHolder.get(convertView, R.id.time_view_stub);
        if ((position + 1) < mConversation.getReplyList().size()) {
            Reply nextReply = mConversation.getReplyList().get(position + 1);
            // 当两条回复相差1分钟时显示时间
            if (nextReply.created_at - reply.created_at > 1 * 60 * 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);
            }
        }
        return convertView;
    }

1.6 适配器的全部代码

package com.kale.mycmcc;

import java.text.SimpleDateFormat;
import java.util.Date;

import android.app.Activity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewStub;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.TextView;

import com.umeng.fb.model.Conversation;
import com.umeng.fb.model.Reply;

/**
 * @author:Jack Tony
 * @description : 定义回话界面的adapter
 * @date :2015年2月9日
 */
class ReplyAdapter extends BaseAdapter {

    // 表示是一对一聊天,有两个类型的信息
    private final int VIEW_TYPE_COUNT = 2;
    // 用户的标识
    private final int VIEW_TYPE_USER = 0;
    // 开发者的标识
    private final int VIEW_TYPE_DEV = 1; 

    // 一次性加载多少条数据
    // private final int LOAD_DATA_NUM = 10;

    private int mCurrentCount = 10; // 默认一次性显示多少条数据

    private DataCallbackActivity mActivity; // 实现反馈接口的activity
    // 回话对象
    private Conversation mConversation; 

    public ReplyAdapter(DataCallbackActivity activity, Conversation conversation) {
        mActivity = activity;
        mConversation = conversation;
    }

    /**
     * @description 添加了一条新数据后调用此方法
     *
     */
    public void addOneCount() {
        mCurrentCount++;
    }

    @Override
    public int getCount() {
        // 如果开始时的数目小于一次性显示的数目,就按照当前的数目显示,否则会数组越界
        int totalCount = mConversation.getReplyList().size();
        if (totalCount < mCurrentCount) {
            mCurrentCount = totalCount;
        }
        return mCurrentCount;
    }

    @Override
    public Object getItem(int position) {
        // getCurrentPosition(position)通过计算得出当前相对的position
        position = getCurrentPosition(position);
        return mConversation.getReplyList().get(position);
    }

    @Override
    public long getItemId(int position) {
        return getCurrentPosition(position);
    }

    /* 
     * @return 表示当前适配器中有两种类型的数据,也就是说item会加载两个布局
     */
    @Override
    public int getViewTypeCount() {
        // 这里是一对一聊天,所以是两种类型
        return VIEW_TYPE_COUNT;
    }

    /* 
     * 通过list中的反馈对象,判断对象的类型,然后返回当前position对应的数据类型
     * 
     * @param position
     * @return 
     */
    @Override
    public int getItemViewType(int position) {
        position = getCurrentPosition(position);
        // 获取单条回复
        Reply reply = mConversation.getReplyList().get(position);
        if (Reply.TYPE_DEV_REPLY.equals(reply.type)) {
            // 开发者回复Item布局
            return VIEW_TYPE_DEV;
        } else {
            // 用户反馈、回复Item布局
            return VIEW_TYPE_USER;
        }
    }
    
    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        // 计算出位置
        position = getCurrentPosition(position);
        // 得到当前位置的reply对象
        Reply reply = mConversation.getReplyList().get(position);
        // 通过converView来优化listview
        if (convertView == null) {
            LayoutInflater inflater = LayoutInflater.from((Activity) mActivity);
            // 根据Type的类型来加载不同的Item布局
            if (Reply.TYPE_DEV_REPLY.equals(reply.type)) {
                // 如果是开发者回复的,那么就加载开发者回复的布局
                convertView = inflater.inflate(R.layout.umeng_fb_dev_reply, null);
            } else {
                convertView = inflater.inflate(R.layout.umeng_fb_user_reply, null);
            }
        }
        // 放入消息
        TextView textView = ViewHolder.get(convertView, R.id.reply_textView);
        textView.setText(reply.content);

        /**
         * 检查发送状态,如果发送失败就进行提示
         * 这里的提示信息有进度条和感叹号两种。如果正在发送就显示进度条,如果发送失败就显示感叹号
         */
        if (!Reply.TYPE_DEV_REPLY.equals(reply.type)) {
            //System.out.println("states = " + reply.status);
            ImageView msgErrorIv;
            ProgressBar msgSentingPb;
            // 根据Reply的状态来设置replyStateFailed的状态,如果发送失败就显示提示图标
            msgErrorIv = (ImageView) ViewHolder.get(convertView, R.id.msg_error_imageView);
            msgSentingPb = (ProgressBar) ViewHolder.get(convertView, R.id.msg_senting_progressBar);
            
            if (Reply.STATUS_NOT_SENT.equals(reply.status)) {
                msgSentingPb.setVisibility(View.GONE);
                msgErrorIv.setVisibility(View.VISIBLE);
            } else if (Reply.STATUS_SENDING.equals(reply.status) || Reply.STATUS_WILL_SENT.equals(reply.status)) {
                msgSentingPb.setVisibility(View.VISIBLE);
                msgErrorIv.setVisibility(View.GONE);
            } else {
                msgSentingPb.setVisibility(View.GONE);
                msgErrorIv.setVisibility(View.GONE);
            }
        }

        /**
         * 设置回复时间,两条Reply之间相差1分钟则展示时间
         */
        ViewStub timeView = ViewHolder.get(convertView, R.id.time_view_stub);
        if ((position + 1) < mConversation.getReplyList().size()) {
            Reply nextReply = mConversation.getReplyList().get(position + 1);
            // 当两条回复相差1分钟时显示时间
            if (nextReply.created_at - reply.created_at > 1 * 60 * 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);
            }
        }
        return convertView;
    }

    /**
     * @description 重要方法,计算出当前的position
     *
     * @param position
     * @return 当前的position
     */
    private int getCurrentPosition(int position) {
        int totalCount = mConversation.getReplyList().size();
        if (totalCount < mCurrentCount) {
            mCurrentCount = totalCount;
        }
        return totalCount - mCurrentCount + position;
        // return position;
    }

    /**
     * @description 加载之前的聊天信息
     *
     * @param dataCount    一次性加载的数据数目
     */
    public void loadOldData(int dataCount) {
        int totalCount = mConversation.getReplyList().size();
        if (mCurrentCount >= totalCount) {
            // 如果要加载的数据超过了数据的总量,算出实际加载的数据条数
            dataCount = dataCount - (mCurrentCount - totalCount);
            mCurrentCount = totalCount;
        }
        mCurrentCount += dataCount;
        /**
         * 下面的代码可以放在异步任务中执行,这里图省事就没写异步任务。 对于这种从磁盘读取之前数据的人物,用asynTask就行,不用loader
         */
        mActivity.onUpdateSuccess(dataCount);
    }

}
View Code

二、Activity

2.1 用接口给Activity添加数据反馈的方法

聊天的activity肯定要接收数据加载反馈结果,所以我定义了一个接口,让activity实现它。

DataCallbackActivity.java

package com.kale.mycmcc;

public interface DataCallbackActivity {

    public void onUpdateSuccess(int dataNum);
    public void onUpdateError();
}

2.2 监听listview的状态并进行处理

通过模仿QQ我们发现,当listview滚动的时候就是用户查看聊天记录的时候,所以应该隐藏输入法,给用户更大的浏览空间。

    /**
     * @author:Jack Tony
     * @description : 监听listview的滑动状态,如果到了顶部就刷新数据
     * @date :2015年2月9日
     */
    private class ListViewListener implements OnScrollListener {

        InputMethodManager inputMethodManager;

        public ListViewListener() {
            inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
        }

        @Override
        public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
        }

        @Override
        public void onScrollStateChanged(AbsListView view, int scrollState) {
            switch (scrollState) {
            // 滚动结束
            case OnScrollListener.SCROLL_STATE_IDLE:
                // 滚动停止
                if (view.getLastVisiblePosition() == (view.getCount() - 1)) {
                    // 如果滚动到底部,就强制显示输入法
                    // inputMethodManager.showSoftInput(mInputEt,
                    // InputMethodManager.SHOW_FORCED);
                } else if (view.getFirstVisiblePosition() == 0) {
                    loadOldData();
                }
                break;
            case OnScrollListener.SCROLL_STATE_FLING:
                // 开始滚动
                break;
            case OnScrollListener.SCROLL_STATE_TOUCH_SCROLL:
                if (inputMethodManager.isActive()) {
                    // 正在滚动, 如果在滚动,就隐藏输入法
                    inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0);
                }
                break;
            }

        }

    }

2.3通过监听EditText的状态来设置button的样式

当editText中没有文字的时候button不可用,如果有文字button变得可用。在这里我还做了回车键发送消息的功能,方便快速发送信息。

        /**
         * 设置发送消息的按钮和输入框 按下回车键,发送消息
         */
        mInputEt = (EditText) findViewById(R.id.conversation_editText);
        mInputEt.setOnKeyListener(new OnKeyListener() {

            @Override
            public boolean onKey(View v, int keyCode, KeyEvent event) {
                // 这两个条件必须同时成立,如果仅仅用了enter判断,就会执行两次
                if (keyCode == KeyEvent.KEYCODE_ENTER && event.getAction() == KeyEvent.ACTION_DOWN) {
                    sendMsgToDev();
                    return true;
                }
                return false;
            }
        });
        // 给editText添加监听器
        mInputEt.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().trim().isEmpty();
                sendBtn.setEnabled(!isEmpty);
                sendBtn.setTextColor(isEmpty ? 0xffa1a2a5 : 0xffffffff);
            }
        });

2.4 发送消息

点击button后,发送消息并且让数据和服务器进行同步

        /**
         * 设置发送按钮的事件
         */
        final Button sendBtn = (Button) findViewById(R.id.conversation_send_btn);
        sendBtn.setEnabled(false);
        sendBtn.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {
                sendMsgToDev();
            }
        });

发送消息

    /**
     * @description 发送消息
     *
     */
    private void sendMsgToDev() {
        String replyMsg = mInputEt.getText().toString().trim();
        mInputEt.getText().clear();
        if (!TextUtils.isEmpty(replyMsg)) {
            // 将反馈信息放入回话中,有可能发送失败,失败的话在适配器中处理
            mComversation.addUserReply(replyMsg);
            sync(false);
            mAdapter.addOneCount();
        }
    }

数据同步

    /**
     * @description 更新数据
     *
     */
    private void updateData() {
        mAdapter.notifyDataSetChanged();
    }

    /**
     * @description 将数据和服务器同步
     *
     */
    private void sync(final boolean isDevReply) {
        if (!isDevReply) {
            // 如果不是开发者回复的信息,那么就先更新数据,再同步到服务器(快)
            updateData();
        }
        mComversation.sync(new SyncListener() {

            @Override
            public void onSendUserReply(List<Reply> replyList) {
            }

            /*
             * 接收开发者回复的信息
             */
            @Override
            public void onReceiveDevReply(List<Reply> replyList) {
                if (replyList == null || replyList.size() < 1) {
                    return;
                }
                if (isDevReply) {
                    // 如果是开发者回复的,就在这里进行数据的同步操作
                    updateData();
                }
            }
        });
        updateData();
    }

2.5 配置下拉刷新控件

当用户下拉刷新时,我们需要去加载n条聊天记录,加载完毕后通知activity更新视图。

     mSwipeLayout = (SwipeRefreshLayout) findViewById(R.id.swipe_container);
        mSwipeLayout.setSize(SwipeRefreshLayout.DEFAULT);
        // 设置下拉圆圈上的颜色,蓝色、绿色、橙色、红色
        mSwipeLayout.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);
        mSwipeLayout.setOnRefreshListener(new OnRefreshListener() {

            @Override
            public void onRefresh() {
                mAdapter.loadOldData(LOAD_DATA_NUM);
            }
        });

数据加载完毕后的回调方法:

    /*
     * 当加载旧的数据完成后的回调方法
     * 
     * @param dataNum 加载了多少个旧的数据
     */
    @Override
    public void onUpdateSuccess(int dataNum) {
        mSwipeLayout.setRefreshing(false);
        // 加载完毕旧的数据,跳到刷新出来数据的位置
        if (dataNum - 1 >= 0) {
            mListView.setSelection(dataNum - 1);
        } else {
            Toast.makeText(mContext, "没有数据了", 0).show();
            mListView.setSelection(0);
        }
    }

    @Override
    public void onUpdateError() {
        // TODO 自动生成的方法存根

    }

2.6 Activity的全部代码

package com.kale.mycmcc;

import java.util.List;

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.view.KeyEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnKeyListener;
import android.view.inputmethod.InputMethodManager;
import android.widget.AbsListView;
import android.widget.AbsListView.OnScrollListener;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.Toast;

import com.umeng.fb.FeedbackAgent;
import com.umeng.fb.SyncListener;
import com.umeng.fb.model.Conversation;
import com.umeng.fb.model.Conversation.OnChangeListener;
import com.umeng.fb.model.Reply;
import com.umeng.message.PushAgent;

public class CustomActivity extends BaseActivity implements DataCallbackActivity {

    private final int LOAD_DATA_NUM = 10;

    private static Context mContext;
    private Conversation mComversation;

    private EditText mInputEt;
    private SwipeRefreshLayout mSwipeLayout;
    private ListView mListView;
    private ReplyAdapter mAdapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.umeng_fb__conversation);

        mContext = this;
        mComversation = new FeedbackAgent(this).getDefaultConversation();
        mAdapter = new ReplyAdapter(this, mComversation);

        inMainActivity();
        initView(); // 初始化各种view
        sync(false); // 更新数据

        // 开启语音反馈
        // new FeedbackAgent(this).openAudioFeedback();
        new FeedbackAgent(this).sync();

        mComversation.setOnChangeListener(new OnChangeListener() {

            @Override
            public void onChange() {
                // 发送消息后会自动调用此方法,在这里更新下发送状态
                updateData();
            }
        });
    }

    /**
     * @description 应该在主activity使用的方法
     *
     */
    private void inMainActivity() {
        // 开启友盟消息推送服务
        PushAgent.getInstance(this).enable();
        // 开启反馈回复推送服务
        FeedbackAgent fbAgent = new FeedbackAgent(this);
        fbAgent.openFeedbackPush();

    }

    /**
     * @description 初始化各种view
     *
     */
    private void initView() {
        mSwipeLayout = (SwipeRefreshLayout) findViewById(R.id.swipe_container);
        mSwipeLayout.setSize(SwipeRefreshLayout.DEFAULT);
        // 设置下拉圆圈上的颜色,蓝色、绿色、橙色、红色
        mSwipeLayout.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);
        mSwipeLayout.setOnRefreshListener(new OnRefreshListener() {

            @Override
            public void onRefresh() {
                mAdapter.loadOldData(LOAD_DATA_NUM);
            }
        });

        /**
         * list不显示分割线,设置滚动监听器,设置适配器
         */
        mListView = (ListView) findViewById(R.id.conversation_listView);
        // 设置listview不显示分割线
        mListView.setDivider(null);
        mListView.setAdapter(mAdapter);
        mListView.setOnScrollListener(new ListViewListener());

        /**
         * 设置发送按钮的事件
         */
        final Button sendBtn = (Button) findViewById(R.id.conversation_send_btn);
        sendBtn.setEnabled(false);
        sendBtn.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {
                sendMsgToDev();
            }
        });

        /**
         * 设置发送消息的按钮和输入框 按下回车键,发送消息
         */
        mInputEt = (EditText) findViewById(R.id.conversation_editText);
        mInputEt.setOnKeyListener(new OnKeyListener() {

            @Override
            public boolean onKey(View v, int keyCode, KeyEvent event) {
                // 这两个条件必须同时成立,如果仅仅用了enter判断,就会执行两次
                if (keyCode == KeyEvent.KEYCODE_ENTER && event.getAction() == KeyEvent.ACTION_DOWN) {
                    sendMsgToDev();
                    return true;
                }
                return false;
            }
        });
        // 给editText添加监听器
        mInputEt.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().trim().isEmpty();
                sendBtn.setEnabled(!isEmpty);
                sendBtn.setTextColor(isEmpty ? 0xffa1a2a5 : 0xffffffff);
            }
        });

    }

    /**
     * @description 发送消息
     *
     */
    private void sendMsgToDev() {
        String replyMsg = mInputEt.getText().toString().trim();
        mInputEt.getText().clear();
        if (!TextUtils.isEmpty(replyMsg)) {
            // 将反馈信息放入回话中,有可能发送失败,失败的话在适配器中处理
            mComversation.addUserReply(replyMsg);
            sync(false);
            mAdapter.addOneCount();
        }
    }

    /*
     * 当这个activity在最上方时不重复启动activity, 如果调用了startActivity,那么就更新下视图
     * 
     * @param intent
     */
    @Override
    protected void onNewIntent(Intent intent) {
        super.onNewIntent(intent);
        sync(true);
        mAdapter.addOneCount();
    }

    /**
     * @description 更新数据
     *
     */
    private void updateData() {
        mAdapter.notifyDataSetChanged();
    }

    /**
     * @description 将数据和服务器同步
     *
     */
    private void sync(final boolean isDevReply) {
        if (!isDevReply) {
            // 如果不是开发者回复的信息,那么就先更新数据,再同步到服务器(快)
            updateData();
        }
        mComversation.sync(new SyncListener() {

            @Override
            public void onSendUserReply(List<Reply> replyList) {
            }

            /*
             * 接收开发者回复的信息
             */
            @Override
            public void onReceiveDevReply(List<Reply> replyList) {
                if (replyList == null || replyList.size() < 1) {
                    return;
                }
                if (isDevReply) {
                    // 如果是开发者回复的,就在这里进行数据的同步操作
                    updateData();
                }
            }
        });
        updateData();
    }

    /*
     * 当加载旧的数据完成后的回调方法
     * 
     * @param dataNum 加载了多少个旧的数据
     */
    @Override
    public void onUpdateSuccess(int dataNum) {
        mSwipeLayout.setRefreshing(false);
        // 加载完毕旧的数据,跳到刷新出来数据的位置
        if (dataNum - 1 >= 0) {
            mListView.setSelection(dataNum - 1);
        } else {
            Toast.makeText(mContext, "没有数据了", 0).show();
            mListView.setSelection(0);
        }
    }

    @Override
    public void onUpdateError() {
        // TODO 自动生成的方法存根

    }

    /**
     * @description 因为这里获取数据很快,所以看不出效果。
     *              当你的数据是从数据库或磁盘中读取的,并且加载的数据很多的时候就可以用下面的方法了。
     *
     */
    private void loadOldData() {
        // 如果滚动到顶部,就刷新出旧的数据
        // System.out.println(" load old data");
        /*
         * mSwipeLayout.setRefreshing(true);
         * mAdapter.loadOldData(LOAD_DATA_NUM); mSwipeLayout.setEnabled(false);
         */
    }

    /**
     * @author:Jack Tony
     * @description : 监听listview的滑动状态,如果到了顶部就刷新数据
     * @date :2015年2月9日
     */
    private class ListViewListener implements OnScrollListener {

        InputMethodManager inputMethodManager;

        public ListViewListener() {
            inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
        }

        @Override
        public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
        }

        @Override
        public void onScrollStateChanged(AbsListView view, int scrollState) {
            switch (scrollState) {
            // 滚动结束
            case OnScrollListener.SCROLL_STATE_IDLE:
                // 滚动停止
                if (view.getLastVisiblePosition() == (view.getCount() - 1)) {
                    // 如果滚动到底部,就强制显示输入法
                    // inputMethodManager.showSoftInput(mInputEt,
                    // InputMethodManager.SHOW_FORCED);
                } else if (view.getFirstVisiblePosition() == 0) {
                    loadOldData();
                }
                break;
            case OnScrollListener.SCROLL_STATE_FLING:
                // 开始滚动
                break;
            case OnScrollListener.SCROLL_STATE_TOUCH_SCROLL:
                if (inputMethodManager.isActive()) {
                    // 正在滚动, 如果在滚动,就隐藏输入法
                    inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0);
                }
                break;
            }

        }

    }
}
View Code

三、不足

因为友盟的开发文档写的真是不清不楚,所以我很难这些东西基本都是试验出来的。我暂时找到一个很好的办法来加载开发者的反馈信息,这里用的是intent的方式来通知的,虽然简单,但会出现开发者一回复,界面会立刻跳转到当前的activity。想要的效果应该是判断当前activity是不是在前台,如果在前台就更新界面,载入新的信息。如果不在前台,就不进行更新信息的操作。把更新信息的操作放在activity的oncreat或者是其他生命周期中做。这个可以用广播来实现,但因为涉及到太多友盟的API,所以就不多说了,谁知道它什么时候又更新了API呢。

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

注意:为了我项目的安全性,源码中没有添加友盟的UMENG_APPKEY、UMENG_MESSAGE_SECRET,请大家自行去友盟建立一个应用,把你申请到的码写在manifest.xml中。这样你就可以完整的测试了~

原文地址:https://www.cnblogs.com/tianzhijiexian/p/4295420.html