view系列-viewGroup进阶(FlowLayout实战)

479 阅读7分钟

前言

前面viewGroup文章简单阐述了自定义viewGroup需要注意的事项,但是过于简单,实战性不强,导致对viewGroup理解的比较浅显,所以这篇文章主要是结合实战例子进行讲解。

深化的自定义viewgroup布局流程

  • 1.自定义属性:声明、设置、解析获取自定义值
  • 2.在onmeasure()测量自身、child宽高
  • 3.在onlayout() 根据自己规则确认children位置
  • 4.绘制ondraw()
  • 5.处理layoutparams
  • 6.触摸反馈:滑动事件

实现FlowLayout思路

  • 1.一行能放几个?
  • 2.怎么摆放子view,每行摆放完后怎么换行?
  • 3.一个屏幕不够放置子view的话,如何实现滑动?以及怎么处理滑动冲突?
  • 4.整个可滑动的区域可滑动最大区域怎么设定?总不能可以一直下滑、上滑,导致控件不在屏幕中 基本上解决了上面的问题,整个控件就达到了我们的要求。

问题一:一行能放几个?

前面我们讲到,测量控件大小 核心方法就是onMeasure(),只有测量出每个控件的大小,我们才能知道每行最多可以放几个控件。我就直接贴代码了

   /**
     * 第一步先测量子控件大小,测量完以后可以设置当前控件的大小了。
     *
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //todo 这个是调用父类(ViewGroup)测量模式 设置大小 为啥需要重写呢
        // 1.因为当父类对当前控件不做限制的话 当前控件高度默认是非常小的
        // 2.父类给的测量模式是match/wrap_content的时候 ,子类默认跟父类一样 实际上可能没有这么大
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        //父类提供给当前的测量模式跟大小(不代表最终的大小)
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        //所以需要重新测量子类大小
        int childCount = getChildCount();
        if (childCount == 0) {
            return;
        }
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            //1. measureChildren();里面会遍历调用measureChild()方法
            //2.measureChild();参数一是要测量的view 参数二跟参数三是传入父类的MeasureSpec
            //todo 重点一 :measureChild分析下
            //这边注意一种场景 :父类采用Exactly,子类布局采用的是match_parent模式 那就会导致当前view占满屏,所以此处需要针对这种情况设置一个固定值
            //都是针对view的
            //todo 重点二:(这种写法不行)if (child.getLayoutParams().height == LayoutParams.MATCH_PARENT) { 这种写法是对getChildMeasureSpec研究不正确导致的
            //    LayoutParams layoutParams = child.getLayoutParams();
            //    int childMeasureSpec = getChildMeasureSpec(widthMeasureSpec, 0, layoutParams.width);
            //    //todo 暂时给死高度100px
            //    int childMeasureSpec1 = getChildMeasureSpec(widthMeasureSpec, 0,100);
            //    child.measure(childMeasureSpec,childMeasureSpec1);
            //} else {
            //    measureChild(child, widthMeasureSpec, heightMeasureSpec);
            //}


            //需要换种思路
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
            if (child.getMeasuredHeight() == heightSize) {
                LayoutParams layoutParams = child.getLayoutParams();
                int childMeasureSpec = getChildMeasureSpec(widthMeasureSpec, 0, layoutParams.width);
                //todo 暂时给死高度100px 这种写法虽然基本上解决问题了 但是有个弊端 必须给死高度 还可以采用当前行高度
                int childMeasureSpec1 = getChildMeasureSpec(widthMeasureSpec, 0, 100);
                child.measure(childMeasureSpec, childMeasureSpec1);
            }
        }
        //测量完以后就开始计算当前控件的大小
        //思路流程:1.因为是流式布局 那么宽度是最大行的宽度,高度是每行高度之和,
        //          2.有些行数默认采用的是跟父控件一样的高度 那就会导致某行直接撑满屏幕的问题,这种需要设置一个固定高度/或者去当前行最大行的高度为这个子view的高度
        //          3.还需要考虑多少个view以后换行的问题
        //          3.测量完以后需要通过onLayout进行摆放,所以需要保存所有的view

        //当前行的高度
        int curLineHeight = 0;
        //当前行宽度
        int curLineWidth = 0;
        //最大行宽度
        int maxLineWidth = 0;
        //所有行高度
        int totalLineHeight = 0;
        // 当前行的view集合
        List<View> curlineViews = new ArrayList<>();
        //所有行view的集合
        totalLineViews = new ArrayList<>();
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            //当前不需要换行
            if (curLineWidth + child.getMeasuredWidth() < widthSize) {

                curLineWidth = curLineWidth + child.getMeasuredWidth();
                curLineHeight = Math.max(curLineHeight, child.getMeasuredHeight());
                curlineViews.add(child);

            } else {
                //换行
                maxLineWidth = Math.max(maxLineWidth, curLineWidth);
                curLineWidth = 0;
                totalLineHeight = totalLineHeight + curLineHeight;
                curLineHeight = 0;
                totalLineViews.add(curlineViews);
                //不能调用clear方法 会导致totalLineViews里面view被清空
                curlineViews = new ArrayList<>();

            }
        }

        //注意:循环外 上面代码忽略了如果有最后一行的话 是没有添加进去的,因为最后一行走了if语句 但是永远走不到else里面 也就没有进行计算
        // 添加最后一行 其实就是再走一遍else方法
        maxLineWidth = Math.max(maxLineWidth, curLineWidth);
        curLineWidth = 0;
        totalLineHeight = totalLineHeight + curLineHeight;
        curLineHeight = 0;
        totalLineViews.add(curlineViews);
        //不能调用clear方法 会导致totalLineViews里面view被清空
        curlineViews = new ArrayList<>();
      setMeasuredDimension(widthMode == MeasureSpec.EXACTLY?widthSize:maxLineWidth, heightMode == MeasureSpec.EXACTLY?heightSize:totalLineHeight);
    }

问题二:怎么摆放子view,每行摆放完后怎么换行?

这个就涉及到onLayout()方法了,也是直接贴代码吧。

protected void onLayout(boolean changed, int l, int t, int r, int b) {
        //思路流程: 按行摆放
        if (totalLineViews.size() <= 0) {
            return;
        }
        int left = 0;
        int top = 0;
        //当前行高最大值
        int curLineMaxHeight = 0;
        for (int i = 0; i < totalLineViews.size(); i++) {
            //当前行view 集合
            List<View> curViews = totalLineViews.get(i);
            if (curViews.size() <= 0) {
                continue;
            }
            //初始化每行左边位置
            left = 0;

            for (int j = 0; j < curViews.size(); j++) {
                View view = curViews.get(j);
                view.layout(left, top, left + view.getMeasuredWidth(), top + view.getMeasuredHeight());
                left = left + view.getMeasuredWidth();
                //获取当前行最大高度
                curLineMaxHeight = Math.max(curLineMaxHeight, view.getMeasuredHeight());
            }
            //为下一行高度赋初始值
            top = top + curLineMaxHeight;
            curLineMaxHeight = 0;
        }
    }

问题3:一个屏幕不够放置子view的话,如何实现滑动?以及怎么处理滑动冲突?

这里面其实难点是事件滑动的过程中事件冲突、事件拦截的处理,可参考我之前的文章事件分发源码分析,我们这边主要是处理viewGroup的事件滑动,所以activity、view的滑动你们可以暂时忽略,注意!如果事件滑动掌握不了,就不建议往下看了。
效果明天放上来。不知道怎么放gif/视频

首先考虑什么时候拦截,上代码

  public FlowLayoutNew(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs);
    }

    /**
     * 初始化
     *
     * @param context
     * @param attrs
     */
    private void init(Context context, AttributeSet attrs) {
        ViewConfiguration configuration = ViewConfiguration.get(context);
        //获取最小滑动距离
        scaledPagingTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration);
        //如果想滑动还需要设置
        setClickable(true);
    }
    
  --------------------------------------------------  
  
 private float downX;
    private float downY;
    //1.考虑什么时候拦截
    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        boolean isIntercept = false;

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                downX = event.getX();
                downY = event.getY();
                isIntercept = false;
                break;
            case MotionEvent.ACTION_MOVE:
                //
                float offsetX = event.getX() - downX;
                float offsetY = event.getY() - downY;
                if (Math.abs(offsetY) > Math.abs(offsetX) && Math.abs(offsetY) > scaledPagingTouchSlop) {
                    isIntercept = true;
                } else {
                    isIntercept = false;
                }
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
            default:
                isIntercept = false;
                break;
        }
       return isIntercept;
    }

