ThinkSSL🔒 一键申购 5分钟快速签发 30天无理由退款 购买更放心 广告
#### **六、自定义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 (); } } ~~~