自定义流式布局具体流程:
- 自定义属性:声明、设置、解析获取自定义值
- 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
- 测量:在onMeasure MeasureSpce.AT_MOST/EXACTLY
- 自身的宽高/child的宽高
- FlowLayout本身宽高AT_MOST
- layout_width:wrap_content:给默认值,宽度最大
- AT_MOST: 所有的行数的累加的高度
- 子view:layout_height wrap_content child layout_height match_parent
这一行里面高度最大的
-
布局:在onLayout方法里根据自己的规则确定child的位置
-
绘制:onDraw
-
处理LayoutParams
-
触摸反馈:滑动事件
- 通过view的ScrollBy和ScrollTo方法滑动
- 通过动画给view添加位移效果实现滑动
- 通过改变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);
}
}
}