自定义流式布局的代码实现

自定义流式布局具体流程:

  1. 自定义属性:声明、设置、解析获取自定义值
    1. attr.xml中声明
<resources>
    <declare-styleable name="FlowLayout">
        <attr name="android:gravity"/>
        <attr name="android:horizontalSpacing" format="dimension|reference"/>
    </declare-styleable>
    <declare-styleable name="FlowLayout_Layout">
        <attr name="android:gravity"/>
    </declare-styleable>
</resources>

​ 2. 自定义属性:引入命名空间

xmlns:app="http://schemas.android.com/apk/res-auto"

​ 3. 解析:构造方法中

TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.FlowLayout_Layout);
try {
    gravity = a.getInt(R.styleable.FlowLayout_Layout_android_layout_gravity, -1);
}finally {
    //释放
    a.recycle();
}

重写generateLayoutParams、generateDefaultLayoutParams、checkLayoutParams

  1. 测量:在onMeasure MeasureSpce.AT_MOST/EXACTLY
    1. 自身的宽高/child的宽高
    2. FlowLayout本身宽高AT_MOST
    3. layout_width:wrap_content:给默认值,宽度最大
    4. AT_MOST: 所有的行数的累加的高度
    5. 子view:layout_height wrap_content child layout_height match_parent

​ 这一行里面高度最大的

  1. 布局:在onLayout方法里根据自己的规则确定child的位置

  2. 绘制:onDraw

  3. 处理LayoutParams

  4. 触摸反馈:滑动事件

    1. 通过view的ScrollBy和ScrollTo方法滑动
    2. 通过动画给view添加位移效果实现滑动
    3. 通过改变view的layoutParams 让view重新布局

具体代码:

package com.edu.cdut.rxjavatest;

import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.widget.OverScroller;
import android.widget.Scroller;

import androidx.core.view.ViewConfigurationCompat;

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

public class FlowLayout extends ViewGroup {

    private static final String TAG = FlowLayout.class.getSimpleName();

    private List<View> lineViews;//每一行的子view
    private List<List<View>> views;//所有的行, 一行一行的存
    private List<Integer> heights;//每一行的高度

    private boolean scrollable = false;
    private int measureHeight;//本身的测量高度
    private int realHeight;//内容的高度

    private int mTouchSlop;//判断是不是一次滑动
    private float mLastInterceptX = 0;
    private float mLastInterceptY = 0;
    private float mLastY = 0;

    private OverScroller mScroller;
    private VelocityTracker mVelocityTracker;
    private int mMaxVelocity;
    private int mMinVelocity;

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

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

