自定義流式佈局

自定義流式佈局

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);
            }
        }
    }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章