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