专业IM即时通讯软件开发,值得信赖!

Android手把手朋友圈实战教程(二十二)重构-RecyclerView的上下拉以及logo的联动

即时通讯软件开发 云聊IM 587℃

项目地址:https://github.com/razerdp/FriendCircle/tree/main-dev

这次重构因为将会从ListView换成RecyclerView,所以很多东西都要重新部署,比如上下拉。

因为朋友圈的特殊性,我们的上下拉需要符合至少两个条件:

下拉刷新可以获取到偏移量(用来联动logo)
下拉刷新时,可以隐藏刷新头部,而只展示我们的logo动画
对于懒惰的我来说,首当其冲还是找库吧。。。。结果找了一下,瞬间想哭了,因为要同时符合上面两个条件的,似乎还真的找不到。。。有一两个比较接近的(比如:IRecyclerView)却因为各种问题导致不能使用。。。

没办法,只好强撸了。。

提交部分截图

写着写着,想到了还得做动画,还得做返回,还得做各种各样的事件分发。。。。天呐噜,我还是乖乖去上班吧。。。

这时候忽然灵机一闪,想起以前撸ListView时不是有个overScroll的吗,那Rv也应会有的,于是面向谷歌编程的我,虽然找不到比较好的描述,但找到了这么一个库:

overscroll-decor

初步看了一下代码,其核心相当于接管了touch事件,通过setTranslationY来进行View的移动的,而且最重要的是,提供的接口有着状态和偏移量的返回!!!!(拍黑板,这是重点!)

有了这两个东西,那就可以嘿嘿嘿了。

控件的布局

首先,我们确定一下我们的控件应该怎么写。

在微信朋友圈中,以我们的目测,至少有三个要求(本项目以iOS的交互为标准):

谜一样的截图

  1. (1) logo要随着下拉的动作同时下拉
  2. (2) RecyclerView拉下来之后,要露出后面的背景
  3. (3) 咱们的logo是跟RecyclerView同级的

所以,咱们的布局肯定不能继承RecyclerView然后干,而是一个ViewGroup,这次我选择了FrameLayout。

所以咱们的初始化这么写:

//构造器什么的,忽略啦~都指向于这里

private void init(Context context) {
    //渐变背景(黑色的背景在上半部分,下半部分是白色的)
    GradientDrawable background = new GradientDrawable(GradientDrawable.Orientation.TOP_BOTTOM, new int[]{0xff323232, 0xff323232, 0xffffffff, 0xffffffff});
    setBackground(background);
    //rv初始化
    if (recyclerView == null) {
        recyclerView = new RecyclerView(context);
        recyclerView.setBackgroundColor(Color.WHITE);
        recyclerView.setLayoutManager(new LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false));
    }
    //logo初始化
    if (refreshIcon == null) {
        refreshIcon = new ImageView(context);
        refreshIcon.setBackgroundColor(Color.TRANSPARENT);
        refreshIcon.setImageResource(R.drawable.rotate_icon);
    }
    FrameLayout.LayoutParams iconParam = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
    iconParam.leftMargin = UIHelper.dipToPx(12);

    //add
    addView(recyclerView, RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.MATCH_PARENT);
    addView(refreshIcon, iconParam);

    //触发刷新的警戒线
    refreshPosition = UIHelper.dipToPx(90);

    //logo的观察类
    iconObserver = new InnerRefreshIconObserver(refreshIcon, refreshPosition);
}

接下来就是咱们的下拉刷新了。前面说过,咱么用的是overscroll那个库,我们针对的是偏移量,所以我们所有的工作都依赖于这个偏移:

private void initOverScroll() {
    IOverScrollDecor decor = new VerticalOverScrollBounceEffectDecorator(new RecyclerViewOverScrollDecorAdapter(recyclerView), 2f, 1f, 2f);
    decor.setOverScrollUpdateListener(new IOverScrollUpdateListener() {
        @Override
        public void onOverScrollUpdate(IOverScrollDecor decor, int state, float offset) {
            if (offset > 0) {
                //正在刷新就不鸟它
                if (currentStatus == REFRESHING) return;
                //更新logo的位置
                iconObserver.catchPullEvent(offset);
                if (offset >= refreshPosition && state == STATE_BOUNCE_BACK) {
                    //state变成返回时,意味着已经松手了,则进行刷新逻辑
                    if (currentStatus != REFRESHING) {
                        setCurrentStatus(REFRESHING);
                        if (onRefreshListener != null) {
                            Log.i(TAG, "refresh");
                            onRefreshListener.onRefresh();
                        }
                        iconObserver.catchRefreshEvent();
                    }
                }
            } else if (offset < 0) {
                //底部的overscroll
            }
        }
    });
}