拦截完后怎么处理滑动(其实这里面也解答了问题四)

 //todo 这个方法多久刷新一次
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (!canScrollable) {
            return super.onTouchEvent(event);
        }
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                //重点:注意:当motionEvent事件状态是ACTION_DOWN,当前控件是不拦截的
                // 所以导致onTouchEvent的是不存在MotionEvent.ACTION_DOWN状态的,因为此处赋值是不生效的
                break;
            case MotionEvent.ACTION_MOVE:
                // 只有isIntercept= true 的状况下,才会走这里面的代码
                float offsetY = downY - event.getY();
                //注意当offsetY大于0的时候,说明期待滑动的效果是上滑,scrollTo 会调用invalidate()方法 然后到draw方法 再到canvas.translate(mLeft - sx, mTop - sy);
                //sx值是scrollTo方法中的参数一,sy值是方法的参数二 scrollTo 其实移动的是画布
                float dy = getScrollY() + offsetY;
                //最上面不能留白
                if (dy<0) {
                    dy = 0;
                }
                //最下面也不能留白
                if (dy>(realHeight-getMeasuredHeight())) {
                    dy = realHeight-getMeasuredHeight();
                }
                // todo 重点:
                //方式一:
                scrollTo(getScrollX(), (int) dy);
                //方式二:

                //注意: 这个地方一定需要重新赋值 因为downY只有在按下的那时候才会赋值
                downY = event.getY();
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
            default:
                break;
        }
        return super.onTouchEvent(event);
    }

