FanChat學習筆記(五)——終結篇

上一篇講了主界面,裏面包含了個人動態、聯繫人列表、會話列表,沒有閱讀的話建議看看FanChat學習筆記(四)——主頁面。本文接着說一說添加好友與聊天界面的邏輯及一些重要的知識點。

  1. 添加好友——AddFriendActivity用到的知識點
  2. 添加好友——AddFriendActivity界面邏輯
  3. 聊天——ChatActivity用到的知識點
  4. 聊天——ChatActivity界面邏輯

1、添加好友——AddFriendActivity用到的知識點:

setHasFixedSize(boolean hasFixedSize)

RecycleView的這個方法的目的是RecycleView的ItemView寬高保持固定,不管Item增加或者減少,ItemView的寬高都不會變化。如果沒有設置爲false的話,RecyclerView會需要額外計算每個item的尺寸,而調用onMesure方法。

EventBus 3.0的使用

其實這個纔是今天的重點,甚至是整個項目的重點,因爲我研究這個項目就是爲了學習EventBus 3.0在項目中的實際運用。在研究該項目之前,也曾翻過弘神的博客Android EventBus源碼解析 帶你深入理解EventBus。因爲我記得當初我面試現在這份工作的時候,老闆說:你們只知道那些東西怎麼用,但是不只是爲什麼這麼用?知其然不知其所以然!
所以,我們先來看看這個EventBus 3.0如何使用,以及其原理是怎麼回事?
其實Github上面已經介紹了一遍,那麼我儘量介紹得和上面不一樣吧!
首先依賴,這個是沒有辦法不一樣的!(因爲我已經開始學習AndroidStudio了,所以Eclipse就不討論了!)

compile 'org.greenrobot:eventbus:3.0.0'

接下來我們就可以註冊了,註冊的作用在後面再解釋!

EventBus.getDefault().register(this);

註冊了是不是就可以註銷了?這就好像李白清的段子,英語剛剛說了一句“Hello”馬上來一句“Goodbye”一樣。所以我們不能鬧這樣的笑話!
那麼我們不結束還能說什麼啊?英文是一句都不會啊!!既然不會,我們就去自定義一個事件,就是我們要消息數據,這個就是我們要表達的內容!!

package com.itheima.leon.qqdemo.event;

/**
 * 創建者:   Leon
 * 創建時間:  2016/10/19 20:26
 * 描述:    EventBus事件
 */
public class AddFriendEvent {
    public static final String TAG = "AddFriendEvent";

    private String mFriendName;

    private String mReason;

    public AddFriendEvent(String friendName, String reason) {
        this.mFriendName = friendName;
        this.mReason = reason;
    }


    public String getFriendName() {
        return mFriendName;
    }

    public void setFriendName(String friendName) {
        this.mFriendName = friendName;
    }

    public String getReason() {
        return mReason;
    }

    public void setReason(String reason) {
        this.mReason = reason;
    }
}

這個類需要講一下,EventBus的事件類型是Object,也就是說我們可以傳遞任意數據。這一點我們在下一個方法後來證實。因此,我們這個類可以只有屬性沒有get/set方法,類裏面的數據類型以及相應的方法根據自己的需要構造。比如AddFriendEvent是按照標準實體來構建的,所有的屬性都是私有的,然後提供公共方法來設置相應屬性,而下面我們需要到這些數據的時候就可以通過方法來調用。
OK,我們知道說什麼話了,那麼我們開始對話吧?

 @Subscribe(threadMode = ThreadMode.MainThread, sticky = true, priority = 100)
    public void deleteFriend(AddFriendEvent event){
        Log.d(TAG, "deleteFriend: "+event.getFriendName());
    }