代码不多,因为多的东西都在库里面干完了。。。

在调用了setAdapter之后,我们执行这个初始化方法,从回调的接口处,不难看到offset的回调有两种,分别是大于0和小于0,其中大于0是从顶部下拉(下拉刷新),而小于0则是从底部上拉(上拉加载)。

但是,有一个问题是,我们没有办法知道松手的触发,也就是相当于touch的up事件。不过幸好,接口同时还返回了状态,当状态发生改变的时候,就肯定是手势发生了变化,通过状态,我们就相当于捕捉到了up事件。所以就有了以上的代码。

因为朋友圈并不需要上拉加载,而是滑动到底部自动加载更多,所以这offset<0的地方我就没有做任何逻辑了,如果有需求的话,也是可以做到上拉加载更多的。

做完上下拉的逻辑之后,接下来就是logo的联动。

从代码上来看,我把所有的逻辑都封到了iconObserver里面了(其实我觉得起名叫iconHelper可能更好,但就是觉得Observer高大上一点←_←)。

在observer里面,我们主要做的东西都是跟UI有关的。代码比较简单,所有就把解释写到代码里面了

/**
 * 刷新Icon的动作观察者
 */

private static class InnerRefreshIconObserver {
    private ImageView refreshIcon;
    private final int refreshPosition;
    private float lastOffset = 0.0f;
    private RotateAnimation rotateAnimation;
    private ValueAnimator mValueAnimator;

    public InnerRefreshIconObserver(ImageView refreshIcon, int refreshPosition) {
        this.refreshIcon = refreshIcon;
        this.refreshPosition = refreshPosition;

        rotateAnimation = new RotateAnimation(0, 360, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
        rotateAnimation.setDuration(600);
        rotateAnimation.setInterpolator(new LinearInterpolator());
        rotateAnimation.setRepeatCount(Animation.INFINITE);

    }

    public void catchPullEvent(float offset) {
        if (checkHacIcon()) {
            refreshIcon.setRotation(offset * 2);
            if (offset >= refreshPosition) {
                offset = refreshPosition;
            }
            int resultOffset = (int) (offset - lastOffset);
            refreshIcon.offsetTopAndBottom(resultOffset);
            Log.d(TAG, "pull  >>  " + offset + "  resultOffset   >>>   " + resultOffset);
            adjustRefreshIconPosition();
            lastOffset = offset;
        }

    }

    /**
     * 调整icon的位置界限
     */
    private void adjustRefreshIconPosition() {
        if (refreshIcon.getTop() < 0) { refreshIcon.offsetTopAndBottom(Math.abs(refreshIcon.getTop())); } else if (refreshIcon.getTop() > refreshPosition) {
            refreshIcon.offsetTopAndBottom(-(refreshIcon.getTop() - refreshPosition));
        }
    }

    public void catchRefreshEvent() {
        if (checkHacIcon()) {
            refreshIcon.clearAnimation();
            refreshIcon.startAnimation(rotateAnimation);
        }
    }

    public void catchResetEvent() {
        refreshIcon.clearAnimation();
        if (mValueAnimator == null) {
            mValueAnimator = ValueAnimator.ofFloat(refreshPosition, 0);
            mValueAnimator.setInterpolator(new DecelerateInterpolator());
            mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    float result = (float) animation.getAnimatedValue();
                    catchPullEvent(result);
                }
            });
            mValueAnimator.setDuration(300);
        }
        mValueAnimator.start();
    }

    private boolean checkHacIcon() {
        return refreshIcon != null;
    }
}

最后是demo动图:

demo

喜欢 (0)
仿微信聊天软件开发
点击这里给我发消息