一起撸个朋友圈吧 (Step6) 评论对齐(未完全版本)【上】

283 阅读10分钟

项目地址:https://github.com/razerdp/FriendCircle

上篇链接:http://www.jianshu.com/p/58894dfb3f09

下篇链接:http://www.jianshu.com/p/513e2eccd7a8

食用注意:

  • 本餐为非完全体,仅仅实现针对动态评论的输入框对齐功能,剩余菜式(后台交互等)敬请期待
  • 本餐存在一定的bug(超多评论时有一定的bug),待补
  • 本餐餐牌稍难理解,我尽量写的易懂一点。
  • 图片较多,文字很多,流量党请注意

预览

开始之前,按照惯例,先弄上preview吧:

preview

什么?你说你看不出什么特别的? 那好,咱么再上一张图:

preview2

这回录制短了一点,比较两张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),得到下图的结果:

smoothscrolltopositionfromtop

为什么与我们想象的不一样?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:文字很多,写的或许还不是很清晰,估计看完的人不多(话说,会有人看么。。。),看完了懂的人更不多。。。。如果有不明白的,可以评论区留下您的脚印或者简信在下。