這裏相當於收到了一句話,但是這句話就像是一個成語,簡單一個方法卻囊括了很多內容,我們一點一點來剖析。方法大家都明白是什麼意思?不明白也沒有關係,只要知道我們通過EventBus發佈的消息會通過這個方法來實現,這個方法類似於回調。就是我們在其它地方發佈消息,然後這個方法就會執行。接下來我們就說一下註解吧!

  1. threadMode

    這個參數是表示這個方法應該在什麼線程運行?屬於必選參數,可選線程有四種,如下:

    1. ThreadMode.PostThread
      在發佈消息所在的線程執行

    2. ThreadMode.MainThread
      在主線程(UI線程)執行

    3. BackgroundThread
      在子線程(後臺線程)執行

    4. ThreadMode.Async
      必須在子線程執行(強制在後臺線程)執行

  2. sticky

    這個參數默認爲false,意思是deleteFriend方法需要先註冊(register), 再發布消息,才能接受到事件; 如果使用post(Object event)方法發佈消息的話該參數完全沒有意義。它的意義在於使用postSticky(Object event)發佈消息時, 參數如果爲true,那麼可以不需要先註冊, 也能接受到事件.如果參數爲false的話,不管什麼方法發佈消息都得先註冊才能接受到事件。

  3. priority

    這個參數代表優先級,屬於可選參數,默認值是0。概念和有序廣播的接受順序類似,即優先級越高,會優先被調用。也僅僅如此而已。比如我們還有一個同類型參數的方法,如下,

 @Subscribe(threadMode = ThreadMode.BACKGROUND,sticky = true,priority = 90)
    public void addFriend(AddFriendEvent event) {
        Log.d(TAG, "addFriend: ");

    }

那麼我們也僅僅是先執行當前這個deleteFriend方法,再執行addFriend方法,而並不能像有序廣播那樣我在優先的接受者(訂閱者)這裏可以通過abortBroadcast()方法終止傳播,這個在EventBus裏面是做不到的。另外,如果我這個接受者只有一個,但是我把級別調至最高(經測試,該參數類型爲Int類型,所以最大值爲2147483647,當然使用Int的最小值-2147483648也不會報錯)也並不會有任何異常?最後提一點,這個參數不受發佈消息的方法影響,兩種方法優先級都是有效的!

接下來可以來研究實現的方法了,埋上面說的坑——“EventBus的事件類型是Object”。這裏我們可以來研究一下發布消息的兩種方式:

void postSticky(Object event)
void post(Object event)

看見上面的方法就知道了,不管我們使用何種方式發佈消息,都可以傳輸Object數據給接收方(訂閱者)。其實看postSticky(Object event)的源碼就知道,event其實是保存了兩次:

  /**
     * Posts the given event to the event bus and holds on to the event (because it is sticky). The most recent sticky
     * event of an event's type is kept in memory for future access by subscribers using {@link Subscribe#sticky()}.
     */
    public void postSticky(Object event) {
        synchronized (stickyEvents) {
            stickyEvents.put(event.getClass(), event);
        }
        // Should be posted after it is putted, in case the subscriber wants to remove immediately
        post(event);
    }

代碼中前者在沒有註冊的時候就可以通知到接收方,也就是說接收方(訂閱者)sticky參數爲true的時候就可以在EventBus未註冊的時候執行了。而後者就不行,它需要在註冊的時候告訴EventBus當前類裏面有幾個接收方(訂閱者)在等待接收消息,註冊實現的原理是將當前的類class作爲key,將接收方法的參數作爲value,然後以鍵對值的形式用Map來保存的。當用post方法發佈消息時就遍歷Map,根據參數類型通知到對應的訂閱者。所以這裏需要注意兩個問題,如果兩個訂閱者的參數類型相同的話,那麼它會通知到每一個訂閱者,也就是說每一個訂閱者方法都會執行。其次,如果在不同的類需要訂閱(接收消息)的話,那麼就需要在不同的類註冊EventBus。
因此,postSticky方法的原理應該是遍歷所有類,然後找到對應的訂閱方法並判斷其sticky參數是否爲true,根據其值決定是否通知訂閱者。當然,這個只是我的猜想。
在上面我們通過註冊來說“Hello”,然後通過post(Object event)方法(或者是postSticky(Object event))方法來說話(發佈消息),通過註解的方式來傾聽人家的對話(接收人家的消息),交談完畢之後就應該說“Goodbye”了。那麼我們應該通過什麼方式來說呢?

EventBus.getDefault().unregister(this);