通过其他方式处理滑动效果

上面代码讲了通过scrollTo方法处理滑动,但是这种处理效果会不太丝滑,就是一顿一顿的。

scroller = new Scroller(context);
---------------------------------------
    //方式二:
                // scroller.startScroll方法 通过提供起点和行进距离开始滚动。 *滚动将在*持续时间内使用默认值250毫秒
                //startx 起始水平滚动偏移量(以像素为单位)。正*号会将内容向左滚动
                //dx x行驶的水平距离。正数会将*内容向左滚动
                //scroller.getFinalY()  最终的Y偏移量是距原点的绝对距离。
                //跟scrollTo方法的区别 https://blog.csdn.net/smile_Running/article/details/81635279
                scroller.startScroll(0,getScrollY(),0, (int) offsetY);
                //注意: 这个地方一定需要重新赋值 因为downY只有在按下的那时候才会赋值
                downY = event.getY();
                invalidate();
------------------------------------
 @Override
    public void computeScroll() {
        super.computeScroll();
        //判断是否在滑动 返回true 表示动画还没结束
        if (scroller.computeScrollOffset()) {
            //返回滚动中的当前Y偏移量。它是距原点的绝对距离
            int currY = scroller.getCurrY();
            if(currY < 0){
                currY = 0;
            }
            if(currY > realHeight - getMeasuredHeight()){
                currY = realHeight - getMeasuredHeight();
            }
            scrollTo(0,currY );
            postInvalidate();
        }
    }
                

