仿QQ控件下拉放大阻尼效果的ScrollView

先放效果图,可能跟QQ原有的实现有点差别,只能说类似
在这里插入图片描述
完成这个效果主要用到的就是ScrollViewoverScrollBy 这个方法主要是当你的滑到边界了然后继续向下滑动的时候触发。

    /**
     * Scroll the view with standard behavior for scrolling beyond the normal
     * content boundaries. Views that call this method should override
     * {@link #onOverScrolled(int, int, boolean, boolean)} to respond to the
     * results of an over-scroll operation.
     *
     * Views can use this method to handle any touch or fling-based scrolling.
     *
     * @param deltaX Change in X in pixels
     * @param deltaY Change in Y in pixels
     * @param scrollX Current X scroll value in pixels before applying deltaX
     * @param scrollY Current Y scroll value in pixels before applying deltaY
     * @param scrollRangeX Maximum content scroll range along the X axis
     * @param scrollRangeY Maximum content scroll range along the Y axis
     * @param maxOverScrollX Number of pixels to overscroll by in either direction
     *          along the X axis.
     * @param maxOverScrollY Number of pixels to overscroll by in either direction
     *          along the Y axis.
     * @param isTouchEvent true if this scroll operation is the result of a touch event.
     * @return true if scrolling was clamped to an over-scroll boundary along either
     *          axis, false otherwise.
     */
    @Override
    protected boolean overScrollBy(int deltaX, int deltaY,
                                   int scrollX, int scrollY,
                                   int scrollRangeX, int scrollRangeY,
                                   int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent) {

这里我们主要注意deltaY 变化就行,代表的是Y轴的上的变化值。 关于onOverScrolled 注释说需要重写来操作over-scroll 。 关注源代码可以知道 onOverScrolled 需要控制scrollRangeY 以及maxOverScrollY 的值才能去操作,比较麻烦。这里我们直接得到 deltaY操作就行。 如果为负代表向下滑动。

开始写代码之前我们先分析布局,解释下我的代码逻辑。

<com.example.behaviordemo.qq.QQOverScrollView
    android:id="@+id/qq_scroll_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">


        <ImageView
            android:id="@+id/zoom_view"
            android:layout_width="match_parent"
            android:layout_height="@dimen/qq_header_height"
            android:scaleType="centerCrop"
            android:src="@mipmap/timg" />
        <LinearLayout
            android:id="@+id/content_layout"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical">

            <LinearLayout
                android:layout_width="wrap_content"
                android:paddingLeft="30dp"
               android:layout_height="@dimen/qq_header_content_height">
                <de.hdodenhof.circleimageview.CircleImageView
                    android:id="@+id/profile_image"
                    android:layout_width="86dp"
                    android:layout_height="86dp"
                    android:layout_gravity="center_vertical"
                    android:layout_alignParentBottom="true"
                    android:src="@mipmap/lufei"
                    app:civ_border_width="2dp"
                    app:civ_border_color="#ffffff"/>
            </LinearLayout>
            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="300dp"
                android:background="@color/aqua" />
            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="300dp"
                android:background="@color/yellow" />
            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="300dp"
                android:background="@color/blue" />
        </LinearLayout>
    </FrameLayout>
</com.example.behaviordemo.qq.QQOverScrollView>

如下图
在这里插入图片描述
左边是布局效果,右边是界面加载到模拟器效果, 可以看出我在初始化的时候,
对下面的内容部分坐了一些margin偏移.在这里插入图片描述
在这里插入图片描述
如上图的说明,头像布局是在content_layout的布局里面,这就是为了固定头像布局让其跟随一齐滑动。这样我就可以专心处理后面的zoom_view 放大以及content_layout 布局的下滑移动问题。
在这里插入图片描述zoom_view的初始大小是300dp, 下滑过程中需要增大100dp, 高度400dp
content_layout 最初位置是在margin_top100dp的位置,由于它始终要在我们看到头像的上面100dp位置,最终他的margin值要是200dp。
所以我定义了下面几个常量值,并且在init初始化的时候获取到

<dimen name="qq_header_height">300dp</dimen>
<dimen name="qq_header_max_height">400dp</dimen>
<dimen name="qq_content_margin_top">100dp</dimen>
<dimen name="qq_header_content_height">100dp</dimen>

private void init() {
    Resources resources = getResources();
    mInitHeaderView = resources.getDimensionPixelOffset(R.dimen.qq_header_height);
    mHeaderMaxHeight = resources.getDimensionPixelOffset(R.dimen.qq_header_max_height);
    mInitContentLayoutTopMargin = resources.getDimensionPixelOffset(R.dimen.qq_content_margin_top);
}

接下来就是处理滑动的逻辑

    protected boolean overScrollBy(int deltaX, int deltaY,
                                   int scrollX, int scrollY,
                                   int scrollRangeX, int scrollRangeY,
                                   int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent) {
        Log.d(TAG, "overScrollBy: deltaY = " + deltaY+"  ; scrollY = "+scrollY);
		//判断方向 小于0 代表下滑
        if (mZoomView != null && deltaY < 0) {
            ViewGroup.LayoutParams lp = mZoomView.getLayoutParams();
            //计算出最大滑动位置 400dp - 300dp
            float maxDistance = mHeaderMaxHeight - mInitHeaderView;
            //最大允许的高度
            if (lp.height < mHeaderMaxHeight) {
            	//乘0.3 为了实现阻力效果
                float offestY = (float) Math.abs(deltaY * 0.3);
                //滑动变化的大小加上背景图片View的高度,并重新测量绘制
                lp.height = (int) (mZoomView.getHeight() + offestY);
                mZoomView.requestLayout();

				//计算content_layout的滑动距离
                int zoomViewHeight = mZoomView.getHeight();
                //当前背景图片的高度 减去 初始高度,得到是变化大小,最后得到变化的比例
                float ratio = (float) (zoomViewHeight - mInitHeaderView) / maxDistance;
                Log.d(TAG, "overScrollBy: zoomViewHeight = "+zoomViewHeight
                        +" ; mInitHeaderView = "+mInitHeaderView
                        +" ; ratio = "+ratio);
                //每次变化这么多  changeOffestY
                // 实际就是200dp大小
                float totalOffsetY = maxDistance * 2 ;

                FrameLayout.LayoutParams content_lp = (LayoutParams) mContentLayout.getLayoutParams();
                //初始margin 加上最大偏移乘以比例, 得到的就是此时应该偏移的大小
                content_lp.topMargin = (int) (mInitContentLayoutTopMargin + ratio * totalOffsetY);
                mContentLayout.requestLayout();
            }

        }
        return super.overScrollBy(deltaX, deltaY,
                scrollX, scrollY,
                scrollRangeX, scrollRangeY, maxOverScrollX, maxOverScrollY, isTouchEvent);
    }

所有逻辑都写在了注释里面, 逻辑相对简单,所以不重复描述。
接下来一个问题,如何回弹。 这里使用的策略也很简单,我们监听用户的手指离开屏幕,我们去执行动画,回到初始位置,也就是mInitHeaderViewmInitContentLayoutTopMargin

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        handlerEvent(ev);
        return super.onTouchEvent(ev);
    }

    private void handlerEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_UP) {
            Log.d(TAG, "handlerEvent: MotionEvent.ACTION_UP");
            if (mZoomView.getHeight() > mInitHeaderView) {
                SpringbackAnimation mSpringbackAnimation = new SpringbackAnimation(mInitHeaderView);
                mSpringbackAnimation.setInterpolator(new OvershootInterpolator());
                mSpringbackAnimation.setDuration(700);
                mZoomView.startAnimation(mSpringbackAnimation);
            }
        }
    }

