今天看啥  ›  专栏  ›  白日依山尽_

自定义滑动 ViewGroup

白日依山尽_  · 掘金  ·  · 2019-08-23 10:12
阅读 5

自定义滑动 ViewGroup

这个示例是学习自定义 ViewGroup 的一个学习示例,总结自定义 ViewGroup 方面的知识。

准备

首先了解自定义一个滑动流畅的 ViewGroup 需要那些知识。

  1. 基础

    • 自定义 ViewGroup onMeasure的处理,主要是让子 View 进行测量,然后根据子View 测量的数据得到本身尺寸。

    • 自定义 ViewGoup onLayout 的处理,如何摆放好子 View 只要计算细心一点很简单,记得处理子 View 的 margin以及自身的 padding。

    • 事件分发机制,在 onInterceptTouchEvent 中拦截滑动事件和处理滑动相关事务。

      onTouchEvent 中真正的处理滑动。

  2. 几个辅助类的使用

    • Scroller 用户处理快速滑动,记住滑的越快View滚得越快越多,主要是几个对应 api 的使用。
    • VelocityTracker 检测用户手势的滑动速度用于给 Scroller 使用,记住先初始化,然后添加手势,然后需要获取的时侯先计算再获取。

开始,处理 onMeasure / onLayout

处理 onMeasure 其实和 ViewGroup 的布局相关,当 ViewGroup 的尺寸不是精确的是 wrap_content 那么在处理的时侯就要考虑到在 x/y 轴空间不足时换行或者换列,而换行换列的操作都会影响 ViewGroup 最终的尺寸。

这里实现的是一个流式布局可垂直滑动的 ViewGroup

PS: 测量的这里没有考虑到布局因素,请各位看官掌眼!

onMeasure

总体步骤是

  • 获取自身测量模式及父View建议的尺寸数据。
  • 循环的获取子View,调用 measureChild 方法使子View进行自我测量。
  • 根据布局策略计算 ViewGroup 高度。
  • 根据测量模式决定最终高度
		@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        // 获取自身测量模式及尺寸数据
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        int realWidth = 0;
        int realHeight = getPaddingTop() + getPaddingBottom();

        // 测量子View,根据子view及自身测量模式完成测量
        int childCount = getChildCount();

        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            // 测量 子View
            measureChild(child, widthMeasureSpec, heightMeasureSpec);

            // 获取子View的宽高
            int childWidth = child.getMeasuredWidth();
            int childHeight = child.getMeasuredHeight();

            // 根据 子 View 的数据计算自身的高度
            realHeight = realHeight + childHeight;
        }
        // 高度最终决定
        switch (heightMode) {
            // 若设置了精确数值
            case MeasureSpec.EXACTLY:
                realHeight = heightSize;
                break;
            // 若是包裹内容
            case MeasureSpec.AT_MOST:
                if (realHeight > heightSize) {
                    realHeight = heightSize;
                }
                break;
        }
        // 宽度最终决定
        switch (widthMode) {
            case MeasureSpec.AT_MOST:
                realWidth = widthSize;
                break;
            case MeasureSpec.EXACTLY:
                realWidth = widthSize;
                break;
        }
        // 设置最终宽高
        setMeasuredDimension(realWidth, realHeight);
    }
复制代码

onLayout

