自定义流式布局
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);
}
}
}