項目地址: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吧:
什么?你說你看不出什么特別的?
那好,咱么再上一張圖:
這回錄制短了一點,比較兩張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é)果:
為什么與我們想象的不一樣?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ū)留下您的腳印或者簡信在下。