實現效果
1、可以根據子View的寬度自動換行
2、子View的高度超過layout的大小時可以滑動
3、根據需要設置子View的Gravity
4、如果需要,可以使用LayoutTransition設置子View添加刪除時的動畫效果
實現自動換行以及可自定義Gravity
實現一個可以自動換行的Flowlayout。
1、onMeasure
遍歷子View測量大小,算出每一行的寬度以及總的高度和寬度。
當對齊方式不是 top|left 時需要根據總的高度和每一行的寬度來決定子View應該放置在哪裏;當layout的寬高設置是wrap_contant時可以根據總寬高設置大小。
累加子View的寬度,如果超過layout的寬度,判斷爲需要換行,將當前行中子View的最大高度記爲當前行的高度,總高度累加。
代碼:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
mLineWidthList.clear();
totalLineHeight = 0;
int totalWidth = 0;
int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);
int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
int modeHeight = MeasureSpec.getMode(heightMeasureSpec);
//去掉padding和margin後實際的寬和高
int parentUsableWidth = sizeWidth - getPaddingLeft() - getPaddingRight();
int parentUsableHeight = sizeHeight - getPaddingTop() - getPaddingBottom();
Log.e(TAG, "parentUsableWidth: " + parentUsableWidth + ", parentUsableHeight: " + parentUsableHeight);
int currentLineWidth = 0;
int currentLineHeight = 0;
int childCount = getChildCount();
if (childCount <= 0) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
return;
}
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
//測量子View
measureChild(childView, widthMeasureSpec, heightMeasureSpec);
Log.d(TAG, String.format("measureChildWithMargins(%s,%s,%s,%s)", widthMeasureSpec, currentLineWidth, heightMeasureSpec, totalLineHeight));
//帶Margin的
MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams();
int childWidth = childView.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
int childHeight = childView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
Log.d(TAG, "childWidth: " + childWidth + ", childHeight: " + childHeight +
String.format(", child margin:%s,%s,%s,%s", lp.leftMargin, lp.rightMargin, lp.topMargin, lp.bottomMargin));
if (currentLineWidth + childWidth > parentUsableWidth) {
//換行處理
Log.v(TAG, "換行," + (currentLineWidth + childWidth) + "," + parentUsableWidth);
totalLineHeight += currentLineHeight; //累加高度
mLineWidthList.add(currentLineWidth); //記錄當前行寬
totalWidth = Math.max(currentLineWidth, totalWidth);
currentLineWidth = childWidth;
currentLineHeight = childHeight;
} else {
currentLineWidth += childWidth;
currentLineHeight = Math.max(childHeight, currentLineHeight);
}
//遍歷到最後一個ChildView時未換行, 單獨處理
if (i == childCount - 1) {
Log.v(TAG, "換行," + (currentLineWidth + childWidth) + "," + parentUsableWidth);
totalLineHeight += currentLineHeight;
mLineWidthList.add(currentLineWidth);
totalWidth = Math.max(currentLineWidth, totalWidth);
}
//根據需要,是否要將子View總的寬高設爲layout的寬高
setMeasuredDimension(modeWidth == MeasureSpec.EXACTLY ? sizeWidth : totalWidth + getPaddingLeft() + getPaddingRight(),
modeHeight == MeasureSpec.EXACTLY ? sizeHeight : totalLineHeight + getPaddingTop() + getPaddingBottom());
}
}
2、onLayout
使用onMeasure的測量結果根據設定的對齊方式在正確的位置放置子View。
1)對齊方式
對齊方式在attrs.xml裏面使用枚舉類型,安卓的Gravity類中幾個值爲:
CENTER = 17; //0001 0001
CENTER_VERTICAL = 16; //0001 0000
CENTER_HORIZONTAL = 1; //0000 0001
LEFT = 3; //0000 0011
RIGHT = 5; //0000 0101
BOTTOM = 80; //0101 0000
TOP = 48; //0011 0000
attrs.xml裏面這麼寫
<declare-styleable name="flowlayout">
<!--<attr name="child_gravity" format="integer"/>-->
<attr name="child_gravity">
<enum name="center" value="17" />
<enum name="center_vertical" value="16" />
<enum name="center_horizontal" value="1" />
<enum name="left" value="3" />
<enum name="right" value="5" />
<enum name="top" value="48" />
<enum name="bottom" value="80" />
</attr>
</declare-styleable>
可以看出,只要將在佈局xml的標籤屬性中的到的實際的gravity值與Gravity類中這些值相與,判斷結果和這些值相不相等就可以知道設置的是哪個值了。
從設計的值和實際道理上可以看出,CENTER_VERTICAL跟BOTTOM、TOP會衝突,而且CENTER的優先級肯定是要低的,只要設置了bottom或是top,垂直居中就會無效。
處理的代碼入下:
/**
* 根據對齊方式計算當前行最左上角那個點的座標
* @param lineWidth 當前行寬
* @return Point x代表當前行的起始的橫座標(left),y代表當前行頂部的座標(top)
*/
private Point getCurrentTopLeft(int parentLeft, int parentTop, int parentHeight, int parentWidth, int lineWidth) {
Log.d(TAG, String.format("getCurrentTopLeft(%s,%s,%s,%s,%s) ", parentLeft, parentTop, parentHeight, parentWidth, lineWidth) + "totalLineHeight:" + totalLineHeight);
Point point = new Point(getPaddingLeft(), getPaddingTop());
if ((mChildGravity & Gravity.CENTER) == Gravity.CENTER) {
Log.d(TAG, "child gravity = CENTER");
point.x = (parentWidth - lineWidth) / 2;
point.y = (parentHeight - totalLineHeight) / 2;
}
if ((mChildGravity & Gravity.CENTER_VERTICAL) == Gravity.CENTER_VERTICAL) {
Log.d(TAG, "child gravity = CENTER_VERTICAL");
point.y = (parentHeight - totalLineHeight) / 2;
}
if ((mChildGravity & Gravity.CENTER_HORIZONTAL) == Gravity.CENTER_HORIZONTAL) {
Log.d(TAG, "child gravity = CENTER_HORIZONTAL");
point.x = (parentWidth - lineWidth) / 2;
}
if ((mChildGravity & Gravity.LEFT) == Gravity.LEFT) {
Log.d(TAG, "child gravity = LEFT");
point.x = getPaddingLeft();
}
if ((mChildGravity & Gravity.RIGHT) == Gravity.RIGHT) {
Log.d(TAG, "child gravity = RIGHT");
point.x = parentWidth - getPaddingLeft() - lineWidth;
}
if ((mChildGravity & Gravity.BOTTOM) == Gravity.BOTTOM) {
Log.d(TAG, "child gravity = BOTTOM");
point.y = parentHeight - totalLineHeight;
}
if ((mChildGravity & Gravity.TOP) == Gravity.TOP) {
Log.d(TAG, "child gravity = TOP");
point.y = getPaddingTop();
}
return point;
}
默認對齊爲左上,所以point初始爲(getPaddingLeft(), getPaddingTop()),處理垂直方向上的對齊是設置point.y, 水平就point.x。最後可以得到子View們整體的左上角的起始點。dian
2)放置子View
得到了每一行最左上角的點之後,就可以根據那個點來從左到右放置子View了:
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
//獲得在onMeasure方法中計算後得到的這個FlowLayout的寬度, 用的currentRight包含了getPaddingLeft,所以可用大小要加回去
int parentUsableWidth = getWidth() - getPaddingLeft() - getPaddingRight() + getPaddingLeft();
Log.e(TAG, "layout parentUsableWidth: " + parentUsableWidth);
int childCount = getChildCount();
if (childCount <= 0) {
//super是個抽象方法。。
return;
}
int currLine = 0; //當前行遊標
Point point = getCurrentTopLeft(getLeft(), getTop(), getHeight(), getWidth(), mLineWidthList.get(currLine++));
int currentRight = point.x; //代表左上角點的兩個座標
int currentTop = point.y;
int currentLineHeight = 0;
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams();
int childWidth = childView.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
int childHeight = childView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
if (currentRight + childWidth > parentUsableWidth) {
//換行處理
currentTop += currentLineHeight;
currentRight = getCurrentTopLeft(getLeft(), getTop(), getHeight(), getWidth(), mLineWidthList.get(currLine++)).x;
currentLineHeight = 0;
}
Log.d(TAG, "child layout, top: " + currentTop + ", left: " + currentRight);
childView.layout(currentRight + lp.leftMargin, currentTop + lp.topMargin,
currentRight + childWidth - lp.rightMargin, currentTop + childHeight - lp.bottomMargin);
//爲下一個view的layout設置當前狀態
currentLineHeight = Math.max(currentLineHeight, childHeight);
currentRight += childWidth;
}
//糾正一下子View的整體位置,當有上下滑動過,糾正位移是必要的
if (isViewAdded || isViewRemoved) {
correctScrollY();
isViewAdded = false;
isViewRemoved = false;
}
}
實現可滑動 Scrollable
1、控制觸摸事件的分發
如果手指滑動的垂直方向超過一定距離的話判斷爲需要scrollY並且阻止傳遞給子View,如果不是,觸摸事件正常傳遞。滑動距離的閾值可以這樣獲得:int touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.d(TAG, "onInterceptTouchEvent, ACTION_DOWN");
mLastMotionY = ev.getY();
scrollEnable = false;
case MotionEvent.ACTION_MOVE:
Log.d(TAG, "onInterceptTouchEvent, ACTION_MOVE");
if (Math.abs(mLastMotionY - ev.getY()) > touchSlop) {
Log.d(TAG, "onInterceptTouchEvent, return true");
return true; //不傳遞
}
}
return false;
}
2、滑動
滑動時,上下滑動不能無限制的任意上滑下滑,
當子View的總高度沒有超出layout的高度時不能滑動(或者可以滑,但是鬆手後要糾正scrollY),
當第一個子View的top比layout的top低時在鬆手後要糾正一下scrollY,最後一個子View的bottom也要注意。
onTouchEvent:
@Override
public boolean onTouchEvent(MotionEvent event) {
super.onTouchEvent(event);
float y = event.getY();
int firstChildTop = getChildAt(0).getTop();
int lastChildBottom = getChildAt(getChildCount() - 1).getBottom();
if (firstChildTop > 0 && lastChildBottom < getHeight()) { //子View沒有超出layout,不滑動
return true;
}
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.d(TAG, "onTouchEvent, ACTION_DOWN");
mLastMotionY = y; //記住開始落下的點
break;
case MotionEvent.ACTION_MOVE:
int detaY = (int) (mLastMotionY - y);
Log.d(TAG, "onTouchEvent ACTION_MOVE, detaY=" + detaY + ", touchSlop=" + touchSlop);
if (scrollEnable || Math.abs(detaY) > touchSlop) {
scrollBy(0, detaY);
mLastMotionY = y;
scrollEnable = true;
}
break;
case MotionEvent.ACTION_UP:
Log.d(TAG, "onTouchEvent ACTION_UP, firstChildTop=" + firstChildTop +
", lastChildBottom=" + lastChildBottom +
", ScrollY=" + getScrollY());
Point point = getCurrentTopLeft(getLeft(), getTop(), getHeight(), getWidth(), mLineWidthList.get(0));
if (firstChildTop - getScrollY() > 0) { //第一個子View的頂部在layout的top下
scrollTo(0, point.y);
} else if (lastChildBottom - getScrollY() < getHeight()) { //最後一個子View的底部在layout的bottom上
scrollTo(0, totalLineHeight - getHeight() + point.y);
}
}
return true;
}
糾正ScrollY的方法:
/**
* 在滑動過之後,添加或者刪除View之後子View整體的Y位移需要調整
* 要在onLayout之後用,不然拿到的top跟bottom是添加之前的值
*/
void correctScrollY() {
int firstChildTop = getChildAt(0).getTop();
int lastChildBottom = getChildAt(getChildCount() - 1).getBottom();
Log.d(TAG, "correctScrollY , firstChildTop=" + firstChildTop +
", lastChildBottom=" + lastChildBottom);
if (firstChildTop > 0 && lastChildBottom < getHeight()) {
Log.e(TAG, "correctScrollY");
scrollTo(0, 0);
} else {
Point p = getCurrentTopLeft(getLeft(), getTop(), getHeight(), getWidth(), mLineWidthList.get(0));
if (isViewAdded) {
//添加View時滑動到底部(因爲新View總是在底部)
scrollTo(0, totalLineHeight - getHeight() + p.y);
} else if (isViewRemoved && lastChildBottom - getScrollY() < getHeight()) {
//刪除了View後,如果最後一個子View的底部跟layout的底部之間有空隙,
//就讓最後一個子View的底部跟layout的底部對齊
scrollTo(0, totalLineHeight - getHeight() + p.y);
}
}
}
添加刪除View時的動畫
在構造函數中這樣:
mLayoutTransition = new LayoutTransition();
this.setLayoutTransition(mLayoutTransition);
然後就通過LayoutTransition設置動畫。動畫應該簡潔,僅僅起到不要讓view出來的太突兀的作用就好。