#### **六、自定義ViewGroup**
ViewGroup存在的目的就是為了對其子view進行管理,為其子view添加顯示、響應的規則,因此,自定義ViewGroup通常需要重寫onMeasure()方法來對子view進行測量,重寫onLayout()方法來確定子view的位置,重寫onTouchEvent()來增加響應事件。
實現類似于原生控件ScrollView的自定義ViewGroup,這個“ScrollView”除了可以上下滑動,還有一個粘性的效果:即一個子view向上滑動大于一定的距離后,松開手指,它將自動向上滑動,顯示下一個子view,同理,如果滑動距離小于一定的距離,松開手指,它將自動滑動到開始的位置。
如下圖所示
:-: 
圖12 自定義ScrollView
示例:[自定義ScrollView](https://github.com/xuyisheng/AndroidHeroes/blob/master/3.Android控件架構/SystemWidget/app/src/main/java/com/imooc/systemwidget/MyScrollView.java)
代碼如下所示
~~~
/**
* 彈性scroll,滑動過程增加一個粘性效果,當一個子view向上滑動大于一定距離后,松開手指,它將自動向上滑動顯示下一個子view
* 同理,小于一定距離,松開手指,將滑動到開始的位置
*/
public class MyScrollView extends ViewGroup {
private int mScreenHeight;
private Scroller mScroller;
private int mLastY;
private int mStart;//觸摸起點
private int mEnd;//觸摸終點
public MyScrollView(Context context) {
super (context);
initView (context);
}
public MyScrollView(Context context, AttributeSet attrs) {
super (context, attrs);
initView (context);
}
public MyScrollView(Context context, AttributeSet attrs,
int defStyleAttr) {
super (context, attrs, defStyleAttr);
initView (context);
}
private void initView(Context context) {
WindowManager wm = (WindowManager) context.getSystemService (
Context.WINDOW_SERVICE);
DisplayMetrics dm = new DisplayMetrics ();
wm.getDefaultDisplay ().getMetrics (dm);
mScreenHeight = dm.heightPixels;
mScroller = new Scroller (context);
}
@Override
protected void onLayout(boolean changed,
int l, int t, int r, int b) {
int childCount = getChildCount ();
// 設置ViewGroup的高度,每個子view占一屏的高度,
MarginLayoutParams mlp = (MarginLayoutParams) getLayoutParams ();
mlp.height = mScreenHeight * childCount;
setLayoutParams (mlp);
//調用子view的layout方法,設定每一個子view需要放置的位置
for (int i = 0; i < childCount; i++) {
View child = getChildAt (i);
if (child.getVisibility () != View.GONE) {
child.layout (l, i * mScreenHeight,
r, (i + 1) * mScreenHeight);
//修改每一個子view的top和bottom屬性
}
}
}
//使用遍歷的方式來通知子view對自身進行測量
@Override
protected void onMeasure(int widthMeasureSpec,
int heightMeasureSpec) {
super.onMeasure (widthMeasureSpec, heightMeasureSpec);
int count = getChildCount ();
for (int i = 0; i < count; ++i) {
View childView = getChildAt (i);
measureChild (childView, widthMeasureSpec, heightMeasureSpec);
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int y = (int) event.getY ();//獲取點擊事件距離控件左邊的距離
switch (event.getAction ()) {
case MotionEvent.ACTION_DOWN:
mLastY = y;
//記錄觸摸起點
mStart = getScrollY ();
break;
//ACTION_MOVE事件中,計算dy,手指滑動的時候,調用scrollBy()方法,讓viewGroup的所有子view跟著滾動dy
case MotionEvent.ACTION_MOVE:
if (!mScroller.isFinished ()) {
mScroller.abortAnimation ();
}
int dy = mLastY - y;
if (getScrollY () < 0) {
dy = 0;
}
if (getScrollY () > getHeight () - mScreenHeight) {
dy = 0;
}
scrollBy (0, dy);
mLastY = y;
break;
//ACTION_UP事件中,判斷手指滑動的距離,若超過一定距離,調用scroller類來平滑移動到下一個子view,
// 否則滾到原來位置
case MotionEvent.ACTION_UP:
int dScrollY = checkAlignment ();
if (dScrollY > 0) {//向下滑動
//超過屏幕高度的1/3,則展示上一張圖片;沒有則使上一張圖片返回去,顯示還是當前圖片
if (dScrollY < mScreenHeight / 3) {
mScroller.startScroll (
0, getScrollY (),
0, -dScrollY);
} else {
mScroller.startScroll (
0, getScrollY (),
0, mScreenHeight - dScrollY);
}
} else {//向上滑動
if (-dScrollY < mScreenHeight / 3) {
mScroller.startScroll (
0, getScrollY (),
0, -dScrollY);
} else {
mScroller.startScroll (
0, getScrollY (),
0, -mScreenHeight - dScrollY);
}
}
break;
}
postInvalidate ();
return true;
}
private int checkAlignment() {
int mEnd = getScrollY ();
boolean isUp = ((mEnd - mStart) > 0) ? true : false;
int lastPrev = mEnd % mScreenHeight;
int lastNext = mScreenHeight - lastPrev;
if (isUp) {
//向下的
return lastPrev;
} else {
return -lastNext;
}
}
//重寫computeScroll()方法,實現模擬滑動,系統在繪制view的時候,會在draw()方法中調用該方法
//通常可以以以下代碼作為模板,詳情見群英傳 P97
@Override
public void computeScroll() {
super.computeScroll ();
//computeScrollOffset ()來判斷是否完成了滑動,false,中斷循環,完成平移移動過程
if (mScroller.computeScrollOffset ()) {
scrollTo (0, mScroller.getCurrY ());
//通過不斷重繪來不斷調用computeScroll()方法,因為只能在computeScroll()方法才能獲得模擬滑動中
// 的scroll坐標,但是computeScrollOffset ()不會自動調用
postInvalidate ();
}
}
}
~~~
**分析如下:**
首先,讓ViewGroup能夠實現類似ScrollView的功能,當然在ViewGroup能滾動之前,需要先放置好它的子view,使用遍歷的方式來通知子view對自身進行測量
~~~
//使用遍歷的方式來通知子view對自身進行測量
@Override
protected void onMeasure(int widthMeasureSpec,
int heightMeasureSpec) {
super.onMeasure (widthMeasureSpec, heightMeasureSpec);
int count = getChildCount ();
for (int i = 0; i < count; ++i) {
View childView = getChildAt (i);
measureChild (childView, widthMeasureSpec, heightMeasureSpec);
}
}
~~~
接著,就要對子view進行位置的設定。讓每個子view都顯示完整的一屏(這樣在滑動的時候,可以比較較好地實現后面的效果),因此viewgroup的高度就是子view的個數乘以屏幕的高度,如下代碼確定整個ViewGroup的高度。
~~~
// 設置ViewGroup的高度,每個子view占一屏的高度,
MarginLayoutParams mlp = (MarginLayoutParams) getLayoutParams ();
mlp.height = mScreenHeight * childCount;
setLayoutParams (mlp);
~~~
在獲取了整個ViewGroup的高度后,就可以通過遍歷來設定每個子view需要放置的位置了,直接通過調用子view的layout方法,并將具體的位置傳遞進去即可。代碼中主要是修改每個子view的top和bottom這2個屬性,讓他們可以依次排列。
~~~
//調用子view的layout方法,設定每一個子view需要放置的位置
for (int i = 0; i < childCount; i++) {
View child = getChildAt (i);
if (child.getVisibility () != View.GONE) {
child.layout (l, i * mScreenHeight,
r, (i + 1) * mScreenHeight);
//修改每一個子view的top和bottom屬性
}
}
~~~
通過上面的操作,我們已經可以將子view放置到ViewGroup中了,但此時的ViewGroup還不能夠響應任何觸控事件,自然也不能滑動,因此我們需要重寫onTouchEvent()方法,為ViewGroup添加響應事件。在ViewGroup中添加滑動事件,通常可以使用scrollBy()方法來輔助滑動。在onTouchEvent()的ACTION_MOVE事件中,只要使用scrollBy(0,dy)方法,讓手指滑動的時候讓ViewGroup中的所有子view也跟著滑動dy即可,計算dy的方法有很多,如下代碼就是一種思路
~~~
@Override
public boolean onTouchEvent(MotionEvent event) {
int y = (int) event.getY ();//獲取點擊事件距離控件左邊的距離
switch (event.getAction ()) {
case MotionEvent.ACTION_DOWN:
mLastY = y;
//記錄觸摸起點
mStart = getScrollY ();
break;
//ACTION_MOVE事件中,計算dy,手指滑動的時候,調用scrollBy()方法,讓viewGroup的所有子view跟著滾動dy
case MotionEvent.ACTION_MOVE:
if (!mScroller.isFinished ()) {
mScroller.abortAnimation ();
}
int dy = mLastY - y;
if (getScrollY () < 0) {
dy = 0;
}
if (getScrollY () > getHeight () - mScreenHeight) {
dy = 0;
}
scrollBy (0, dy);
mLastY = y;
break;
//ACTION_UP事件中,判斷手指滑動的距離,若超過一定距離,調用scroller類來平滑移動到下一個子view,
// 否則滾到原來位置
case MotionEvent.ACTION_UP:
int dScrollY = checkAlignment ();
if (dScrollY > 0) {//向下滑動
//超過屏幕高度的1/3,則展示上一張圖片;沒有則使上一張圖片返回去,顯示還是當前圖片
if (dScrollY < mScreenHeight / 3) {
mScroller.startScroll (
0, getScrollY (),
0, -dScrollY);
} else {
mScroller.startScroll (
0, getScrollY (),
0, mScreenHeight - dScrollY);
}
} else {//向上滑動
if (-dScrollY < mScreenHeight / 3) {
mScroller.startScroll (
0, getScrollY (),
0, -dScrollY);
} else {
mScroller.startScroll (
0, getScrollY (),
0, -mScreenHeight - dScrollY);
}
}
break;
}
postInvalidate ();
return true;
}
~~~
最后實現這個粘性效果,要實現該效果,自然想到了onTouchEvent()的ACTION_UP事件和Scroll類,在ACTION_UP事件中判斷手指滑動的距離,如果超過一定的距離,則使用Scroller類來平滑移動到下一個view,如果下雨一定距離,則滾回到原來的位置。
當然最后不要忘記加上computeScroll(),
~~~
//重寫computeScroll()方法,實現模擬滑動,系統在繪制view的時候,會在draw()方法中調用該方法
//通常可以以以下代碼作為模板,詳情見群英傳 P97
@Override
public void computeScroll() {
super.computeScroll ();
//computeScrollOffset ()來判斷是否完成了滑動,false,中斷循環,完成平移移動過程
if (mScroller.computeScrollOffset ()) {
scrollTo (0, mScroller.getCurrY ());
//通過不斷重繪來不斷調用computeScroll()方法,因為只能在computeScroll()方法才能獲得模擬滑動中
// 的scroll坐標,但是computeScrollOffset ()不會自動調用
postInvalidate ();
}
}
~~~