自定义流式布局

自定义流式布局

1、自定义流式布局


废话不多说,先上效果图:

代码中已经对流式布局做了详尽的描述,代码如下:

    /**
     *流式布局demo
     */
    public class MyFlowLayout extends ViewGroup {
        // 保存所有行
        private List<Line> mLineList;
        // 当前行
        private Line mLine;
        // 列间距 水平间距,左右间距
        private int horizontalSpace;
        // 行间距 竖直间距,上下间距
        private int verticalSpace;
        // 每一行可添加的宽度
        private int mValidWidth;
        // 最大行数
        private int mMaxLineCount = 100;


        public MyFlowLayout(Context context) {
            super(context);
        }

        public MyFlowLayout(Context context, AttributeSet attrs) {
            super(context, attrs);
        }

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


        /**
         *在onMeasure部分对所有的子控件进行宽度的测量,并将他们封装为一个个的Line对象
         */
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            int widthSize = MeasureSpec.getSize(widthMeasureSpec);
            int heightSize = MeasureSpec.getSize(heightMeasureSpec);
            int widthMode = MeasureSpec.getMode(widthMeasureSpec);
            int heightMode = MeasureSpec.getMode(heightMeasureSpec);

            // 当前子控件最多被允许的宽度
            mValidWidth = widthSize - getPaddingLeft() - getPaddingRight();
            // 循环遍历所有的子控件,计算每一行的宽度
            for (int i = 0; i < getChildCount(); i++) {
                View childView = getChildAt(i);
                // 给child设置宽高标准,如果父控件的模式是无限制时,宽高都0,所以子控件也要无限制
                // 如果父控件的值是精确模式,或者至多模式,子控件则是至多模式
                int widthAtMost = MeasureSpec.makeMeasureSpec(mValidWidth, widthMode == MeasureSpec.UNSPECIFIED ? MeasureSpec.UNSPECIFIED : MeasureSpec.AT_MOST);
                int heightAtMost = MeasureSpec.makeMeasureSpec(heightSize, heightMode == MeasureSpec.UNSPECIFIED ? MeasureSpec.UNSPECIFIED : MeasureSpec.AT_MOST);
                childView.measure(widthAtMost, heightAtMost);
                // 拿到设置标准后的宽
                int measuredWidth = childView.getMeasuredWidth();
                // 初始提供一个line
                if (mLine == null)
                    mLine = new Line(mValidWidth);

                // 如果所剩的宽度不够时,需要换行,但是换行分为当前行已有 数据 和 无数据 两种情况
                if (mLine.surplusWidth < measuredWidth) {
                    // 行中已经有数据时,需要换行
                    if (mLine.getLineCount() != 0) {
                        //换新行,换行失败跳出
                        if (!newLine()) {
                            break;
                        }
                    }
                    // 如果行中没有数据,但宽度不够时也要硬塞
                }
                mLine.addView(childView);
                // 减去这个child宽度
                mLine.surplusWidth -= measuredWidth;
                // 减去列间距
                mLine.surplusWidth -= horizontalSpace;
            }
            // 在循环结束后,要把最后一行添加上去
            if(mLine!=null)
                mLineList.add(mLine);
            // 设置整个控件的高度
            heightSize = 0;
            for (int i = 0, size = mLineList.size(); i < size; i++) {
                heightSize += mLineList.get(i).maxTop;
                if (i > 0) {
                    heightSize += verticalSpace;
                }
            }
            heightSize = heightSize + getPaddingTop() + getPaddingBottom();
            // 设置整个控件的宽度
            widthSize = mValidWidth + getPaddingRight() + getPaddingLeft();
            // 设置整个控件的大小
            setMeasuredDimension(widthSize, heightSize);

        }

        /**
         * 换新行
         *
         * @return 超出最大行数时返回false
         */
        private boolean newLine() {
            if (mLineList == null) {
                mLineList = new ArrayList<>();
            }
            // 换行时将上一行添加到集合中,这导致最后一行需要手动添加
            mLineList.add(mLine);
            if (mLineList.size() < getMaxLineCount()) {
                mLine = new Line(mValidWidth);
                return true;
            }
            return false;
        }

        /**
        * onLayout方法是在onMeasure之后执行的,onLayout的方法要将每一个Line对象进行布局,
        * 主要方式便是给每一个指定的左上角坐标,让Line对象自己去完成行中对象的layout
        */
        @Override
        protected void onLayout(boolean changed, int l, int t, int r, int b) {
            l += getPaddingLeft();
            t += getPaddingTop();
            for (int i = 0, size = mLineList.size(); i < size; i++) {
                mLineList.get(i).layout(l, t);
                t += verticalSpace + mLineList.get(i).maxTop;
            }
            System.out.print("");
        }

        /**
         * Line类,用来封装每一行中的控件
         */
        class Line {
            // 行中元素
            private List<View> mList = new ArrayList<View>();
            // 剩余宽度
            public int surplusWidth;
            // 行最大高度
            public int maxTop;

            public Line(int surplusWidth) {
                this.surplusWidth = surplusWidth;
            }

            public void addView(View view) {
                mList.add(view);
                // 判断子控件的高度
                int measuredHeight = view.getMeasuredHeight();
                // 判断并设置最大的高度
                if (measuredHeight > maxTop) {
                    maxTop = measuredHeight;
                }
            }

            // 用于判断行中是否有数据
            public int getLineCount() {
                return mList.size();
            }

            public void layout(int left, int top) {
                View view = null;

                // 平均宽度,当宽度有剩余时要将这些宽度平均出来
                int totalWidth = 0;
                for (int i = 0, size = mList.size(); i < size; i++) {
                    totalWidth += mList.get(i).getMeasuredWidth();
                    if (i > 0)
                        totalWidth += horizontalSpace;
                }
                int aveWidth = (mValidWidth - totalWidth) / mList.size();
                if (aveWidth <= 0)
                    aveWidth = 0;

                for (int i = 0, size = mList.size(); i < size; i++) {
                    view = mList.get(i);

                    int viewWidth = view.getMeasuredWidth() + aveWidth;
                    int viewHeight = view.getMeasuredHeight();

                    // 重新测量加上平均宽度的宽高
                    int widthSpec = MeasureSpec.makeMeasureSpec(viewWidth, MeasureSpec.EXACTLY);
                    int heightSpec = MeasureSpec.makeMeasureSpec(viewHeight, MeasureSpec.EXACTLY);
                    view.measure(widthSpec, heightSpec);

                    int offsetHeight = 0;
                    // 加上不同高度控件的偏移
                    if (viewHeight < maxTop)
                        offsetHeight = (maxTop - viewHeight) / 2;
                    view.layout(left, top + offsetHeight, left + view.getMeasuredWidth(), top + view.getMeasuredHeight());
                    left = left + view.getMeasuredWidth() + horizontalSpace;
                }
            }
        }

        public void setMaxLineCount(int maxLineCount) {
            mMaxLineCount = maxLineCount;
        }

        public int getMaxLineCount() {
            return mMaxLineCount;
        }

        public void setHorizontalSpace(int horizontalSpace) {
            this.horizontalSpace = horizontalSpace;
        }

        public void setVerticalSpace(int verticalSpace) {
            this.verticalSpace = verticalSpace;
        }
    }