上面我們說過EventBus註冊的原理,那麼我們在當前類關閉的時候就應該將保存的class給清除掉,避免內存泄漏。所以我們應該在註冊的當前類的時候,考慮什麼時候應該註銷掉,假如我們需要在Activity裏使用EventBus訂閱消息的話,那麼我們可以在onCreate方法裏面註冊,在onDestory方法裏面註銷。說到內存泄漏我想到了內存泄漏和內存溢出的區別,如下:

內存溢出 out of memory,是指程序在申請內存時,沒有足夠的內存空間供其使用,出現out of memory;比如申請了一個integer,但給它存了long才能存下的數,那就是內存溢出。
內存泄露 memory leak,是指程序在申請內存後,無法釋放已申請的內存空間,一次內存泄露危害可以忽略,但內存泄露堆積後果很嚴重,無論多少內存,遲早會被佔光。
memory leak會最終會導致out of memory!

OK,上面講完了使用EventBus來收發數據溝通,如果說得還是覺得膚淺的話,可以下載項目看看項目裏具體是怎麼使用的。

2. 添加好友——AddFriendActivity界面邏輯:

在聊邏輯之前,先看看具體界面,否則會感覺很抽象,所以先上圖:

這裏寫圖片描述

首先,當我們輸入搜索的關鍵字以後,然後點擊圖片中的任意一個放大鏡都可以實現搜索好友,搜索好友的主要實現在代碼的業務邏輯實現類AddFriendPresenterImpl,所以先看看這個方法:

    @Override
    public void searchFriend(final String keyword) {

        mAddFriendView.onStartSearch();
        //注:模糊查詢只對付費用戶開放,付費後可直接使用。
        BmobQuery<User> query = new BmobQuery<User>();
        query.addWhereContains("username", keyword).addWhereNotEqualTo("username", EMClient.getInstance().getCurrentUser());
        query.findObjects(new FindListener<User>() {
            @Override
            public void done(List<User> list, BmobException e) {
                processResult(list, e);
            }
        });
    }

通過代碼可以發現,就是通過Bmob的API實現查詢,然後將查詢結果返回給processResult方法了,那麼我們接下來可以看看這個方法具體做了什麼事務?

private void processResult(final List<User> list, final BmobException e) {
        ThreadUtils.runOnBackgroundThread(new Runnable() {
            @Override
            public void run() {
                if (e == null && list.size() > 0) {
                    List<String> contacts = DatabaseManager.getInstance().queryAllContacts();
                    for (int i = 0; i < list.size(); i++) {
                        AddFriendItem item = new AddFriendItem();
                        item.timestamp = list.get(i).getCreatedAt();
                        item.userName = list.get(i).getUsername();
                        item.isAdded = contacts.contains(item.userName);
                        mAddFriendItems.add(item);
                    }
                    ThreadUtils.runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            mAddFriendView.onSearchSuccess();
                        }
                    });
                } else {
                    ThreadUtils.runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            mAddFriendView.onSearchFailed();
                        }
                    });
                }
            }
        });
    }

代碼邏輯比較簡單,但是需要注意的是線程的切換,返回的結果在後臺線程處理,UI操作需要在UI線程。接下來我們看看UI的好友搜索成功和搜索失敗具體是怎麼執行的?
搜索失敗

 @Override
    public void onSearchFailed() {

        hideProgress();
        mFriendNotFound.setVisibility(View.VISIBLE);
        mRecyclerView.setVisibility(View.GONE);
    }

具體效果:

這裏寫圖片描述

搜索成功:

    @Override
    public void onSearchSuccess() {
        hideProgress();
        mFriendNotFound.setVisibility(View.GONE);
        mRecyclerView.setVisibility(View.VISIBLE);
        mAddFriendListAdapter.notifyDataSetChanged();
    }
//mAddFriendListAdapter的實例化
 mAddFriendListAdapter = new AddFriendListAdapter(this, mAddFriendPresenter.getAddFriendList());

我們實例化的時候調用了AddFriendPresenterImpl對象的getAddFriendList()方法獲取數據,那麼我們回去看看數據返回的什麼就明白了!

   @Override
    public List<AddFriendItem> getAddFriendList() {
        return mAddFriendItems;
    }

