一起擼個朋友圈吧 (Step6)- 評論對齊(未完全版本)【上】

項目地址:https://github.com/razerdp/FriendCircle
一起擼個朋友圈吧這是本文所處文集,所有更新都會在這個文集里面哦,歡迎關(guān)注

上篇鏈接:http://www.lxweimin.com/p/58894dfb3f09
下篇鏈接:http://www.lxweimin.com/p/513e2eccd7a8

食用注意:

  • 本餐為非完全體,僅僅實現(xiàn)針對動態(tài)評論的輸入框?qū)R功能,剩余菜式(后臺交互等)敬請期待
  • 本餐存在一定的bug(超多評論時有一定的bug),待補
  • 本餐餐牌稍難理解,我盡量寫的易懂一點。
  • 圖片較多,文字很多,流量黨請注意

預覽

開始之前,按照慣例,先弄上preview吧:


preview

什么?你說你看不出什么特別的?
那好,咱么再上一張圖:

preview2

這回錄制短了一點,比較兩張gif,不難看出兩者的區(qū)別:

  • 第一張圖在點擊評論的時候,會自動將動態(tài)的底部對齊評論框頂部
  • 第二張圖僅僅是單純的彈出輸入框,沒有任何其他操作(所以咱們錄制的時間就短了←_←)。

就用戶體驗來說,肯定是第一張圖的比較好,同時,這也是微信的做法,所以很多地方微信的細節(jié)真的抓的很好啊。


思路

OK,既然比較結(jié)果出來了,接下來就得思考一下做法了。
因為咱們不是微信的開發(fā)員,所以只能按照我的想法去做了。

首先想想listview針對item的位移操作有哪些:

  • setselection:不推薦,因為是即刻就到,沒有過渡
  • smoothscrolltoposition:可以用,但不能完全滿足我們的需求。
  • setselectionfromtop:不推薦,理由同一
  • smoothscrolltopositionfromtop:騷年,別想了,就是它了。

常用的方法和理由都寫在上面了,這里我們打算采用smoothscrolltopositionfromtop,理由很簡單:

  • 其一它有過渡的scroll效果
  • 其二,它能移到指定位置
  • 其三 ,它還有一個位移,在到達位置后進行一段位移。

OK,采用的方法也有了,接下來就是要想想怎么利用這個方法了。

smoothscrolltopositionfromtop常用的方法有兩個參數(shù),第一個是item的位置,第二個是位移。第一個很好辦,我們可以在點擊的時候?qū)⑽恢脪伋鰜恚诙€就有點難辦了,因為這個位移量并非那么好計算的。

這時候也許就會有一種難以入手的感覺了。

既然不知道從哪方面入手,咱們不妨先看看最終效果:

期望效果圖

如圖,我們點了上面那個item,此時輸入框彈了上來,但是我們的預期是希望item的底部能夠?qū)R到輸入框的頂部,很明顯,現(xiàn)在沒有達到我們的預期。

那么如果按照圖中的效果,我們需要listview自動滑動一段距離,在現(xiàn)在這張圖,我們的偏移量很好看,不就是圖中箭頭的那段距離么。

理論上的確如此,我們可以得到item的bottom,減去輸入框的top得到偏移量,然而在實際測試過程中,我們得到的位移量并不準確,當然,也有可能是我的計算有問題,這也許是一個很好的思路,但暫時來說我們先放到一邊。

回到本篇,我們不妨看一下,在輸入框彈上來之后,我們的可以見到的view的范圍,為了更加直觀,我們直接上圖:

可見范圍

如圖,在鍵盤彈上來之后,整個黃色的蒙層區(qū)域就是我們當前可見的視圖層。在圖上我們也標注了一些必要參數(shù),因此很明顯,我們的可見區(qū)域范圍計算如下:
contentHeight = ScreenHeight - StatusBarHeight - KeyBoardHeight - InputLayoutHeight

那么得到這個有什么用呢?別急,還記得我們上面說過的方法嗎?

smoothscrolltopositionfromtop,第一個參數(shù)跟setselection差不多,移動到指定的item。

我們試試調(diào)用smoothscrolltopositionfromtop(當前item的position,0),得到下圖的結(jié)果:

smoothscrolltopositionfromtop

為什么與我們想象的不一樣?Item的top不應該在titlebar的下方么?

別急。。。。

