Android 仿IOS拖拽回弹之进阶ReboundFrameLayout

前言

  IOS拖拽回弹给用户的体验不得不赞。然后Android原生的API各种不支持,于是乎出现的很多仿IOS拖拽回弹的Android控件ReboundScrollView,比起原生的ScrollView,体验上好了很多,但是还是不尽人意:RebounScrollView里的内容超出屏幕时,你想一次性在显示完ScrollView里的内容紧跟着拽出屏幕,你发现你做不到,臣妾也做不到!于是乎,我想我是不是可以试试?下面分享一个让你也可以有IOS拖拽回弹般体验的ReboundFrameLayout。既然Android原生的ScrollView做不到,那我干脆舍弃不用,我们自己做类似ScrollView的滑动,这样方便我们在滑动到顶部(或底部)时可以继续向下(或向上滑动)。

这里链接两个大神写的拖拽回弹控件:
1、ReboundScrollView 仿IOS 拖拽回弹
2、 Android上实现仿IOS弹性ScrollView

我的思路

《Android 仿IOS拖拽回弹之进阶ReboundFrameLayout》 思路图

截屏效果

《Android 仿IOS拖拽回弹之进阶ReboundFrameLayout》

立即体验

扫描以下二维码下载体验App(从0.2.3版本开始,体验App内嵌版本更新检测功能):

《Android 仿IOS拖拽回弹之进阶ReboundFrameLayout》

JSCKit库传送门:https://github.com/JustinRoom/JSCKit

源码简析

利用OverScroller方式实现滚动。
ReboundFrameLayout.java关键逻辑代码(请参阅代码里的详细注释):

  • 重写 onTouchEvent(MotionEvent event)方法:
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (getChildCount() == 0)
            return super.onTouchEvent(event);

//        trackerEvent(event);
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mLastTouchY = event.getY();
                break;
            case MotionEvent.ACTION_MOVE:
                float curTouchY = event.getY();
                float dy = mLastTouchY - curTouchY;
                int deltaY = (int) (dy);
                mLastTouchY = curTouchY;
                //执行滑动处理
                executeScroll(getChildAt(0), deltaY);
                break;

            case MotionEvent.ACTION_UP:
                //计算滑动速度
//                int initialVelocity = getYVelocity();
                //释放滑动事件跟踪器
//                recycleVelocityTracker();
//                if (Math.abs(initialVelocity) > mMinimumVelocity) {
//                    mOverScroller.fling(0, getScrollY(), 0, - initialVelocity, 0, 0, -getHeight() / 2, getHeight() / 2);
//                    invalidate();
//                } else {
//                    rebound();
//                }

                rebound();
                break;
            default:
                break;
        }
        return true;

    }
  • 滑动处理:
    /**
     * 滑动处理
     * <p>
     * * @param target target
     *
     * @param deltaY deltaY
     */
    private void executeScroll(View target, int deltaY) {
        //为了方便下面的描述,我们用content代表target的内容,即:content=target的内容。
        //content下滑的高度
        int verticalOffset = target.getScrollY();

        int scrollDistance = 0;
        int scrollY = -getScrollY();
        if (deltaY < 0) {//向下滑动
            //target底部已经向上离开ReboundFrameLayout的底部
            if (scrollY < 0) {
                scrollDistance = Math.max(deltaY, scrollY);
                scrollBy(0, (int) (scrollDistance * scrollRatio));
                deltaY = deltaY - scrollDistance;
                if (deltaY >= 0)
                    return;
            }

            scrollDistance = Math.max(deltaY, -verticalOffset);
            target.scrollBy(0, scrollDistance);
            deltaY = deltaY - scrollDistance;
            if (deltaY >= 0)
                return;

            scrollBy(0, (int) (deltaY * scrollRatio));
        } else if (deltaY > 0) {//向上滑动
            //content在target中的可见区域
            Rect rect = new Rect();
            target.getLocalVisibleRect(rect);
            //content的实际高度
            int realHeight = target.getMeasuredHeight();
            //content底部与target底部对齐时需要向上滑动的距离。
            // 如果target底部已向上经拉出屏幕外,则认为distanceFromBottom为0
            int distanceFromBottom = realHeight - rect.bottom;
            distanceFromBottom = Math.max(distanceFromBottom, 0);

            //target顶部已经向下离开ReboundFrameLayout的顶部
            if (scrollY > 0) {
                scrollDistance = Math.min(deltaY, scrollY);
                scrollBy(0, (int) (scrollDistance * scrollRatio));
                deltaY = deltaY - scrollDistance;
                if (deltaY <= 0)
                    return;
            }

            scrollDistance = Math.min(deltaY, distanceFromBottom);
            target.scrollBy(0, scrollDistance);
            deltaY = deltaY - scrollDistance;
            if (deltaY <= 0)
                return;

            scrollBy(0, (int) (deltaY * scrollRatio));
        }
    }
  • 执行反弹:
    /**
     * 执行反弹
     */
    private void rebound() {
        if (getScrollY() == 0)
            return;

        mOverScroller.startScroll(0, getScrollY(), 0, -getScrollY(), calculateDurationByScrollY());
        invalidate();
    }

我们来看看下面几处重要的地方:

int realHeight = target.getMeasuredHeight();

  • 关于获取target的内容高度问题。
    1、如果target是从layout布局文件中解释出来的,我们可以通过target.measure(0, 0)测量出来
    2、如果targetnew出来的,通过target.measure(0, 0)测量不出来的。
    为了考虑到上面两种情况,所以我们在protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)方法中预先测量target
  • 在测量之前,我们先看看ViewGroup的解析流程,根据测试结果,流程图如下:
    《Android 仿IOS拖拽回弹之进阶ReboundFrameLayout》 ViewGroup解析流程图.jpg
    根据上图,不管是从layout布局文件中解释出来的还是new出来的,最终都会走测量方法onMeasure(int widthMeasureSpec, int heightMeasureSpec),那么接下来我们就在这里测量好target就ok了
  • 测量
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //先调用super方法固定本身的高度
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //测量target的实际内容高度
        if (getChildCount() > 0) {
            View target = getChildAt(0);
            ViewGroup.LayoutParams params = target.getLayoutParams();
            int newHeightMeasureSpec = MeasureSpec.makeMeasureSpec(params.height, MeasureSpec.AT_MOST);
            target.measure(widthMeasureSpec, newHeightMeasureSpec);
        }
    }

公共方法

《Android 仿IOS拖拽回弹之进阶ReboundFrameLayout》

结尾

  不求打赏,点个赞加个关注吧!我是JustinRoom,QQ:1006368252

    原文作者:SwitchLife
    原文地址: https://www.jianshu.com/p/53d13719a6c4
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