自定義ViewGroup支持margin,gravity以及水平,垂直排列

最近在學習android的view部分,於是動手實現了一個類似ViewPager的可上下或者左右拖動的ViewGroup,中間遇到了一些問題(例如touchEvent在onInterceptTouchEvent和onTouchEvent之間的傳遞流程),現在將我的實現過程記錄下來。

首先,要實現一個ViewGroup,必須至少重寫onLayout()方法(當然還有構造方法啦:))。onLayout()主要是用來安排子View在我們這個ViewGroup中的擺放位置的。除了onLayout()方法之外往往還需要重寫onMeasure()方法,用於測算我們所需要佔用的空間。

首先,我們來重寫onMeasure()方法:(先只考慮水平方向)

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
	// 計算所有child view 要佔用的空間
	desireWidth = 0;
	desireHeight = 0;
	int count = getChildCount();
	for (int i = 0; i < count; ++i) {
		View v = getChildAt(i);
		if (v.getVisibility() != View.GONE) {
			measureChild(v, widthMeasureSpec,
					heightMeasureSpec);
			desireWidth += v.getMeasuredWidth();
			desireHeight = Math
					.max(desireHeight, v.getMeasuredHeight());
		}
	}

	// count with padding
	desireWidth += getPaddingLeft() + getPaddingRight();
	desireHeight += getPaddingTop() + getPaddingBottom();

	// see if the size is big enough
	desireWidth = Math.max(desireWidth, getSuggestedMinimumWidth());
	desireHeight = Math.max(desireHeight, getSuggestedMinimumHeight());

	setMeasuredDimension(resolveSize(desireWidth, widthMeasureSpec),
			resolveSize(desireHeight, heightMeasureSpec));
}
我們計算出所有Visilibity不是Gone的View的寬度的總和作爲viewgroup的最大寬度,以及這些view中的最高的一個作爲viewgroup的高度。這裏需要注意的是要考慮咱們viewgroup自己的padding。(目前先忽略子View的margin)。

onLayout():

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
	final int parentLeft = getPaddingLeft();
	final int parentRight = r - l - getPaddingRight();
	final int parentTop = getPaddingTop();
	final int parentBottom = b - t - getPaddingBottom();

	if (BuildConfig.DEBUG)
		Log.d("onlayout", "parentleft: " + parentLeft + "   parenttop: "
				+ parentTop + "   parentright: " + parentRight
				+ "   parentbottom: " + parentBottom);

	int left = parentLeft;
	int top = parentTop;

	int count = getChildCount();
	for (int i = 0; i < count; ++i) {
		View v = getChildAt(i);
		if (v.getVisibility() != View.GONE) {
			final int childWidth = v.getMeasuredWidth();
			final int childHeight = v.getMeasuredHeight();
				v.layout(left, top, left + childWidth, top + childHeight);
				left += childWidth;
		}
	}
}


上面的layout方法寫的比較簡單,就是簡單的計算出每個子View的left值,然後調用view的layout方法即可。

現在我們加上xml佈局文件,來看一下效果:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <com.example.testslidelistview.SlideGroup
        android:id="@+id/sl"
        android:layout_width="match_parent"
        android:layout_height="500dp"
        android:layout_marginTop="50dp"
        android:background="#FFFF00" >

        <ImageView
            android:id="@+id/iv1"
            android:layout_width="150dp"
            android:layout_height="300dp"
            android:scaleType="fitXY"
            android:src="@drawable/lead_page_1" />

        <ImageView
            android:layout_width="150dp"
            android:layout_height="300dp"
            android:scaleType="fitXY"
            android:src="@drawable/lead_page_2" />

        <ImageView
            android:layout_width="150dp"
            android:layout_height="300dp"
            android:scaleType="fitXY"
            android:src="@drawable/lead_page_3" />
    </com.example.testslidelistview.SlideGroup>

</LinearLayout>
效果圖如下:

從效果圖中我們看到,3個小圖連在一起(因爲現在不支持margin),然後我們也沒辦法讓他們垂直居中(因爲現在還不支持gravity)。

現在我們首先爲咱們的ViewGroup增加一個支持margin和gravity的LayoutParams。