总结

自定义流式布局可以让我们更好的理解MeasureSpec到底是干什么的。

三种模式:

MeasureSpec.EXACTLY 精确数值模式 01

MeasureSpec.AT_MOST 最大值模式 10

MeasureSpec.UNSPECIFIED 未限制模式 00

与在xml中的 xxxdp,match-parent,wrap-content都有什么联系呢?

我们在onMeasure(int widthMeasureSpec, int heightMeasureSpec)中拿到的widthMeasureSpec与heightMeasureSpec实际上都包含了父控件传进来的两个参数,一个是父控件提供的大小(size),另一个是父控件对这个size的规定(mode)。

  • exactly 表示size是精确值,子控件必须使用这个size作为width/height;
  • at_most 表示size是子控件最大的值,子控件自己本身有一个minSize,那么子控件的只能选择这两者中最小的那个值;
  • unspecified 表示子控件可以使用自己的minSize无论这个值是是怎样的,所以很多时候都会使用measure(0,0)的方式去手动测量一个布局的宽高,因为我们测量时一般无法给子控件一个确切的size;

所以流式布局中大量的使用MeasureSpec去测量子控件的宽高,实际上就是在提醒我们这些属性的含义。

2、属性动画的使用


下面对属性进行简单的使用,主要使用的是ValueAnimation.ofObject(),因为前面没有学过属性动画,所以这里简单学习一下,ofObject的重点是TypeEvaluator的实现。

先上效果图:

代码如下:

    /**
    * 这仍然是封装的一个Fragement,使用仍然是封装好的BaseFragment,ViewHolder,BaseProtocol
    */
    public class RankFragment extends BaseFragment {

        private RankProtocol mRankProtocol;
        private List<String> mData;
        private ScrollView mScrollView;
        private MyFlowLayout mFlowLayout;

        @Override
        protected void initListener() {}

        @Override
        protected State loadData() {
            if (mRankProtocol == null)
                mRankProtocol = new RankProtocol();
            mData = mRankProtocol.getData(0);
            return checkLoad(mData);
        }

        @Override
        public View initSuccessLayout() {
            // 可滑动
            mScrollView = new ScrollView(mActivity);
            mScrollView.setVerticalScrollBarEnabled(false);
            // 流式布局
            mFlowLayout = new MyFlowLayout(mActivity);
            // 设置边距
            int padding = UIUtils.dip2px(10);
            mFlowLayout.setPadding(padding, 0, padding, 0);
            // 设置内部控件的边距
            mFlowLayout.setHorizontalSpace(padding);
            mFlowLayout.setVerticalSpace(padding);
            // 根据数据生成随机颜色Button
            for (int i = 0, size = mData.size(); i < size; i++) {
                int red = 100 + (int) (155 * Math.random());
                int blue = 100 + (int) (155 * Math.random());
                int green = 100 + (int) (155 * Math.random());
                int rgb = Color.rgb(red, blue, green);

                Drawable gradientDrawable = DrawableUtil.getSelector(rgb, Color.GRAY, padding);

                final Button button = new Button(mActivity);
                button.setPadding(padding, padding, padding, padding);
                button.setText(mData.get(i));
                button.setTextColor(Color.WHITE);
                button.setBackgroundDrawable(gradientDrawable);
                // button的点击事件
                button.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        Button btn = (Button) v;
                        ToastUtil.show(mActivity, btn.getText().toString());
                    }
                });
                // button的触摸事件
                button.setOnTouchListener(new MyOnTouchListener());
                mFlowLayout.addView(button);
            }

            mScrollView.addView(mFlowLayout);

            initListener();
            return mScrollView;
        }

        private class MyOnTouchListener implements View.OnTouchListener{
            private Point mUpP;
            private Point mStartP;
            private float mMoveY;
            private float mMoveX;
            private float mStartX;
            private float mStartY;

            /**
            * 触摸事件,实际上在之前的学习中已经练习了很多次,类似的事件处理方式,都有两种固定写法。
            * 第一种 计算移动点和初始按下点之间的偏移量。在layout中始终只计算初始布局位置加上偏移量。这种方法始终只计算两个点的偏移。
            * 第二种 计算每次移动点和上次点之间的偏移量。在layout中每次都计算上次布局位置加上这次的偏移量,这种写法,需要每次计算后都将 startX值更新为moveX。
            * 这里采用第一种。
            */
            @Override
            public boolean onTouch(final View v, MotionEvent event) {
                // 请求父控件不要拦截事件,通知viewpager不要拦截事件,这样就可以拖着控件左右移动
                mFlowLayout.requestDisallowInterceptTouchEvent(true);

                switch (event.getAction()) {
                    case MotionEvent.ACTION_DOWN:
                        mStartX = event.getRawX();
                        mStartY = event.getRawY();
                        // 记下控件原来的layout位置
                        mStartP = new Point(v.getLeft(), v.getTop());
                        break;
                    case MotionEvent.ACTION_MOVE:
                        mMoveX = event.getRawX();
                        mMoveY = event.getRawY();

                        int pathX = (int) (mMoveX - mStartX + 0.5f);
                        int pathY = (int) (mMoveY - mStartY + 0.5f);

                        v.layout(mStartP.x + pathX, mStartP.y + pathY, mStartP.x + pathX + v.getWidth(), mStartP.y + pathY + v.getHeight());
                        return true;
                    case MotionEvent.ACTION_UP:
                        // 记录松开时,控件所在的布局位置
                        mUpP = new Point(v.getLeft(), v.getTop());
                        /*
                        * 属性动画:使用属性动画,让控件在松手后回到原位
                        * 注意这里有bug,会出现多指点击后错位
                        */
                        ValueAnimator backAnim = ValueAnimator.ofObject(new
                        PointEvaluator(), mUpP, mStartP);
                        // 监听计算的结果来实时设置控件所在布局
                        backAnim.addUpdateListener(new 
                        ValueAnimator.AnimatorUpdateListener() {
                            public void onAnimationUpdate(ValueAnimator animation) {
                                Point p = (Point) animation.getAnimatedValue();
                                v.layout(p.x, p.y, p.x + v.getWidth(), p.y + v.getHeight());
                                v.invalidate();
                                LogUtils.i(p.toString());
                            }
                        });
                        // 设置动画时间
                        backAnim.setDuration(500);
                        backAnim.start();

                        break;
                }
                return false;
            }
        }

        /**
         * 定义的类型计算器,用于计算point,也是evaluator最简单的用法
         */
        private class PointEvaluator implements TypeEvaluator {
            @Override
            public Object evaluate(float fraction, Object startValue, Object endValue) {
                // fraction表示当前的进度
                Point start = (Point) startValue;
                Point end = (Point) endValue;
                // 这里要返回进度对应的位置,所以不要忘记加上起点的位置,只返回两者之差是错误的!
                int pathX = start.x + (int) (fraction * (end.x - start.x));
                int pathY = start.y + (int) (fraction * (end.y - start.y));
                return new Point(pathX, pathY);
            }
        }
    }
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值