#### **六、自定义ViewGroup**
ViewGroup存在的目的就是为了对其子view进行管理,为其子view添加显示、响应的规则,因此,自定义ViewGroup通常需要重写onMeasure()方法来对子view进行测量,重写onLayout()方法来确定子view的位置,重写onTouchEvent()来增加响应事件。
实现类似于原生控件ScrollView的自定义ViewGroup,这个“ScrollView”除了可以上下滑动,还有一个粘性的效果:即一个子view向上滑动大于一定的距离后,松开手指,它将自动向上滑动,显示下一个子view,同理,如果滑动距离小于一定的距离,松开手指,它将自动滑动到开始的位置。
如下图所示
:-: ![](https://box.kancloud.cn/f47a8ca3d3209a1559b7896e5fe65148_363x602.gif)
图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 ();
}
}
~~~