布局就简单了,想要子 View 摆成什么样子就给子View什么定位参数,计算精确一点,注意处理 padding 和 margin。

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {

        int width = getMeasuredWidth();
        // 已经使用的高度
        viewHeight = getMeasuredHeight();
        // 下一行开始的 top 位置
        int nextTopPoint = getPaddingTop();

        // 已经使用的宽度
        int useWidth = getPaddingLeft();
        // 已经使用的高度
        int useHeight = getPaddingTop();

        int childCount = getChildCount();

        MarginLayoutParams marginLayoutParams;
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);

            Log.e("sun", child.getLayoutParams().getClass().getName());
            marginLayoutParams = (MarginLayoutParams) child.getLayoutParams();

            int childWidth = child.getMeasuredWidth();
            int childHeight = child.getMeasuredHeight();

            int left = useWidth + marginLayoutParams.leftMargin;
            int top = nextTopPoint + marginLayoutParams.topMargin;
            int right = left + childWidth + marginLayoutParams.rightMargin;
            int bottom = top + childHeight + marginLayoutParams.bottomMargin;

            // 是否需要换行
            if (right > width) {
                left = getPaddingLeft() + marginLayoutParams.leftMargin;
                top = top + childHeight + marginLayoutParams.topMargin;
                right = left + childWidth + marginLayoutParams.rightMargin;
                bottom = top + childHeight + marginLayoutParams.bottomMargin;
                nextTopPoint = top;
            }
            useWidth = right;

            viewContentHeight = nextTopPoint;

            child.layout(left, top, right, bottom);
        }
    }
复制代码

处理滑动

Android 中所有的 View 都是支持滑动,系统有提供 scrollBy() 以及 scrollTo 两个方法来处理滑动,两个方法的区别在于一个是相对 view 之前的位置,一个是绝对与 view 的 top。

拦截滑动事件

拦截滑动事件是在 onInterceptTouchEvent 中处理,为什么呢?方便处理滑动冲突,以及避免被子View抢了事件。

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        int eventKey = ev.getAction();
        // 判断是何种事件
        switch (eventKey) {
            case MotionEvent.ACTION_DOWN:
                // 记下按下的屏幕坐标
                if (mVelocityTracker == null) {
                    mVelocityTracker = VelocityTracker.obtain();
                }
                // 如果 scroller 动画没停止,但是用户已经触摸,则该立刻停止
                if (!mScroller.isFinished()) {
                    mScroller.abortAnimation();
                }
                lastY = ev.getY();
                break;
            case MotionEvent.ACTION_MOVE:
                float moveY = ev.getY();
                float moveSlop = Math.abs(moveY - lastY);
            		// 滑动值大于最小滑动值则拦截事件
                if (moveSlop > mTouchSlop) {
                    return true;
                }
                break;
        }
        return super.onInterceptTouchEvent(ev);
    }
复制代码

处理滑动事件

		@Override
    public boolean onTouchEvent(MotionEvent event) {
        int eventKey = event.getAction();
        switch (eventKey) {
            case MotionEvent.ACTION_DOWN:
                break;
            case MotionEvent.ACTION_MOVE:
                mVelocityTracker.addMovement(event);

                float moveY = event.getY();
                int moveDis = (int) (lastY - moveY);
                // 几个参数的意思:需要滑动 x 的距离,需滑动 y 的距离,x 已经滑动的值,y 已经滑动的值,
                //      x 滑动的范围 ,y 滑动的范围,x 显示边缘效果时的最大值(待验证,可能是回弹间隙),y 显示边缘效果时的最大值(待验证,可能是回弹间隙),执行完滚动之后是否继续处理后续事件(没用到的参数)
                overScrollBy(0, moveDis, 0, getScrollY(), 0, getRangY(), 0, 0, true);
                // 记录最后一次的坐标
                lastY = event.getY();
                return true;
            case MotionEvent.ACTION_UP:
                // 当快速滑动时,使用 scroller 完成流程滑动
                mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
                float velocity = mVelocityTracker.getYVelocity();
                Log.e("sun", "速度" + velocity + "默认速度" + mMinimumVelocity);

                if (Math.abs(velocity) > mMinimumVelocity) {
                    mScroller.fling(0, getScrollY(), 0, (int) -velocity, 0, 0, 0, getRangY());
//                    invalidate();
                }
                mVelocityTracker.clear();
                break;
        }
        return super.onTouchEvent(event);
    }
复制代码

完整代码请戳:gitHub




原文地址:访问原文地址
快照地址: 访问文章快照