@Override
	protected android.view.ViewGroup.LayoutParams generateDefaultLayoutParams() {
		return new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
				ViewGroup.LayoutParams.MATCH_PARENT);
	}

	@Override
	public android.view.ViewGroup.LayoutParams generateLayoutParams(
			AttributeSet attrs) {
		return new LayoutParams(getContext(), attrs);
	}

	@Override
	protected android.view.ViewGroup.LayoutParams generateLayoutParams(
			android.view.ViewGroup.LayoutParams p) {
		return new LayoutParams(p);
	}

	public static class LayoutParams extends MarginLayoutParams {
		public int gravity = -1;

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

			TypedArray ta = c.obtainStyledAttributes(attrs,
					R.styleable.SlideGroup);

			gravity = ta.getInt(R.styleable.SlideGroup_layout_gravity, -1);

			ta.recycle();
		}

		public LayoutParams(int width, int height) {
			this(width, height, -1);
		}

		public LayoutParams(int width, int height, int gravity) {
			super(width, height);
			this.gravity = gravity;
		}

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

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

xml的自定義屬性如下:
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <attr name="layout_gravity">
        <!-- Push object to the top of its container, not changing its size. -->
        <flag name="top" value="0x30" />
        <!-- Push object to the bottom of its container, not changing its size. -->
        <flag name="bottom" value="0x50" />
        <!-- Push object to the left of its container, not changing its size. -->
        <flag name="left" value="0x03" />
        <!-- Push object to the right of its container, not changing its size. -->
        <flag name="right" value="0x05" />
        <!-- Place object in the vertical center of its container, not changing its size. -->
        <flag name="center_vertical" value="0x10" />
        <!-- Place object in the horizontal center of its container, not changing its size. -->
        <flag name="center_horizontal" value="0x01" />
    </attr>
    
    <declare-styleable name="SlideGroup">
        <attr name="layout_gravity" />
    </declare-styleable>
</resources>

現在基本的準備工作差不多了,然後需要修改一下onMeasure()和onLayout()。

onMeasure():(上一個版本,我們在計算最大寬度和高度時忽略了margin)

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
	// 計算所有child view 要佔用的空間
	desireWidth = 0;
	desireHeight = 0;
	int count = getChildCount();
	for (int i = 0; i < count; ++i) {
		View v = getChildAt(i);
		if (v.getVisibility() != View.GONE) {

			LayoutParams lp = (LayoutParams) v.getLayoutParams();
			//將measureChild改爲measureChildWithMargin
			measureChildWithMargins(v, widthMeasureSpec, 0,
					heightMeasureSpec, 0);
			//這裏在計算寬度時加上margin
			desireWidth += v.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
			desireHeight = Math
					.max(desireHeight, v.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
		}
	}

	// count with padding
	desireWidth += getPaddingLeft() + getPaddingRight();
	desireHeight += getPaddingTop() + getPaddingBottom();

	// see if the size is big enough
	desireWidth = Math.max(desireWidth, getSuggestedMinimumWidth());
	desireHeight = Math.max(desireHeight, getSuggestedMinimumHeight());

	setMeasuredDimension(resolveSize(desireWidth, widthMeasureSpec),
			resolveSize(desireHeight, heightMeasureSpec));
}

onLayout()(加上margin和gravity)

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
	final int parentLeft = getPaddingLeft();
	final int parentRight = r - l - getPaddingRight();
	final int parentTop = getPaddingTop();
	final int parentBottom = b - t - getPaddingBottom();

	if (BuildConfig.DEBUG)
		Log.d("onlayout", "parentleft: " + parentLeft + "   parenttop: "
				+ parentTop + "   parentright: " + parentRight
				+ "   parentbottom: " + parentBottom);

	int left = parentLeft;
	int top = parentTop;

	int count = getChildCount();
	for (int i = 0; i < count; ++i) {
		View v = getChildAt(i);
		if (v.getVisibility() != View.GONE) {
			LayoutParams lp = (LayoutParams) v.getLayoutParams();
			final int childWidth = v.getMeasuredWidth();
			final int childHeight = v.getMeasuredHeight();
			final int gravity = lp.gravity;
			final int horizontalGravity = gravity
					& Gravity.HORIZONTAL_GRAVITY_MASK;
			final int verticalGravity = gravity
					& Gravity.VERTICAL_GRAVITY_MASK;

			left += lp.leftMargin;
			top = parentTop + lp.topMargin;
			if (gravity != -1) {
				switch (verticalGravity) {
				case Gravity.TOP:
					break;
				case Gravity.CENTER_VERTICAL:
					top = parentTop
							+ (parentBottom - parentTop - childHeight)
							/ 2 + lp.topMargin - lp.bottomMargin;
					break;
				case Gravity.BOTTOM:
					top = parentBottom - childHeight - lp.bottomMargin;
					break;
				}
			}

			if (BuildConfig.DEBUG) {
				Log.d("onlayout", "child[width: " + childWidth
						+ ", height: " + childHeight + "]");
				Log.d("onlayout", "child[left: " + left + ", top: "
						+ top + ", right: " + (left + childWidth)
						+ ", bottom: " + (top + childHeight));
			}
			v.layout(left, top, left + childWidth, top + childHeight);
			left += childWidth + lp.rightMargin;
			
		}
	}
}