上面搜索成功時我們在後臺線程不是完成了對mAddFriendItems數據的填充調用UI線程更新嗎?這裏就調用了我們填充的數據,最後刷新後如圖:

這裏寫圖片描述

至於爲什麼有的顯示已經添加,有的顯示已添加,這個就涉及到之前的模塊化思維,大家可以具體看相關的實體以及UI的實現,這裏就不繼續重複講了!
我們接下來要講的就是當我們點擊添加的Button時,我們就需要用到EventBus了,上面講了模塊化思維,所以我們的Button點擊事件並沒有在Adapter裏面實現,而是在模塊AddFriendItemView裏面實現的,具體看代碼:

 @OnClick(R.id.add)
    public void onClick() {
        String friendName = mUserName.getText().toString().trim();

        AddFriendEvent event = new AddFriendEvent(friendName, "我是馬雲,有沒有興趣聊一聊?");
        EventBus.getDefault().post(event);
    }

AddFriendItemView裏面並沒有註冊、註銷,因爲它只負責說(發佈消息),不負責聽(訂閱消息),所以我們看看AddFriendPresenterImpl聽到以後做了什麼?

   @Subscribe(threadMode = ThreadMode.BACKGROUND,priority = 90)
    public void addFriend(AddFriendEvent event) {
        Log.d(TAG, "addFriend: ");
        try {
            EMClient.getInstance().contactManager().addContact(event.getFriendName(), event.getReason());
            ThreadUtils.runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    mAddFriendView.onAddFriendSuccess();
                }
            });
        } catch (HyphenateException e) {
            e.printStackTrace();
            ThreadUtils.runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    mAddFriendView.onAddFriendFailed();
                }
            });
        }
    }

邏輯還是比較簡單,調用了環信提供的API添加好友,然後對添加成功和失敗分別調用了UI的刷新,此處再次強調線程的切換。接下來我們需要在AddFriendActivity裏面看看好友添加成功和添加的相應UI邏輯:

  @Override
    public void onAddFriendSuccess() {
        toast(getString(R.string.add_friend_success));
    }

    @Override
    public void onAddFriendFailed() {
        toast(getString(R.string.add_friend_failed));
    }

ok,我們已經實現了對於AddFriendActivity界面邏輯,這裏就不再更新了有興趣的朋友可以繼續在此基礎上繼續完善,比如右上角增加一個返回按鈕、好友添加成功跳轉到聊天界面等。

3. 聊天——ChatActivity用到的知識點

  1. smoothScrollToPosition(int position)
  2. scrollToPosition(int position)

二者有什麼分別?前者我之前根本沒有聽說過,然後我就老老實實的查源碼,結果發現是一個坑:

 public void smoothScrollToPosition(int position) {
        if (mLayoutFrozen) {
            return;
        }
        if (mLayout == null) {
            Log.e(TAG, "Cannot smooth scroll without a LayoutManager set. " +
                    "Call setLayoutManager with a non-null argument.");
            return;
        }
        mLayout.smoothScrollToPosition(this, mState, position);
    }

繼續挖坑:

/**
         * <p>Smooth scroll to the specified adapter position.</p>
         * <p>To support smooth scrolling, override this method, create your {@link SmoothScroller}
         * instance and call {@link #startSmoothScroll(SmoothScroller)}.
         * </p>
         * @param recyclerView The RecyclerView to which this layout manager is attached
         * @param state    Current State of RecyclerView
         * @param position Scroll to this adapter position.
         */
        public void smoothScrollToPosition(RecyclerView recyclerView, State state,
                int position) {
            Log.e(TAG, "You must override smoothScrollToPosition to support smooth scrolling");
        }

我必須實現 smoothScrollToPosition,在源碼裏要求我實現?我完全是懵逼了,於是我繼續查官網文檔:

smoothScrollToPosition(int position)
Starts a smooth scroll to an adapter position.
//開始平滑滾動到適配器位置。

我還是不明白什麼是平滑的滾動到適配器位置,於是我點開方法看了看:

smoothScrollToPosition

void smoothScrollToPosition (int position)
Starts a smooth scroll to an adapter position.