    public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        ViewConfiguration configuration = ViewConfiguration.get(context);
        mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration);
        mScroller = new OverScroller(context);
    }

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

    private void initVelocityTrackerIfNotExists() {
        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
        }
    }

    private void recycleVelocityTrackerIfNotExists() {
        if (mVelocityTracker != null) {
            mVelocityTracker.recycle();
            mVelocityTracker = null;
        }
    }

    private void init(){
        views = new ArrayList<>();
        lineViews = new ArrayList<>();
        heights = new ArrayList<>();
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {

        boolean intercepted = false;
        float xIntercept = ev.getX();
        float yIntercept = ev.getY();
        switch (ev.getAction()){
            case MotionEvent.ACTION_DOWN:
                intercepted = false;
                mLastInterceptY = yIntercept;
                mLastInterceptX = xIntercept;
                break;
            case MotionEvent.ACTION_MOVE:
                float dx = xIntercept - mLastInterceptX;
                float dy = yIntercept - mLastInterceptY;
                if (Math.abs(dy) > Math.abs(dx) && Math.abs(dy) > mTouchSlop){
                    intercepted = true;
                }else {
                    intercepted = false;
                }
                break;
            case MotionEvent.ACTION_UP:
                intercepted = false;
                break;
        }
        mLastInterceptY = yIntercept;
        mLastInterceptX = xIntercept;
        return intercepted;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        if (!scrollable) {
            return super.onTouchEvent(event);
        }
        initVelocityTrackerIfNotExists();
        mVelocityTracker.addMovement(event);
        float curY = event.getY();
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                if (mScroller.isFinished()) {
                    mScroller.abortAnimation();
                }
                mLastY = curY;
                break;
            case MotionEvent.ACTION_MOVE:
                float dy = mLastY - curY;//本次滑动距离
                int oldScrollY = getScrollY();//已经偏移的距离
//                int scrollY = oldScrollY + (int) dy;//本次需要偏移的距离 = 之前已经偏移的距离 + 本次手势滑动的距离
//                if (scrollY < 0) {
//                    scrollY = 0;
//                }
//                if (scrollY > realHeight - measureHeight) {
//                    scrollY = realHeight - measureHeight;
//                }
//                scrollTo(0, scrollY);
                mScroller.startScroll(0, mScroller.getFinalY(), 0, (int) dy);//mCurrY = oldScrollY = dy * scale
                invalidate();//触发computeScroll
                mLastY = curY;
                break;
            case MotionEvent.ACTION_UP:
                final VelocityTracker velocityTracker = mVelocityTracker;
                velocityTracker.computeCurrentVelocity(1000, mMaxVelocity);
                int initialVelocity = (int) velocityTracker.getYVelocity();

                if (Math.abs(initialVelocity) > mMinVelocity) {
                    fling(-initialVelocity);
                }else if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0,
                        (realHeight - measureHeight))) {
                    postInvalidateOnAnimation();
                }
                break;
        }

        return super.onTouchEvent(event);
    }

    private void fling(int velocityY) {
        if (getChildCount() > 0) {
            int height = measureHeight;
            int bottom = realHeight;

            mScroller.fling(getScrollX(), getScrollY(),
                    0, velocityY, 0, 0, 0,
                    Math.max(0, bottom - height), 0, height / 2);
            postInvalidateOnAnimation();
        }
    }

    @Override
    public void computeScroll() {
        super.computeScroll();
        if (mScroller.computeScrollOffset()) {//mCurrY = oldScrollY = dy * scale
//            int scrollY = oldScrollY + (int) dy;//本次需要偏移的距离 = 之前已经偏移的距离 + 本次手势滑动的距离
//            if (scrollY < 0) {
//                scrollY = 0;
//            }
//            if (scrollY > realHeight - measureHeight) {
//                scrollY = realHeight - measureHeight;
//            }
//            scrollTo(0, scrollY);
            scrollTo(0, mScroller.getCurrY());
            postInvalidate();
        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //1. 测量自身
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //2. 为每一个子view计算测量的限制信息 Mode / Size
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        measureHeight = heightSize;
        //当前行的宽高
        int lineWidth = 0;//子view的宽度之和
        int lineHeight = 0;//子view的高度中的最大值

        //整个流式布局的
        int flowLayoutWidth = 0;//所有行宽度中的最大值
        int flowLayoutHeight = 0;//所有行的高度的累加

        //初始化参数列表
        init();
        //遍历所有的子view,对子view测量,分配到具体的行
        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            //测量子view,获取当前子view测量出来的宽高
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
            //获取当前子view测量出来的宽高
            int childWidth = child.getMeasuredWidth();
            int childHeight = child.getMeasuredHeight();

            LayoutParams lp = (LayoutParams) child.getLayoutParams();
            //看一下当前行剩余宽度是否可以容纳下一个子view
            //如果放不下就换行,保存当前行的所有的子view并累加高,然后当前宽度高度置零
            if (lineWidth + childWidth > widthSize) {
                if (lineViews.size() == 1 && lineViews.get(0).getLayoutParams().height == LayoutParams.MATCH_PARENT) {
                    lineHeight = Utils.dp2px(150);
                }
                views.add(lineViews);
                lineViews = new ArrayList<>();//创建新的一行
                flowLayoutWidth = Math.max(flowLayoutWidth, lineWidth);
                flowLayoutHeight += lineHeight;
                heights.add(lineHeight);
                lineWidth = 0;
                lineHeight = 0;
            }
            lineViews.add(child);
            lineWidth += childWidth;
            if (lp.height != LayoutParams.MATCH_PARENT){//height == match_parent不处理
                lineHeight = Math.max(childHeight, lineHeight);
            }

           if (i == childCount - 1) {
               flowLayoutHeight += lineHeight;
               flowLayoutWidth = Math.max(flowLayoutWidth, lineWidth);
               heights.add(lineHeight);
               views.add(lineViews);
           }
        }

        remeasureChild(widthMeasureSpec, heightMeasureSpec);

        if (heightMode == MeasureSpec.EXACTLY) {
            flowLayoutHeight = Math.max(heightSize, flowLayoutHeight);
        }

        realHeight = flowLayoutHeight;
        scrollable = realHeight > measureHeight;
        //FlowLayout最终宽高
        setMeasuredDimension((widthMode == MeasureSpec.EXACTLY) ? widthSize : flowLayoutWidth,
                (heightMode == MeasureSpec.EXACTLY) ? heightSize : flowLayoutHeight);
    }

    //重新测量一次layout 当height == match_parent
    private void remeasureChild(int widthMeasureSpec, int heightMeasureSpec){
        int lineSize = views.size();
        for (int i = 0; i < lineSize; i++) {
            int lineHeight = heights.get(i);//每一行的高
            List<View> lineViews = views.get(i);//每一行的子view
            int size = lineViews.size();
            for (int j = 0; j < size; j++) {
                View child = lineViews.get(j);
                LayoutParams lp = (LayoutParams) child.getLayoutParams();
                if (lp.height == LayoutParams.MATCH_PARENT){
                    int childWidthSpec = getChildMeasureSpec(widthMeasureSpec, 0, lp.width);
                    int childHeightSpec = getChildMeasureSpec(heightMeasureSpec, 0, lp.height);
                    child.measure(childWidthSpec, childHeightSpec);
                }
            }

        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int childCount = views.size();
        int curX = 0;
        int curY = 0;

        for (int i = 0; i < childCount; i++) {
            List<View> lineView  = views.get(i);
            int lineHeight = heights.get(i);
            //遍历当前行的子view
            int size = lineView.size();
            for (int j = 0; j < size; j++) {//布局当前行的每一个view
                View child = lineView.get(j);
                int left = curX;
                int top = curY;
                int right = left + child.getMeasuredWidth();
                int bottom = top + child.getMeasuredHeight();
                child.layout(left, top, right, bottom);
                //确定下一个view的left值
                curX += child.getMeasuredWidth();
            }
            curY += lineHeight;
            curX = 0;
        }
    }

    //对传入的lp进行转化--xml
    @Override
    public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
        return super.generateLayoutParams(attrs);
    }

    //对传入的lp进行转化--自定义
    @Override
    protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
        return super.generateLayoutParams(p);
    }

    //生成默认lp
    @Override
    protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
        return super.generateDefaultLayoutParams();
    }

    //检查lp是否合法
    @Override
    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
        return super.checkLayoutParams(p);
    }

    public static class LayoutParams extends MarginLayoutParams{

        public int gravity = -1;

        public LayoutParams(Context c, AttributeSet attrs) {
            super(c, attrs);

            TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.FlowLayout_Layout);
            try {
                gravity = a.getInt(R.styleable.FlowLayout_Layout_android_layout_gravity, -1);
            }finally {
                //释放
                a.recycle();
            }
        }

        public LayoutParams(int width, int height) {
            super(width, height);
        }

        public LayoutParams(MarginLayoutParams source) {
            super(source);
        }

        public LayoutParams(ViewGroup.LayoutParams source) {
            super(source);
        }
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值