現在修改一下xml佈局文件,加上例如xmlns:ly="http://schemas.android.com/apk/res-auto",的xml命名空間,來引用我們設置的layout_gravity屬性。(這裏的“res-auto”其實還可以使用res/com/example/testslidelistview來代替,但是前一種方法相對簡單,尤其是當你將某個ui組件作爲library來使用的時候)

現在的效果圖如下:有了margin,有了gravity。


其實在這個基礎上,我們可以很容易的添加一個方向屬性,使得它可以通過設置一個xml屬性或者一個java api調用來實現垂直排列。

下面我們增加一個用於表示方向的枚舉類型:

public static enum Orientation {
		HORIZONTAL(0), VERTICAL(1);
		
		private int value;
		private Orientation(int i) {
			value = i;
		}
		public int value() {
			return value;
		}
		public static Orientation valueOf(int i) {
			switch (i) {
			case 0:
				return HORIZONTAL;
			case 1:
				return VERTICAL;
			default:
				throw new RuntimeException("[0->HORIZONTAL, 1->VERTICAL]");
			}
		}
	}

然後我們需要改變onMeasure(),來正確的根據方向計算需要的最大寬度和高度。

@Override
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		// 計算所有child view 要佔用的空間
		desireWidth = 0;
		desireHeight = 0;
		int count = getChildCount();
		for (int i = 0; i < count; ++i) {
			View v = getChildAt(i);
			if (v.getVisibility() != View.GONE) {
				LayoutParams lp = (LayoutParams) v.getLayoutParams();
				measureChildWithMargins(v, widthMeasureSpec, 0,
						heightMeasureSpec, 0);

				//只是在這裏增加了垂直或者水平方向的判斷
				if (orientation == Orientation.HORIZONTAL) {
					desireWidth += v.getMeasuredWidth() + lp.leftMargin
							+ lp.rightMargin;
					desireHeight = Math.max(desireHeight, v.getMeasuredHeight()
							+ lp.topMargin + lp.bottomMargin);
				} else {
					desireWidth = Math.max(desireWidth, v.getMeasuredWidth()
							+ lp.leftMargin + lp.rightMargin);
					desireHeight += v.getMeasuredHeight() + lp.topMargin
							+ lp.bottomMargin;
				}
			}
		}

		// count with padding
		desireWidth += getPaddingLeft() + getPaddingRight();
		desireHeight += getPaddingTop() + getPaddingBottom();

		// see if the size is big enough
		desireWidth = Math.max(desireWidth, getSuggestedMinimumWidth());
		desireHeight = Math.max(desireHeight, getSuggestedMinimumHeight());

		setMeasuredDimension(resolveSize(desireWidth, widthMeasureSpec),
				resolveSize(desireHeight, heightMeasureSpec));
	}

onLayout():