要支持平滑滾動,必須覆蓋smoothScrollToPosition(RecyclerView,State,int)並創建RecyclerView.SmoothScroller。

To support smooth scrolling, you must override smoothScrollToPosition(RecyclerView, State, int) and create a RecyclerView.SmoothScroller.

RecyclerView.LayoutManager負責創建實際的滾動操作。 如果要提供自定義平滑滾動邏輯,請在LayoutManager中覆蓋smoothScrollToPosition(RecyclerView,State,int)。

RecyclerView.LayoutManager is responsible for creating the actual scroll action. If you want to provide a custom smooth scroll logic, override smoothScrollToPosition(RecyclerView, State, int) in your LayoutManager.

Parameters
position    int: The adapter position to scroll to
See also:

smoothScrollToPosition(RecyclerView, State, int)

看完這裏我是哭了,

這裏寫圖片描述

哭了以後擦乾淚水我又去找LinerLayoutManager裏面smoothScrollToPosition(RecyclerView,State,int)方法。

    @Override
    public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state,
            int position) {
        LinearSmoothScroller linearSmoothScroller =
                new LinearSmoothScroller(recyclerView.getContext()) {
                    @Override
                    public PointF computeScrollVectorForPosition(int targetPosition) {
                        return LinearLayoutManager.this
                                .computeScrollVectorForPosition(targetPosition);
                    }
                };
        linearSmoothScroller.setTargetPosition(position);
        startSmoothScroll(linearSmoothScroller);
    }
  public void setTargetPosition(int targetPosition) {
            mTargetPosition = targetPosition;
        }

然後我查詢mTargetPosition,發現在start(RecyclerView recyclerView, LayoutManager layoutManager)、 stop()、onAnimation(int dx, int dy)中用到,我分析這個值應該只是LinerLayoutManager在移動和動畫的過程中對於位置的一個判斷,但是如何“平滑”的滑動我卻是連一個鬼影子都沒有看見。所以說這是一次學習源碼的反面教材,因爲自己最後都不明白自己需要探究的“平滑”是如何實現的?

但是我們不能就此放棄,我不會,難道其他人都不會,如果其他人都不會我更要想辦法學會了,如果其他人會的話,那我更要學習,不然差距會越來越大,,,,,,

這裏寫圖片描述

不廢話,直接上大神的乾貨:
RecyclerView smoothScrollToPosition的滾動時間,文中是這樣說的:

當RecyclerView中的數據集很大時,通過smoothScrollToPosition去滾動到一個位置,如果這個位置和當前位置相差很遠,比如說300項,你會發現整個過程很長,比如說我遇到的,滾動300項,用了3.5秒。
這主要跟RecyclerView smoothScroll的方式有關,它內部有一個常量值代表每滾動1px需要多少時間,所以滾動的距離越遠,需要的時間越長。

如何修改這個值呢?根據上方文檔說明,自定義一個LinearSmoothScroller,然後發現需要重寫protected int calculateTimeForScrolling(int dx),看這名字就知道函數的作用了,所以修改返回值,讓它最多返回小一點就行了。不得不說,這裏還有一個坑:
calculateTimeForScrolling(int dx)不是計算這一個smoothScrollToPosition需要的時間的,實際情況時,當實際需要滾動的距離大於10000時,滾動會分多次進行,比如說滾動52000距離,實際會這個函數會調用6次,dx的值前5次是10000,最後一次是2000。實際滾動時間是這6次返回值的和。
知道了這個,解決也簡單了,它想分多次調用就讓它多次調用吧,我只要每次返回的時間值很小就行了。方法有兩個。

  • 直接修改返回值,讓它足夠小
  • 修改傳入的參數,當dx足夠小時,計算出的時間自然就小了。

文中還給出了示例代碼:

mLayoutManager = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false) {
    @Override
    public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position) {
        LinearSmoothScroller linearSmoothScroller =
                new LinearSmoothScroller(recyclerView.getContext()) {
                    @Override
                    protected int calculateTimeForScrolling(int dx) {
                        // 此函數計算滾動dx的距離需要多久,當要滾動的距離很大時,比如說52000,
                        // 經測試,系統會多次調用此函數,每10000距離調一次,所以總的滾動時間
                        // 是多次調用此函數返回的時間的和,所以修改每次調用該函數時返回的時間的
                        // 大小就可以影響滾動需要的總時間,可以直接修改些函數的返回值,也可以修改
                        // dx的值,這裏暫定使用後者.
                        // (See LinearSmoothScroller.TARGET_SEEK_SCROLL_DISTANCE_PX)
                        if (dx > 3000) {
                            dx = 3000;
                        }
                        return super.calculateTimeForScrolling(dx);
                    }

                    @Override
                    public PointF computeScrollVectorForPosition(int targetPosition) {
                        return mLayoutManager.computeScrollVectorForPosition(targetPosition);
                    }
                };
        linearSmoothScroller.setTargetPosition(position);
        startSmoothScroll(linearSmoothScroller);
    }
};

我們說的是區別,現在我們知道了怎麼“平滑的滾回去”了,那麼“滾回去”應該怎麼“滾”呢?
這次我們也不再去RecycleView裏面踩坑了,直接去LinerLayoutManager裏面看看源碼:

 /**
     * <p>Scroll the RecyclerView to make the position visible.</p>
     *
     * <p>RecyclerView will scroll the minimum amount that is necessary to make the
     * target position visible. If you are looking for a similar behavior to
     * {@link android.widget.ListView#setSelection(int)} or
     * {@link android.widget.ListView#setSelectionFromTop(int, int)}, use
     * {@link #scrollToPositionWithOffset(int, int)}.</p>
     *
     * <p>Note that scroll position change will not be reflected until the next layout call.</p>
     *
     * @param position Scroll to this adapter position
     * @see #scrollToPositionWithOffset(int, int)
     */
    @Override
    public void scrollToPosition(int position) {
        mPendingScrollPosition = position;
        mPendingScrollPositionOffset = INVALID_OFFSET;
        if (mPendingSavedState != null) {
            mPendingSavedState.invalidateAnchor();
        }
        requestLayout();
    }
    @Override
    public void requestLayout() {
        if (mEatRequestLayout == 0 && !mLayoutFrozen) {
            super.requestLayout();
        } else {
            mLayoutRequestEaten = true;
        }
    }
 @CallSuper
    public void requestLayout() {
        if (mMeasureCache != null) mMeasureCache.clear();

        if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) {
            // Only trigger request-during-layout logic if this is the view requesting it,
            // not the views in its parent hierarchy
            ViewRootImpl viewRoot = getViewRootImpl();
            if (viewRoot != null && viewRoot.isInLayout()) {
                if (!viewRoot.requestLayoutDuringLayout(this)) {
                    return;
                }
            }
            mAttachInfo.mViewRequestingLayout = this;
        }

        mPrivateFlags |= PFLAG_FORCE_LAYOUT;
        mPrivateFlags |= PFLAG_INVALIDATED;

        if (mParent != null && !mParent.isLayoutRequested()) {
            mParent.requestLayout();
        }
        if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
            mAttachInfo.mViewRequestingLayout = null;
        }
    }

最後一路追蹤到View的requestLayout()方法,感覺這個方法和重新繪製佈局,有刷新的意思:

Call this when something has changed which has invalidated the layout of this view. This will schedule a layout pass of the view tree. This should not be called while the view hierarchy is currently in a layout pass ({@link #isInLayout()}. If layout is happening, the request may be honored at the end of the current layout pass (and then layout will run again) or after the current frame is drawn and the next layout occurs.


當某些更改已使此視圖的佈局無效時調用此方法。 這將安排視圖樹的佈局傳遞。 當視圖層次結構當前在佈局中時,不應調用此方法 如果佈局正在進行,請求可以在當前佈局傳遞結束時(然後佈局將再次運行)或在當前幀被繪製和下一佈局發生之後被執行。


於是我看了一下網上大神關於requestLayout的解釋,如下:

requestLayout:當view確定自身已經不再適合現有的區域時,該view本身調用這個方法要求parent view重新調用他的onMeasure onLayout來對重新設置自己位置。 特別的當view的layoutparameter發生改變,並且它的值還沒能應用到view上,這時候適合調用這個方法。

invalidate:View本身調用迫使view重畫。


對於上面的判斷可以直接通過代碼測試,Item我測試了600項,scrollToPosition方法耗時不到1毫秒就解決,但是smoothScrollToPosition方法耗時7.8秒。所以這樣可以判斷二者的區別了。一個就是慢悠悠的平滑滾動,一個是直接刷新繪製。

4. 聊天——ChatActivity界面邏輯

總的來說,ChatActivity界面邏輯還是相對來講比較簡單的!爲了描述清楚,先上圖:

這裏寫圖片描述

首先,我們將界面實例化以後需要乾的第一件事就是將好友加入到RecycleView裏面來,但是這些數據我們怎麼獲取呢?這個就由業務邏輯實現類ChatPresenterImpl來完成。接下來不要說話,看代碼:

    @Override
    public void loadMessages(final String userName) {
        ThreadUtils.runOnBackgroundThread(new Runnable() {
            @Override
            public void run() {
                EMConversation conversation = EMClient.getInstance().chatManager().getConversation(userName);
                if (conversation != null) {
                    //獲取此會話的所有消息
                    List<EMMessage> messages = conversation.getAllMessages();
                    mEMMessageList.addAll(messages);
                    //指定會話消息未讀數清零
                    conversation.markAllMessagesAsRead();
                }
                ThreadUtils.runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        mChatView.onMessagesLoaded();
                    }
                });
            }
        });
    }

再次強調線程的切換——子線程加載數據,主線程更新UI,接下來到Activity的主線程看看更新UI是做了什麼工作?

  @Override
    public void onMoreMessagesLoaded(int size) {
        toast("加載數據成功");
        mMessageListAdapter.notifyDataSetChanged();
        mRecyclerView.scrollToPosition(size);
    }

這裏比較簡單了,就是做個彈窗,然後刷新數據並將RecycleView滾動到最後一條數據的位置。

那麼我們在聊天界面發送數據怎麼辦呢?老規矩,我們支持Button發送或者軟鍵盤的Enter發送,這裏需要強調的是軟鍵盤發送處需要處理一個細節,否則程序會崩掉:

Process: com.itheima.leon.qqdemo, PID: 23729
                                                                         java.lang.NullPointerException: Attempt to invoke virtual method 'void com.hyphenate.chat.EMMessage.setStatus(com.hyphenate.chat.EMMessage$Status)' on a null object reference
                                                                             at com.itheima.leon.qqdemo.presenter.impl.ChatPresenterImpl$1.run(ChatPresenterImpl.java:41)
                                                                             at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1133)
                                                                             at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:607)
                                                                             at java.lang.Thread.run(Thread.java:761)

這個原因是客戶還沒有輸入內容的時候觸碰了軟鍵盤的Enter鍵,所以聊天內容就報空指針了。爲了更好的體驗(最起碼不能動不動就崩),我們可以在此處增加一個判斷:

 mEdit.setOnEditorActionListener(mOnEditorActionListener);
 private TextView.OnEditorActionListener mOnEditorActionListener = new TextView.OnEditorActionListener() {
        @Override
        public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
            if (actionId == EditorInfo.IME_ACTION_SEND && mEdit.getText().toString().length()>0) {
                sendMessage();
                return true;
            }
            return false;
        }
    };

舉一反三,那麼Button的發送我們是不是也要這樣判斷一下呢?其實我們有更好的處理方法:

mEdit.addTextChangedListener(mTextWatcher);
    private TextWatcherAdapter mTextWatcher = new TextWatcherAdapter() {
        @Override
        public void afterTextChanged(Editable s) {
            mSend.setEnabled(s.length() != 0);
        }
    };

效果如圖:

這裏寫圖片描述

接下來我們不廢話,繼續看發送信息的代碼是如何實現的?

  private void sendMessage() {
        mChatPresenter.sendMessage(mUserName, mEdit.getText().toString().trim());
        hideKeyBoard();
        mEdit.getText().clear();
    }