還記得我們第一篇的布局嗎,titlebar的層是在listview的上方,所以item的頂部被遮擋了。

如果我們調(diào)用smoothscrolltopositionfromtop(當前item的position,titlebar.getHeight)就會得到我們想要的結(jié)果了,為了篇幅,咱們就不上圖了。

在這兩次小小的測試調(diào)用中,我們得到了兩個信息:

  • smoothscrolltopositionfromtop可以讓listview順利的滑倒指定item
  • offset方向,offset>0時,listview等同于我們手指向下拉,否則反之

OK,我們現(xiàn)在可以讓item在可是區(qū)域的頂部了,但是底部還沒有對齊,如上圖,我在圖中用紅色虛線標明了該item的底部。

所以這時候我們的offset其實很容易計算:
offset = -1 * ( ItemHeight - contentHeight );

這樣,當item底部大于contentHeight時,listview會朝y軸負方向移動,使item底部對齊contentHeight,即inputlayout的top,否則反之。


代碼

呼呼,思路終于確定。接下來就是代碼方面了。

在上一篇的重構(gòu)中,我們的評論框調(diào)用方法是這樣的:

@Override 
public void showInputBox(int currentDynamicPos, @CommonValue.CommentType int commentType, CommentInfo commentInfo) { }

根據(jù)type來判斷當前評論是評論動態(tài)還是回復評論,但是這樣太冗余了,所以這次又將它改了一下:

@Override
public void showInputBox(int currentDynamicPos, CommentWidget commentWidget, DynamicInfo dynamicInfo){ }

我們直接把commentWidget拋出來,這樣對這個控件空引用判斷就能知道是評論動態(tài)還是回復評論了。

首先我們補全showInputBox代碼,為了節(jié)省篇幅,輸入框的xml布局就不展示了,可以到github看完整代碼:

  @Override
    public void showInputBox(int currentDynamicPos, CommentWidget commentWidget, DynamicInfo dynamicInfo) {
        this.currentDynamicPos = currentDynamicPos;
        this.mCommentWidget = commentWidget;
        if (!TextUtils.isEmpty(draftStr)) {
            mInputBox.setText(draftStr);
            mInputBox.setSelection(draftStr.length());
        }
        if (commentWidget == null) {
            // 評論動態(tài)
            mInputLayout.setVisibility(View.VISIBLE);
            InputMethodUtils.showInputMethod(mInputBox);
        }
        else {
            // 回復評論

        }
    }

在輸入框彈出來時,如果草稿不空,則將草稿設(shè)置到edittext中,否則就不設(shè)置。(其中草稿在點擊發(fā)送的時候清空,在輸入法隱藏的時候保存)

在思考那部分,我們知道contentHeight的計算方法,但問題就在于輸入法的高度獲取問題,幸好,網(wǎng)上的大神們已經(jīng)提供了方法,在谷歌一番后,我們得到了以下這個方法(方法來源:http://blog.csdn.net/daguaio_o/article/details/47127993 ):

不過這個方法有一點點小問題,因為OnGlobalLayoutListener在view改變時會被調(diào)用,所以即使輸入法隱藏了,接口依然被調(diào)用,所以我稍微改變了一下(寫到UIHelper.java里面):

 /**
     * 監(jiān)聽軟鍵盤高度和狀態(tài)
     *
     * source web link:
     * http://blog.csdn.net/daguaio_o/article/details/47127993
     */
    public static void observeSoftKeyboard(Activity activity, final OnSoftKeyboardChangeListener listener) {
        final View decorView = activity.getWindow().getDecorView();
        decorView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            int previousKeyboardHeight = -1;
            Rect rect = new Rect();
            boolean lastVisibleState = false;

            @Override
            public void onGlobalLayout() {
                rect.setEmpty();
                decorView.getWindowVisibleDisplayFrame(rect);
                int displayHeight = rect.bottom - rect.top;
                int height = decorView.getHeight();
                int keyboardHeight = height - displayHeight;
                if (previousKeyboardHeight != keyboardHeight) {
                    boolean hide = (double) displayHeight / height > 0.8;
                    if (hide!=lastVisibleState) {
                        listener.onSoftKeyBoardChange(keyboardHeight, !hide);
                        lastVisibleState=hide;
                    }
                }
                previousKeyboardHeight = height;
            }
        });
    }

