最近在學習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)支持滑動,並處理多指觸摸可能產生的跳動問題)