然後我們需要到ChatPresenterImpl繼續觀察發送信息需要做什麼,比如說發送失敗了呢?

    @Override
    public void sendMessage(final String userName, final String message) {
        ThreadUtils.runOnBackgroundThread(new Runnable() {
            @Override
            public void run() {
                EMMessage emMessage = EMMessage.createTxtSendMessage(message, userName);
                emMessage.setStatus(EMMessage.Status.INPROGRESS);
                emMessage.setMessageStatusCallback(mEMCallBackAdapter);
                mEMMessageList.add(emMessage);
                EMClient.getInstance().chatManager().sendMessage(emMessage);
                ThreadUtils.runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        mChatView.onStartSendMessage();
                    }
                });
            }
        });
    }

這裏大家可能會說,我只看見了正在發送的回調,那麼剛剛說的發送失敗呢,我怎麼沒有看見?
其實這裏封裝了一個接口,我們看看這個mEMCallBackAdapter是怎麼定義的?

  private EMCallBackAdapter mEMCallBackAdapter = new EMCallBackAdapter() {
        @Override
        public void onSuccess() {
            ThreadUtils.runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    mChatView.onSendMessageSuccess();
                }
            });
        }

        @Override
        public void onError(int i, String s) {
            ThreadUtils.runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    mChatView.onSendMessageFailed();
                }
            });
        }
    };

OK,接下來我們一起看看Activity主線程是怎麼實現的?

 @Override
    public void onStartSendMessage() {
       dialog.show();
    }

    private void updateList() {
        mMessageListAdapter.notifyDataSetChanged();
        smoothScrollToBottom();
    }

    @Override
    public void onSendMessageSuccess() {
        dialog.dismiss();
        hideProgress();
        toast(getString(R.string.send_success));
        updateList();
    }

    @Override
    public void onSendMessageFailed() {
        dialog.dismiss();
        hideProgress();
        toast(getString(R.string.send_failed));
    }

這裏的dialog是我們自定義的,其實也比較簡單:

    <style name="loading_dialog" parent="Animation.AppCompat.Dialog">

        <item name="android:windowFrame">@null</item>
        <item name="android:windowNoTitle">true</item>
        <item name="android:windowIsFloating">true</item>
        <item name="android:windowContentOverlay">@null</item>
    </style>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="wrap_content"
    android:id="@+id/dialog_view"
    android:layout_height="wrap_content">

    <ImageView
        android:id="@+id/img"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/send_message_progress"
        />
</LinearLayout>
    /**
     * 得到自定義的progressDialog
     * @param context
     * @return
     */
    public static Dialog createLoadingDialog(Context context) {

        LayoutInflater inflater = LayoutInflater.from(context);
        View v = inflater.inflate(R.layout.loading_dialog, null);// 得到加載view
        LinearLayout layout = (LinearLayout) v.findViewById(R.id.dialog_view);// 加載佈局
        // main.xml中的ImageView
        ImageView spaceshipImage = (ImageView) v.findViewById(R.id.img);

        AnimationDrawable animationDrawable = (AnimationDrawable) spaceshipImage.getDrawable();
        animationDrawable.start();


        Dialog loadingDialog = new Dialog(context, R.style.loading_dialog);// 創建自定義樣式dialog

        loadingDialog.setCancelable(false);// 不可以用“返回鍵”取消
        loadingDialog.setContentView(layout, new LinearLayout.LayoutParams(
                LinearLayout.LayoutParams.WRAP_CONTENT,
                LinearLayout.LayoutParams.WRAP_CONTENT));// 設置佈局
        return loadingDialog;

    }

OK,這個項目基本上學習結束了,但是不得不說這個項目現在基本上只是一個雛形,還有很多地方沒有寫好!比如現在最火的表情、語音、未讀消息計數更新等等。其實最簡單的是將發送信息的表情添加到模塊裏,然後通過接口調用幀動畫播放和關閉。但是因爲最近還有其它事情還要做,所以不打算繼續在這些細節花費時間了,感興趣的朋友可以嘗試EaseUI 庫繼續完善,我之前也寫過仿微信表情圖片,可以借鑑一下。

學習的項目地址:

github:https://github.com/Vicent9920/FanChat

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章