项目地址:https://github.com/razerdp/FriendCircle
一起撸个朋友圈吧这是本文所处文集,所有更新都会在这个文集里面哦,欢迎关注
上篇链接:http://www.jianshu.com/p/58894dfb3f09
下篇链接:http://www.jianshu.com/p/513e2eccd7a8
食用注意:
- 本餐为非完全体,仅仅实现针对动态评论的输入框对齐功能,剩余菜式(后台交互等)敬请期待
- 本餐存在一定的bug(超多评论时有一定的bug),待补
- 本餐餐牌稍难理解,我尽量写的易懂一点。
- 图片较多,文字很多,流量党请注意
预览
开始之前,按照惯例,先弄上preview吧:
什么?你说你看不出什么特别的?
那好,咱么再上一张图:
这回录制短了一点,比较两张gif,不难看出两者的区别:
- 第一张图在点击评论的时候,会自动将动态的底部对齐评论框顶部
- 第二张图仅仅是单纯的弹出输入框,没有任何其他操作(所以咱们录制的时间就短了←_←)。
就用户体验来说,肯定是第一张图的比较好,同时,这也是微信的做法,所以很多地方微信的细节真的抓的很好啊。
思路
OK,既然比较结果出来了,接下来就得思考一下做法了。
因为咱们不是微信的开发员,所以只能按照我的想法去做了。
首先想想listview针对item的位移操作有哪些:
- setselection:不推荐,因为是即刻就到,没有过渡
- smoothscrolltoposition:可以用,但不能完全满足我们的需求。
- setselectionfromtop:不推荐,理由同一
- smoothscrolltopositionfromtop:骚年,别想了,就是它了。
常用的方法和理由都写在上面了,这里我们打算采用smoothscrolltopositionfromtop,理由很简单:
- 其一它有过渡的scroll效果
- 其二,它能移到指定位置
- 其三 ,它还有一个位移,在到达位置后进行一段位移。
OK,采用的方法也有了,接下来就是要想想怎么利用这个方法了。
smoothscrolltopositionfromtop常用的方法有两个参数,第一个是item的位置,第二个是位移。第一个很好办,我们可以在点击的时候将位置抛出来,但第二个就有点难办了,因为这个位移量并非那么好计算的。
这时候也许就会有一种难以入手的感觉了。
既然不知道从哪方面入手,咱们不妨先看看最终效果:
如图,我们点了上面那个item,此时输入框弹了上来,但是我们的预期是希望item的底部能够对齐到输入框的顶部,很明显,现在没有达到我们的预期。
那么如果按照图中的效果,我们需要listview自动滑动一段距离,在现在这张图,我们的偏移量很好看,不就是图中箭头的那段距离么。
理论上的确如此,我们可以得到item的bottom,减去输入框的top得到偏移量,然而在实际测试过程中,我们得到的位移量并不准确,当然,也有可能是我的计算有问题,这也许是一个很好的思路,但暂时来说我们先放到一边。
回到本篇,我们不妨看一下,在输入框弹上来之后,我们的可以见到的view的范围,为了更加直观,我们直接上图:
如图,在键盘弹上来之后,整个黄色的蒙层区域就是我们当前可见的视图层。在图上我们也标注了一些必要参数,因此很明显,我们的可见区域范围计算如下:
contentHeight = ScreenHeight - StatusBarHeight - KeyBoardHeight - InputLayoutHeight
那么得到这个有什么用呢?别急,还记得我们上面说过的方法吗?
smoothscrolltopositionfromtop,第一个参数跟setselection差不多,移动到指定的item。
我们试试调用smoothscrolltopositionfromtop(当前item的position,0),得到下图的结果:
为什么与我们想象的不一样?Item的top不应该在titlebar的下方么?
别急。。。。
还记得我们第一篇的布局吗,titlebar的层是在listview的上方,所以item的顶部被遮挡了。
如果我们调用smoothscrolltopositionfromtop(当前item的position,titlebar.getHeight)就会得到我们想要的结果了,为了篇幅,咱们就不上图了。
在这两次小小的测试调用中,我们得到了两个信息:
- smoothscrolltopositionfromtop可以让listview顺利的滑倒指定item
- offset方向,offset>0时,listview等同于我们手指向下拉,否则反之
OK,我们现在可以让item在可是区域的顶部了,但是底部还没有对齐,如上图,我在图中用红色虚线标明了该item的底部。
所以这时候我们的offset其实很容易计算:
offset = -1 * ( ItemHeight - contentHeight );
这样,当item底部大于contentHeight时,listview会朝y轴负方向移动,使item底部对齐contentHeight,即inputlayout的top,否则反之。
代码
呼呼,思路终于确定。接下来就是代码方面了。
在上一篇的重构中,我们的评论框调用方法是这样的:
@Override
public void showInputBox(int currentDynamicPos, @CommonValue.CommentType int commentType, CommentInfo commentInfo) { }
根据type来判断当前评论是评论动态还是回复评论,但是这样太冗余了,所以这次又将它改了一下:
@Override
public void showInputBox(int currentDynamicPos, CommentWidget commentWidget, DynamicInfo dynamicInfo){ }
我们直接把commentWidget抛出来,这样对这个控件空引用判断就能知道是评论动态还是回复评论了。
首先我们补全showInputBox代码,为了节省篇幅,输入框的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) {
// 评论动态
mInputLayout.setVisibility(View.VISIBLE);
InputMethodUtils.showInputMethod(mInputBox);
}
else {
// 回复评论
}
}
在输入框弹出来时,如果草稿不空,则将草稿设置到edittext中,否则就不设置。(其中草稿在点击发送的时候清空,在输入法隐藏的时候保存)
在思考那部分,我们知道contentHeight的计算方法,但问题就在于输入法的高度获取问题,幸好,网上的大神们已经提供了方法,在谷歌一番后,我们得到了以下这个方法(方法来源:http://blog.csdn.net/daguaio_o/article/details/47127993 ):
不过这个方法有一点点小问题,因为OnGlobalLayoutListener在view改变时会被调用,所以即使输入法隐藏了,接口依然被调用,所以我稍微改变了一下(写到UIHelper.java里面):
/**
* 监听软键盘高度和状态
*
* 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矩形的创建移到回调外,防止多次创建,然后记录软键盘的状态,当且仅当软键盘的可视性与上一次不同的时候,才会回调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我们实现listview的偏移。因为我们对代码实现过一些改变,所以我们可以确保这个回调只会在软键盘可视性改变时才会调用,所以不担心死循环问题。
接下来写出我们计算偏移量的方法:
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) {
// 评论控件为空,证明回复的是整个动态
result = getOffsetOfDynamic(currentDynamicPos, keyBoardHeight);
}
else {
// 评论控件不空,证明回复的是评论
}
return result;
}
screenHeight 和statusBarHeight我们设置为本类全局变量,这样就不用每次都消耗系统资源。然后针对commentWidget 是否为空再分别计算。
接下来是最重要的部分getOffsetOfDynamic:
// 得到动态高度
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方法,经常写adapter的我们都知道,三个参数里面我们知道的有position和parent(即listview),但convertView不知道,所以传入null,此时adapter会因为我们的重载会重新inflate出来,所以我们通过这个方法得到的convertView需要手动调用measure进行测量,否则是不会有属性信息的。
- listview.getChildAt:
- 因为listview可以算是一个viewgroup,所以可以直接得到对应的子view,不过需要留意的是,因为listview的复用机制,我们不可以直接传入position,而是需要得到listview顶部展示的view的position,然后用真正的itemPosition减去第一个可见的,如果有headerView则加上headerView的数量,这样才能正确得到指定item,并且不需要重新测量。
得到了item后,我们就可以得到其高度。
最后只是套用上面我们思路的两条公式(ps:本例并没有减去statusBarHeight,因为我发现查到的博客地址里面包含有,当输入法不可见时,就会有50这个高度,这个高度就是statusBarHeight高度,这也是为什么在写入sharePreference时会判断键盘高度的原因)
得到偏移量,我们就可以在keyboard变化的回调中操作了
@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隐藏的时候依然会触发这个回调,因此会重新计算一次,所以我们在隐藏的时候,item依然会自动对齐到输入框的顶部。
(值得留意的是,我们的朋友圈headerview只有一个,所以我们的position要+1哦,这里可以改成加上listview.getHeaderViewCount())
Finally
最后,我们需要补充一下在软键盘可见时,如果点击了listview,则需要消掉键盘并保存草稿。
做法很简单,我们只需要在listview的onTouch回调做手脚,但问题在于,百万哥的ptrFrameLayout的事件分发是在dispatchTouchEvent里面实现的,这就导致了我们即使setOnTouchListener也会被截断。
所以我们需要重写一下,在调用框架的dispatchTouchEvent前实现:
来到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调用:
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:
- 评论数过多时,无法每次都正确对齐(如例子中的第二条朋友圈,50条评论)
- 有时候如果上一个item并没有完全滑出屏幕外,点下一个item时会导致跳到上一个item的底部(原因在于position是在getView中传出去的,这部分下一篇进行下修改)
【END】
下一篇将会完成剩余的评论功能
ps:文字很多,写的或许还不是很清晰,估计看完的人不多(话说,会有人看么。。。),看完了懂的人更不多。。。。如果有不明白的,可以评论区留下您的脚印或者简信在下。