首先將Rect矩形的創(chuàng)建移到回調(diào)外,防止多次創(chuàng)建,然后記錄軟鍵盤的狀態(tài),當且僅當軟鍵盤的可視性與上一次不同的時候,才會回調(diào)OnSoftKeyboardChangeListener 。

OnSoftKeyboardChangeListener 在那個博客文章上有,這里就不闡述了,接下來到我們的Activity層使用:

...import

/**
 * Created by 大燈泡 on 2016/2/25.
 * 朋友圈demo窗口
 */
public class FriendCircleDemoActivity extends FriendCircleBaseActivity
        implements DynamicView, View.OnClickListener, OnSoftKeyboardChangeListener {
...變量定義

    @Override
    protected void onCreate(Bundle savedInstanceState) {
      ...與之前一樣
        UIHelper.observeSoftKeyboard(this, this);
    }

...之前的方法不變

    //============================================================= tools method

    @Override
    public void onSoftKeyBoardChange(int softKeybardHeight, boolean visible) {
        Log.d("keyboardheight", "" + softKeybardHeight + "         visible=     " + visible);
        // 保存軟鍵盤高度
        if ((int) PreferenceUtils.INSTANCE.getSharedPreferenceData("KeyBoardHeight", 0) < softKeybardHeight) {
            PreferenceUtils.INSTANCE.setSharedPreferenceData("KeyBoardHeight", softKeybardHeight);
        }
    }
}

在onSoftKeyBoardChange我們實現(xiàn)listview的偏移。因為我們對代碼實現(xiàn)過一些改變,所以我們可以確保這個回調(diào)只會在軟鍵盤可視性改變時才會調(diào)用,所以不擔心死循環(huán)問題。

接下來寫出我們計算偏移量的方法:

    private int screenHeight = 0;
    private int statusBarHeight = 0;

    private int calculateListViewOffset(int currentDynamicPos, CommentWidget commentWidget, int keyBoardHeight) {
        int result = 0;
        if (screenHeight == 0) screenHeight = UIHelper.getScreenPixHeight(this);
        if (statusBarHeight == 0) statusBarHeight = UIHelper.getStatusHeight(this);

        if (commentWidget == null) {
            // 評論控件為空,證明回復的是整個動態(tài)
            result = getOffsetOfDynamic(currentDynamicPos, keyBoardHeight);
        }
        else {
            // 評論控件不空,證明回復的是評論
        }
        return result;
    }

screenHeight 和statusBarHeight我們設(shè)置為本類全局變量,這樣就不用每次都消耗系統(tǒng)資源。然后針對commentWidget 是否為空再分別計算。

接下來是最重要的部分getOffsetOfDynamic:

// 得到動態(tài)高度
    private int getOffsetOfDynamic(int currentDynamicPos, int keyBoardHeight) {
        int result = 0;
        ListView contentListView = null;
        if (mListView.getContentView() instanceof ListView) {
            contentListView = (ListView) mListView.getContentView();
        }

        if (contentListView == null) return 0;

        int firstItemPos = contentListView.getFirstVisiblePosition();
        int dynamicItemHeight = 0;
        View currentDynamicItem = contentListView.getChildAt(
                currentDynamicPos - firstItemPos + contentListView.getHeaderViewsCount());
        if (currentDynamicItem != null) {
            dynamicItemHeight = currentDynamicItem.getHeight();
            Log.d("dynamicItemHeight", "dynamicItemHeight=========    " + dynamicItemHeight);
        }
        int contentHeight = 0;
        contentHeight = screenHeight - keyBoardHeight - mInputLayout.getHeight();
        result = dynamicItemHeight - contentHeight;
        return -result;
    }

這部分代碼我基本沒怎么寫注釋,因為我打算在文章里面記錄,所以就沒怎么寫注釋了。

不過應該不難理解。

首先,因為我們使用百萬哥的ultr下拉刷新控件,并且再度封裝,所以我們的真正的listview其實是ptrFrameLayout的contentView,因此我們需要得到listview。

接下來需要得到當前位置的item,得到item的view有兩個方法:

  • adapter.getView:
    • 沒錯,這個就是我們寫adapter時重載的getView方法,經(jīng)常寫adapter的我們都知道,三個參數(shù)里面我們知道的有position和parent(即listview),但convertView不知道,所以傳入null,此時adapter會因為我們的重載會重新inflate出來,所以我們通過這個方法得到的convertView需要手動調(diào)用measure進行測量,否則是不會有屬性信息的。
  • listview.getChildAt:
    • 因為listview可以算是一個viewgroup,所以可以直接得到對應的子view,不過需要留意的是,因為listview的復用機制,我們不可以直接傳入position,而是需要得到listview頂部展示的view的position,然后用真正的itemPosition減去第一個可見的,如果有headerView則加上headerView的數(shù)量,這樣才能正確得到指定item,并且不需要重新測量。