@Override
	protected void onLayout(boolean changed, int l, int t, int r, int b) {
		final int parentLeft = getPaddingLeft();
		final int parentRight = r - l - getPaddingRight();
		final int parentTop = getPaddingTop();
		final int parentBottom = b - t - getPaddingBottom();

		if (BuildConfig.DEBUG)
			Log.d("onlayout", "parentleft: " + parentLeft + "   parenttop: "
					+ parentTop + "   parentright: " + parentRight
					+ "   parentbottom: " + parentBottom);

		int left = parentLeft;
		int top = parentTop;

		int count = getChildCount();
		for (int i = 0; i < count; ++i) {
			View v = getChildAt(i);
			if (v.getVisibility() != View.GONE) {
				LayoutParams lp = (LayoutParams) v.getLayoutParams();
				final int childWidth = v.getMeasuredWidth();
				final int childHeight = v.getMeasuredHeight();
				final int gravity = lp.gravity;
				final int horizontalGravity = gravity
						& Gravity.HORIZONTAL_GRAVITY_MASK;
				final int verticalGravity = gravity
						& Gravity.VERTICAL_GRAVITY_MASK;

				if (orientation == Orientation.HORIZONTAL) {
					// layout horizontally, and only consider vertical gravity

					left += lp.leftMargin;
					top = parentTop + lp.topMargin;
					if (gravity != -1) {
						switch (verticalGravity) {
						case Gravity.TOP:
							break;
						case Gravity.CENTER_VERTICAL:
							top = parentTop
									+ (parentBottom - parentTop - childHeight)
									/ 2 + lp.topMargin - lp.bottomMargin;
							break;
						case Gravity.BOTTOM:
							top = parentBottom - childHeight - lp.bottomMargin;
							break;
						}
					}

					if (BuildConfig.DEBUG) {
						Log.d("onlayout", "child[width: " + childWidth
								+ ", height: " + childHeight + "]");
						Log.d("onlayout", "child[left: " + left + ", top: "
								+ top + ", right: " + (left + childWidth)
								+ ", bottom: " + (top + childHeight));
					}
					v.layout(left, top, left + childWidth, top + childHeight);
					left += childWidth + lp.rightMargin;
				} else {
					// layout vertical, and only consider horizontal gravity

					left = parentLeft;
					top += lp.topMargin;
					switch (horizontalGravity) {
					case Gravity.LEFT:
						break;
					case Gravity.CENTER_HORIZONTAL:
						left = parentLeft
								+ (parentRight - parentLeft - childWidth) / 2
								+ lp.leftMargin - lp.rightMargin;
						break;
					case Gravity.RIGHT:
						left = parentRight - childWidth - lp.rightMargin;
						break;
					}
					v.layout(left, top, left + childWidth, top + childHeight);
					top += childHeight + lp.bottomMargin;
				}
			}
		}
	}

現在我們可以增加一個xml屬性:

<attr name="orientation">
            <enum name="horizontal" value="0" />
            <enum name="vertical" value="1" />
</attr>

現在就可以在佈局文件中加入ly:orientation="vertical"來實現垂直排列了(ly是自定義的xml命名空間)

佈局文件如下:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <com.example.testslidelistview.SlideGroup
        xmlns:gs="http://schemas.android.com/apk/res-auto"
        android:id="@+id/sl"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_marginTop="50dp"
        android:background="#FFFF00" >

        <ImageView
            android:id="@+id/iv1"
            android:layout_width="300dp"
            android:layout_height="200dp"
            android:layout_marginBottom="20dp"
            gs:layout_gravity="left"
            android:scaleType="fitXY"
            android:src="@drawable/lead_page_1" />

        <ImageView
            android:layout_width="300dp"
            android:layout_height="200dp"
            android:layout_marginBottom="20dp"
            gs:layout_gravity="center_horizontal"
            android:scaleType="fitXY"
            android:src="@drawable/lead_page_2" />

        <ImageView
            android:layout_width="300dp"
            android:layout_height="200dp"
            android:layout_marginBottom="20dp"
            gs:layout_gravity="right"
            android:scaleType="fitXY"
            android:src="@drawable/lead_page_3" />
    </com.example.testslidelistview.SlideGroup>

</LinearLayout>

現在效果圖如下:


現在基本上是實現了一個簡單的基於ViewGroup的layout,但是從上面的圖中可以看出,第三張都沒有顯示完整,那麼爲了能夠顯示更多的內容,我們需要支持滑動,那就涉及到onTouchEvent(),以及Scroller的使用,這些就在下一篇中記錄吧。。。自定義ViewGroup (2)支持滑動,並處理多指觸摸可能產生的跳動問題

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章