//setBackgroundResource(R.drawable.search_bg); //int defaultWidth = getContext().getResources() // .getDimensionPixelOffset(R.dimen.dp_290); //int defaultHeight = getContext().getResources() // .getDimensionPixelOffset(R.dimen.dp_30); // setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,getd)); 跟下面区别在那 // www.jianshu.com/p/36b200a0b… //setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); //setLayoutParams(new LayoutParams(defaultWidth, defaultHeight)); setOrientation(HORIZONTAL); //setBackgroundResource(R.drawable.search_bg);

项目中SearchLayout使用 主要是LayoutParams使用 还有getlayoutparam()获取的是什么
new LinearLayout.LayoutParams跟new LayoutParams区别
    https://www.jianshu.com/p/0d6f753fdd92
    http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2012/1202/668.html
    https://www.jianshu.com/p/36b200a0bff4

知识储备

1.Scroller类中startScroll()、computeScroll()方法、scrollTo()和scrollBy()方法 Scroller的使用及解析
2.事件分发 onInterceptTouchEvent onTouchEvent 可以参考我之前的文章
3.onmeasure onlayout 主要是viewGroup的事件分发
4. scroller类(为了滑动流畅) 涉及到
mstartX:滑动时起点偏移坐标
mFInalx:滑动完成后的偏移坐标
mcurrX:滑动过程中,根据fun计算当前的滑动偏移距离【start-final】
mdeltax:滑动过程中,mfianl-mcurr
方法 startSrcoll主要是做初始化
computeScroll
invalidate 跟postinvalidate区别 :第一个区别就不用说了 postinvalidate封装了hanlder,但是第二个区别就不是很懂,说是如果invalidate没有执行完再次调用invalidate不会执行 而postinvalidate不会,如果哪位老友知道区别,请评论告知 谢谢

完整代码粘贴

1.核心类

package com.xm.wanapp.view;

import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.widget.Scroller;

import java.util.ArrayList;
import java.util.List;

import androidx.core.view.ViewConfigurationCompat;

public class FlowLayoutNew extends ViewGroup {

    /**
     * 整个是否可以滑动。
     */
    boolean canScrollable = false;
    private List<List<View>> totalLineViews;
    /**
     * 最小滑动距离
     */
    private int scaledPagingTouchSlop;

    private Scroller scroller;

    public FlowLayoutNew(Context context) {
        this(context, null);
    }

    public FlowLayoutNew(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public FlowLayoutNew(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs);
    }

    /**
     * 初始化
     *
     * @param context
     * @param attrs
     */
    private void init(Context context, AttributeSet attrs) {
        ViewConfiguration configuration = ViewConfiguration.get(context);
        //获取最小滑动距离
        scaledPagingTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration);
        //如果想滑动还需要设置
        setClickable(true);

