美文网首页Android开发Android技术知识
一起撸个朋友圈吧 (Step6)- 评论对齐(未完全版本)【上】

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

作者: Razerdp | 来源:发表于2016-03-25 13:57 被阅读1092次

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

    相关文章

      网友评论

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

        本文链接:https://www.haomeiwen.com/subject/dhtdlttx.html