监听ACTION_UP 事件然后判断高度是否大于mInitHeaderView 如果大于我们就开启动画执行恢复功能。


    private class SpringbackAnimation extends Animation {

        private int currentHeight;
        private int diffHeight;

        public SpringbackAnimation(int targetHeight) {
            this.currentHeight = mZoomView.getHeight();
            this.diffHeight = mZoomView.getHeight() - targetHeight;
        }

        /**
         * @param interpolatedTime 0 -1
         * @param t
         */
        @Override
        protected void applyTransformation(float interpolatedTime, Transformation t) {
            Log.d(TAG, "applyTransformation: interpolatedTime = " + interpolatedTime);
            mZoomView.getLayoutParams().height = (int) (currentHeight - diffHeight * interpolatedTime);
            mZoomView.requestLayout();

            float maxDistance = mInitHeaderView / 3;
            int zoomViewHeight = mZoomView.getHeight();
            float ratio = (float) (zoomViewHeight - mInitHeaderView) / maxDistance;
            Log.d(TAG, "overScrollBy: zoomViewHeight = "+zoomViewHeight
                    +" ; mInitHeaderView = "+mInitHeaderView
                    +" ; ratio = "+ratio);
            //每次变化这么多  changeOffestY
            float totalOffsetY = maxDistance * 2 ;

            FrameLayout.LayoutParams content_lp = (LayoutParams) mContentLayout.getLayoutParams();
            content_lp.topMargin = (int) (mInitContentLayoutTopMargin + ratio * totalOffsetY);
            mContentLayout.requestLayout();
            super.applyTransformation(interpolatedTime, t);
        }
    }

这段代码的逻辑其实非常简单,但是需要注意的是有一个方法参数需要解释。protected void applyTransformation(float interpolatedTime, Transformation t)其中interpolatedTime取值范围是0-1, 其实就是一个比例。 当前动画执行时间的比例。700ms的动画,时间过去了100ms 那么就是七分之一秒值。
最后贴一下完整的代码逻辑

public class QQOverScrollView extends ScrollView {

    private static final String TAG = "QQHeaderScrollView";

    private ImageView mZoomView;
    private View mContentLayout;

    private int mInitHeaderView;
    private int mInitContentLayoutTopMargin;
    private int mHeaderMaxHeight;