        scroller = new Scroller(context);
    }


    /**
     * 第一步先测量子控件大小,测量完以后可以设置当前控件的大小了。
     *
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //todo 这个是调用父类(ViewGroup)测量模式 设置大小 为啥需要重写呢
        // 1.因为当父类对当前控件不做限制的话 当前控件高度默认是非常小的
        // 2.父类给的测量模式是match/wrap_content的时候 ,子类默认跟父类一样 实际上可能没有这么大
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        //父类提供给当前的测量模式跟大小(不代表最终的大小)
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        //所以需要重新测量子类大小
        int childCount = getChildCount();
        if (childCount == 0) {
            return;
        }
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            //1. measureChildren();里面会遍历调用measureChild()方法
            //2.measureChild();参数一是要测量的view 参数二跟参数三是传入父类的MeasureSpec
            //todo 重点一 :measureChild分析下
            //这边注意一种场景 :父类采用Exactly,子类布局采用的是match_parent模式 那就会导致当前view占满屏,所以此处需要针对这种情况设置一个固定值
            //都是针对view的
            //todo 重点二:(这种写法不行)if (child.getLayoutParams().height == LayoutParams.MATCH_PARENT) { 这种写法是对getChildMeasureSpec研究不正确导致的
            //    LayoutParams layoutParams = child.getLayoutParams();
//                interactions childMeasureSpec = getChildMeasureSpec(widthMeasureSpec, 0, layoutParams.width);
            //    //todo 暂时给死高度100px
            //    int childMeasureSpec1 = getChildMeasureSpec(widthMeasureSpec, 0,100);
            //    child.measure(childMeasureSpec,childMeasureSpec1);
            //} else {
            //    measureChild(child, widthMeasureSpec, heightMeasureSpec);
            //}


            //需要换种思路
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
            if (child.getMeasuredHeight() == heightSize) {
                LayoutParams layoutParams = child.getLayoutParams();
                int childMeasureSpec = getChildMeasureSpec(widthMeasureSpec, 0, layoutParams.width);
                //todo 暂时给死高度100px 这种写法虽然基本上解决问题了 但是有个弊端 必须给死高度 还可以采用当前行高度
                int childMeasureSpec1 = getChildMeasureSpec(widthMeasureSpec, 0, 100);
                child.measure(childMeasureSpec, childMeasureSpec1);
            }
        }
        //测量完以后就开始计算当前控件的大小
        //思路流程:1.因为是流式布局 那么宽度是最大行的宽度,高度是每行高度之和,
        //          2.有些行数默认采用的是跟父控件一样的高度 那就会导致某行直接撑满屏幕的问题,这种需要设置一个固定高度/或者去当前行最大行的高度为这个子view的高度
        //          3.还需要考虑多少个view以后换行的问题
        //          3.测量完以后需要通过onLayout进行摆放,所以需要保存所有的view

        //当前行的高度
        int curLineHeight = 0;
        //当前行宽度
        int curLineWidth = 0;
        //最大行宽度
        int maxLineWidth = 0;
        //所有行高度
        int totalLineHeight = 0;
        // 当前行的view集合
        List<View> curlineViews = new ArrayList<>();
        //所有行view的集合
        totalLineViews = new ArrayList<>();
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            //当前不需要换行
            if (curLineWidth + child.getMeasuredWidth() < widthSize) {

                curLineWidth = curLineWidth + child.getMeasuredWidth();
                curLineHeight = Math.max(curLineHeight, child.getMeasuredHeight());
                curlineViews.add(child);

            } else {
                //换行
                maxLineWidth = Math.max(maxLineWidth, curLineWidth);
                curLineWidth = 0;
                totalLineHeight = totalLineHeight + curLineHeight;
                curLineHeight = 0;
                totalLineViews.add(curlineViews);
                //不能调用clear方法 会导致totalLineViews里面view被清空
                curlineViews = new ArrayList<>();

            }
        }

        //注意:循环外 上面代码忽略了如果有最后一行的话 是没有添加进去的,因为最后一行走了if语句 但是永远走不到else里面 也就没有进行计算
        // 添加最后一行 其实就是再走一遍else方法
        maxLineWidth = Math.max(maxLineWidth, curLineWidth);
        curLineWidth = 0;
        totalLineHeight = totalLineHeight + curLineHeight;
        curLineHeight = 0;
        totalLineViews.add(curlineViews);
        //不能调用clear方法 会导致totalLineViews里面view被清空
        curlineViews = new ArrayList<>();

        //todo 其实这个地方计算是不准确的
        if (heightSize<totalLineHeight) {
            canScrollable = true;
        }
        realHeight = totalLineHeight;
        setMeasuredDimension(widthMode == MeasureSpec.EXACTLY?widthSize:maxLineWidth, heightMode == MeasureSpec.EXACTLY?heightSize:totalLineHeight);
    }

    /**
     * 真实内容的高度
     */
    int realHeight;

    /**
     * 计算子view 摆放布局。
     *
     * @param changed
     * @param l
     * @param t
     * @param r
     * @param b
     */
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        //思路流程: 按行摆放
        if (totalLineViews.size() <= 0) {
            return;
        }
        int left = 0;
        int top = 0;
        //当前行高最大值
        int curLineMaxHeight = 0;
        for (int i = 0; i < totalLineViews.size(); i++) {
            //当前行view 集合
            List<View> curViews = totalLineViews.get(i);
            if (curViews.size() <= 0) {
                continue;
            }
            //初始化每行左边位置
            left = 0;

            for (int j = 0; j < curViews.size(); j++) {
                View view = curViews.get(j);
                view.layout(left, top, left + view.getMeasuredWidth(), top + view.getMeasuredHeight());
                left = left + view.getMeasuredWidth();
                //获取当前行最大高度
                curLineMaxHeight = Math.max(curLineMaxHeight, view.getMeasuredHeight());
            }
            //为下一行高度赋初始值
            top = top + curLineMaxHeight;
            curLineMaxHeight = 0;
        }
    }


    private float downX;
    private float downY;
    //1.考虑什么时候拦截
    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        boolean isIntercept = false;

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                downX = event.getX();
                downY = event.getY();
                isIntercept = false;
                break;
            case MotionEvent.ACTION_MOVE:
                //
                float offsetX = event.getX() - downX;
                float offsetY = event.getY() - downY;
                if (Math.abs(offsetY) > Math.abs(offsetX) && Math.abs(offsetY) > scaledPagingTouchSlop) {
                    isIntercept = true;
                } else {
                    isIntercept = false;
                }
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
            default:
                isIntercept = false;
                break;
        }
       return isIntercept;
    }

    //todo 这个方法多久刷新一次
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (!canScrollable) {
            return super.onTouchEvent(event);
        }
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                //重点:注意:当motionEvent事件状态是ACTION_DOWN,当前控件是不拦截的
                // 所以导致onTouchEvent的是不存在MotionEvent.ACTION_DOWN状态的,因为此处赋值是不生效的
                break;
            case MotionEvent.ACTION_MOVE:
                // 只有isIntercept= true 的状况下,才会走这里面的代码
                float offsetY = downY - event.getY();
                //注意当offsetY大于0的时候,说明期待滑动的效果是上滑,scrollTo 会调用invalidate()方法 然后到draw方法 再到canvas.translate(mLeft - sx, mTop - sy);
                //sx值是scrollTo方法中的参数一,sy值是方法的参数二 scrollTo 其实移动的是画布
                float dy = getScrollY() + offsetY;
                //最上面不能留白
                if (dy<0) {
                    dy = 0;
                }
                //最下面也不能留白
                if (dy>(realHeight-getMeasuredHeight())) {
                    dy = realHeight-getMeasuredHeight();
                }
                // todo 重点:
                //方式一:
                scrollTo(getScrollX(), (int) dy);
                //方式二:
                // scroller.startScroll方法 通过提供起点和行进距离开始滚动。 *滚动将在*持续时间内使用默认值250毫秒
                //startx 起始水平滚动偏移量(以像素为单位)。正*号会将内容向左滚动
                //dx x行驶的水平距离。正数会将*内容向左滚动
                //scroller.getFinalY()  最终的Y偏移量是距原点的绝对距离。
                //跟scrollTo方法的区别 https://blog.csdn.net/smile_Running/article/details/81635279
                scroller.startScroll(0,getScrollY(),0, (int) offsetY);
                //注意: 这个地方一定需要重新赋值 因为downY只有在按下的那时候才会赋值
                downY = event.getY();
                invalidate();
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
            default:
                break;
        }
        return super.onTouchEvent(event);
    }

    @Override
    public void computeScroll() {
        super.computeScroll();
        //判断是否在滑动 返回true 表示动画还没结束
        if (scroller.computeScrollOffset()) {
            //返回滚动中的当前Y偏移量。它是距原点的绝对距离
            int currY = scroller.getCurrY();
            if(currY < 0){
                currY = 0;
            }
            if(currY > realHeight - getMeasuredHeight()){
                currY = realHeight - getMeasuredHeight();
            }
            scrollTo(0,currY );
            postInvalidate();
        }
    }
}