得到了item后,我們就可以得到其高度。

最后只是套用上面我們思路的兩條公式(ps:本例并沒有減去statusBarHeight,因為我發(fā)現(xiàn)查到的博客地址里面包含有,當輸入法不可見時,就會有50這個高度,這個高度就是statusBarHeight高度,這也是為什么在寫入sharePreference時會判斷鍵盤高度的原因)

得到偏移量,我們就可以在keyboard變化的回調(diào)中操作了

 @Override
    public void onSoftKeyBoardChange(int softKeybardHeight, boolean visible) {
        Log.d("keyboardheight", "" + softKeybardHeight + "         visible=     " + visible);
        // 保存軟鍵盤高度
        if ((int) PreferenceUtils.INSTANCE.getSharedPreferenceData("KeyBoardHeight", 0) < softKeybardHeight) {
            PreferenceUtils.INSTANCE.setSharedPreferenceData("KeyBoardHeight", softKeybardHeight);
        }

        // listview偏移
        final int offset = calculateListViewOffset(currentDynamicPos, mCommentWidget, softKeybardHeight);
        Log.d("offset", "offset===========    " + offset);
        // http://stackoverflow.com/questions/11431832/android-smoothscrolltoposition-not-working-correctly
        final int pos = currentDynamicPos + 1;
        mListView.smoothScrollToPositionFromTop(pos, offset);
    }

因為我們的公式是針對可視范圍,所以當keyboard隱藏的時候依然會觸發(fā)這個回調(diào),因此會重新計算一次,所以我們在隱藏的時候,item依然會自動對齊到輸入框的頂部。
(值得留意的是,我們的朋友圈headerview只有一個,所以我們的position要+1哦,這里可以改成加上listview.getHeaderViewCount())


Finally

最后,我們需要補充一下在軟鍵盤可見時,如果點擊了listview,則需要消掉鍵盤并保存草稿。

做法很簡單,我們只需要在listview的onTouch回調(diào)做手腳,但問題在于,百萬哥的ptrFrameLayout的事件分發(fā)是在dispatchTouchEvent里面實現(xiàn)的,這就導致了我們即使setOnTouchListener也會被截斷。

所以我們需要重寫一下,在調(diào)用框架的dispatchTouchEvent前實現(xiàn):

來到FriendCirclePtrListView,重載dispatchTouchEvent:

 @Override
    public boolean dispatchTouchEvent(MotionEvent e) {
        if (mDispatchTouchEventListener!=null)mDispatchTouchEventListener.OnDispatchTouchEvent(e);
        return super.dispatchTouchEvent(e);
    }

其中OnDispatchTouchEventListener:

 public interface OnDispatchTouchEventListener{
        boolean OnDispatchTouchEvent(MotionEvent ev);
    }

最后在activity調(diào)用:

 mListView.setOnDispatchTouchEventListener(new FriendCirclePtrListView.OnDispatchTouchEventListener() {
            @Override
            public boolean OnDispatchTouchEvent(MotionEvent ev) {
                if (mInputLayout.getVisibility() == View.VISIBLE) {
                    draftStr = mInputBox.getText().toString().trim();
                    mInputLayout.setVisibility(View.GONE);
                    InputMethodUtils.hideInputMethod(mInputBox);
                    return true;
                }
                return false;
            }
        });

目前已知的bug:

  • 評論數(shù)過多時,無法每次都正確對齊(如例子中的第二條朋友圈,50條評論)
  • 有時候如果上一個item并沒有完全滑出屏幕外,點下一個item時會導致跳到上一個item的底部(原因在于position是在getView中傳出去的,這部分下一篇進行下修改)

【END】

下一篇將會完成剩余的評論功能

ps:文字很多,寫的或許還不是很清晰,估計看完的人不多(話說,會有人看么。。。),看完了懂的人更不多。。。。如果有不明白的,可以評論區(qū)留下您的腳印或者簡信在下。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內(nèi)容