    public void setViewId(@IdRes int zoomViewId, @IdRes int contentLayoutId) {
        mZoomView = findViewById(zoomViewId);
        mContentLayout = findViewById(contentLayoutId);

        FrameLayout.LayoutParams lp = (LayoutParams) mContentLayout.getLayoutParams();
        lp.topMargin = mInitContentLayoutTopMargin;
        mContentLayout.requestLayout();
    }


    public QQOverScrollView(@NonNull Context context) {
        super(context);
        init();
    }

    public QQOverScrollView(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public QQOverScrollView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        Resources resources = getResources();
        mInitHeaderView = resources.getDimensionPixelOffset(R.dimen.qq_header_height);
        mHeaderMaxHeight = resources.getDimensionPixelOffset(R.dimen.qq_header_max_height);
        mInitContentLayoutTopMargin = resources.getDimensionPixelOffset(R.dimen.qq_content_margin_top);
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        handlerEvent(ev);
        return super.onTouchEvent(ev);
    }

    private void handlerEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_UP) {
            Log.d(TAG, "handlerEvent: MotionEvent.ACTION_UP");
            if (mZoomView.getHeight() > mInitHeaderView) {
                SpringbackAnimation mSpringbackAnimation = new SpringbackAnimation(mInitHeaderView);
                mSpringbackAnimation.setInterpolator(new OvershootInterpolator());
                mSpringbackAnimation.setDuration(700);
                mZoomView.startAnimation(mSpringbackAnimation);
            }
        }
    }

    @Override
    protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) {
        super.onOverScrolled(scrollX, scrollY, clampedX, clampedY);
        Log.d(TAG, "onOverScrolled: scrollY = "+scrollY+" ; clampedY = "+clampedY);
    }

    @Override
    protected boolean overScrollBy(int deltaX, int deltaY,
                                   int scrollX, int scrollY,
                                   int scrollRangeX, int scrollRangeY,
                                   int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent) {
        Log.d(TAG, "overScrollBy: deltaY = " + deltaY+"  ; scrollY = "+scrollY);

        if (mZoomView != null && deltaY < 0) {
            ViewGroup.LayoutParams lp = mZoomView.getLayoutParams();
            float maxDistance = mHeaderMaxHeight - mInitHeaderView;
            if (lp.height < mHeaderMaxHeight) {
                float offestY = (float) Math.abs(deltaY * 0.3);
                lp.height = (int) (mZoomView.getHeight() + offestY);
                mZoomView.requestLayout();

                int zoomViewHeight = mZoomView.getHeight();
                float ratio = (float) (zoomViewHeight - mInitHeaderView) / maxDistance;
                Log.d(TAG, "overScrollBy: zoomViewHeight = "+zoomViewHeight
                        +" ; mInitHeaderView = "+mInitHeaderView
                        +" ; ratio = "+ratio);
                //每次变化这么多  changeOffestY
                float totalOffsetY = maxDistance * 2 ;

                FrameLayout.LayoutParams content_lp = (LayoutParams) mContentLayout.getLayoutParams();
                content_lp.topMargin = (int) (mInitContentLayoutTopMargin + ratio * totalOffsetY);
                mContentLayout.requestLayout();
            }

        }
        return super.overScrollBy(deltaX, deltaY,
                scrollX, scrollY,
                scrollRangeX, scrollRangeY, maxOverScrollX, maxOverScrollY, isTouchEvent);
    }


    private class SpringbackAnimation extends Animation {

        private int currentHeight;
        private int diffHeight;

        public SpringbackAnimation(int targetHeight) {
            this.currentHeight = mZoomView.getHeight();
            this.diffHeight = mZoomView.getHeight() - targetHeight;
        }

        /**
         * @param interpolatedTime 0 -1
         * @param t
         */
        @Override
        protected void applyTransformation(float interpolatedTime, Transformation t) {
            Log.d(TAG, "applyTransformation: interpolatedTime = " + interpolatedTime);
            mZoomView.getLayoutParams().height = (int) (currentHeight - diffHeight * interpolatedTime);
            mZoomView.requestLayout();

            float maxDistance = mInitHeaderView / 3;
            int zoomViewHeight = mZoomView.getHeight();
            float ratio = (float) (zoomViewHeight - mInitHeaderView) / maxDistance;
            Log.d(TAG, "overScrollBy: zoomViewHeight = "+zoomViewHeight
                    +" ; mInitHeaderView = "+mInitHeaderView
                    +" ; ratio = "+ratio);
            //每次变化这么多  changeOffestY
            float totalOffsetY = maxDistance * 2 ;

            FrameLayout.LayoutParams content_lp = (LayoutParams) mContentLayout.getLayoutParams();
            content_lp.topMargin = (int) (mInitContentLayoutTopMargin + ratio * totalOffsetY);
            mContentLayout.requestLayout();
            super.applyTransformation(interpolatedTime, t);
        }
    }
}

如果您有更好的解决办法或者比较好的实现方式,恳请指教一下。同时也欢迎大家指出不足, 谢谢。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值