XML文件

<com.xm.wanapp.view.FlowLayoutNew
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="200dp"
        tools:context=".MainActivity"
        android:background="#40000000">
        <Button
            android:layout_width="wrap_content"
            android:layout_height="55dp"
            android:text="Hello hi ..." />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:text="你是谁呀" />
        <View
            android:layout_width="30dp"
            android:layout_height="wrap_content"
            android:background="@color/colorAccent" />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="人在他在,塔亡人亡"
            android:layout_gravity="bottom"/>

        <Button
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:text="生活不止眼前的苟且,还有诗和远方" />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="90dp"
            android:text="发电房" />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="小麻小儿郎呀" />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Hello hi ..." />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="你是谁呀" />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="85dp"
            android:text="人在他在,塔亡人亡"
            android:layout_gravity="bottom"/>

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="生活不止眼前的苟且,还有诗和远方" />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="发电房" />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="小麻小儿郎呀" />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="45dp"
            android:text="Hello hi ..." />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="你是谁呀" />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="人在他在,塔亡人亡"
            android:layout_gravity="bottom"/>

        <Button
            android:layout_width="wrap_content"
            android:layout_height="75dp"
            android:text="生活不止眼前的苟且,还有诗和远方" />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="发电房" />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="小麻小儿郎呀" />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Hello hi ..." />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="你是谁呀" />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="60dp"
            android:text="人在他在,塔亡人亡"
            android:layout_gravity="bottom"/>

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="生活不止眼前的苟且,还有诗和远方" />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="发电房" />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="85dp"
            android:text="小麻小儿郎呀" />


        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Hello hi ..." />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="你是谁呀" />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="100dp"
            android:text="人在他在,塔亡人亡"
            android:layout_gravity="bottom"/>

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="生活不止眼前的苟且,还有诗和远方" />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="发电房" />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="小麻小儿郎呀" />


        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Hello hi ..." />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="你是谁呀" />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="85dp"
            android:text="人在他在,塔亡人亡"
            android:layout_gravity="bottom"/>

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="生活不止眼前的苟且,还有诗和远方" />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="发电房" />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="小麻小儿郎呀" />


        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Hello hi ..." />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="你是谁呀" />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="人在他在,塔亡人亡"
            android:layout_gravity="bottom"/>

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="生活不止眼前的苟且,还有诗和远方" />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="85dp"
            android:text="发电房" />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="小麻小儿郎呀" />


        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Hello hi ..." />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="你是谁呀" />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="人在他在,塔亡人亡"
            android:layout_gravity="bottom"/>

        <Button
            android:layout_width="wrap_content"
            android:layout_height="65dp"
            android:text="生活不止眼前的苟且,还有诗和远方" />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="100dp"
            android:text="发电房" />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="小麻小儿郎呀" />


        <Button
            android:layout_width="wrap_content"
            android:layout_height="75dp"
            android:text="Hello hi ..." />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="你是谁呀" />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="人在他在,塔亡人亡"
            android:layout_gravity="bottom"/>

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="生活不止眼前的苟且,还有诗和远方" />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="发电房" />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="80dp"
            android:text="小麻小儿郎呀" />


        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Hello hi ..." />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="你是谁呀" />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="人在他在,塔亡人亡"
            android:layout_gravity="bottom"/>

        <Button
            android:layout_width="wrap_content"
            android:layout_height="300dp"
            android:text="生活不止眼前的苟且,还有诗和远方" />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="发电房" />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="小麻小儿郎呀" />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Hello hi ..." />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="你是谁呀" />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="65dp"
            android:text="人在他在,塔亡人亡"
            android:layout_gravity="bottom"/>

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="生活不止眼前的苟且,还有诗和远方1" />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="250dp"
            android:text="发电房" />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="这是结束" />

    </com.xm.wanapp.view.FlowLayoutNew>