3.7 自定义ViewGroup

前面我们分析了如何自定义View,下面我们继续来分析如何创建自定义ViewGroup。ViewGroup存在的目的就是为了对其子View进行管理,为其子View添加显示、响应的规则。因此,自定义ViewGroup通常需要重写onMeasure()方法来对子View进行测量,重写onLayout()方法来确定子View的位置,重写onTouchEvent()方法增加响应事件。下面通过一个实例,来看看如何自定义ViewGroup。

本例准备实现一个类似Android原生控件ScrollView的自定义ViewGroup,自定义ViewGroup可以实现ScrollView所具有的上下滑动功能,但是在滑动的过程中,增加一个黏性的效果,即当一个子View向上滑动大于一定的距离后,松开手指,它将自动向上滑动,显示下一个子View。同理,如果滑动距离小于一定的距离,松开手指,它将自动滑动到开始的位置,相信大家在很多App应用中都看见过这样的效果。

首先让自定义ViewGroup能够实现类似ScrollView的功能。

当然,在ViewGroup能够滚动之前,需要先放置好它的子View。使用遍历的方式来通知子View对自身进行测量,代码如下所示。

  1. @Override
  2. protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
  3. super.onMeasure(widthMeasureSpec, heightMeasureSpec);
  4. int count = getChildCount();
  5. for (int i = 0; i < count; ++i) {
  6. View childView = getChildAt(i);
  7. measureChild(childView, widthMeasureSpec, heightMeasureSpec);
  8. }
  9. }

接下来,就要对子View进行放置位置的设定。让每个子View都显示完整的一屏,这样在滑动的时候,可以比较好地实现后面的效果。在放置子View前,需要确定整个ViewGroup的高度。在本例中,由于让每个子View占一屏的高度,因此整个ViewGroup的高度即子View的个数乘以屏幕的高度,我们通过如下代码来确定整个ViewGroup的高度。

  1. //设置ViewGroup的高度
  2. MarginLayoutParams mlp = (MarginLayoutParams) getLayoutParams();
  3. mlp.height = mScreenHeight * childCount;
  4. setLayoutParams(mlp);

在获取了整个ViewGroup的高度之后,就可以通过遍历来设定每个子View需要放置的位置了,直接通过调用子View的layout()方法,并将具体的位置作为参数传递进去即可,代码如下所示。

  1. @Override
  2. protected void onLayout(boolean changed, int l, int t, int r, int b) {
  3. int childCount = getChildCount();
  4. //设置ViewGroup的高度
  5. MarginLayoutParams mlp = (MarginLayoutParams) getLayoutParams();
  6. mlp.height = mScreenHeight * childCount;
  7. setLayoutParams(mlp);
  8. for (int i = 0; i < childCount; i++) {
  9. View child = getChildAt(i);
  10. if (child.getVisibility() != View.GONE) {
  11. child.layout(l, i * mScreenHeight,
  12. r, (i + 1) * mScreenHeight);
  13. }
  14. }
  15. }

在代码中主要是去修改每个子View的top和bottom这两个属性,让它们能依次排列下来。

通过上面的步骤,就可以将子View放置到ViewGroup中了。但此时的ViewGroup还不能响应任何触控事件,自然也不能滑动,因此我们需要重写onTouchEvent()方法,为ViewGroup添加响应事件。在ViewGroup中添加滑动事件,通常可以使用scrollBy()方法来辅助滑动。在onTouchEvent()的ACTION_MOVE事件中,只要使用scrollBy(0, dy)方法,让手指滑动的时候让ViewGroup中的所有子View也跟着滚动dy即可,计算dy的方法有很多,如下代码就提供了一种思路。

  1. case MotionEvent.ACTION_DOWN:
  2. mLastY = y;
  3. break;
  4. case MotionEvent.ACTION_MOVE:
  5. if (!mScroller.isFinished()) {
  6. mScroller.abortAnimation();
  7. }
  8. int dy = mLastY - y;
  9. if (getScrollY() < 0) {
  10. dy = 0;
  11. }
  12. if (getScrollY() > getHeight() - mScreenHeight) {
  13. dy = 0;
  14. }
  15. scrollBy(0, dy);
  16. mLastY = y;
  17. break;

按如上方法操作就可以实现类似ScrollView的滚动效果了。当然,系统的原生ScrollView有更大的功能,比较滑动的惯性效果等,这些功能可以在后面慢慢添加,这也是一个控件的迭代过程。

最后,我们来实现这个自定义ViewGroup的黏性效果。要实现手指离开后ViewGroup的黏性效果,我们很自然地想到onTouchEvent()的ACTION_UP事件和Scroller类。在ACTION_UP事件中判断手指滑动的距离,如果超过一定距离,则使用Scroller类来平滑移动到下一个子View;如果小于一定距离,则回滚到原来的位置,代码如下所示。

  1. case MotionEvent.ACTION_DOWN:
  2. //记录触摸起点
  3. mStart = getScrollY();
  4. break;
  5. case MotionEvent.ACTION_UP:
  6. //记录触摸终点
  7. mEnd = getScrollY();
  8. int dScrollY = mEnd - mStart;
  9. if (dScrollY > 0) {
  10. if (dScrollY < mScreenHeight / 3) {
  11. mScroller.startScroll(
  12. 0, getScrollY(),
  13. 0, -dScrollY);
  14. } else {
  15. mScroller.startScroll(
  16. 0, getScrollY(),
  17. 0, mScreenHeight - dScrollY);
  18. }
  19. } else {
  20.  
  21. if (-dScrollY < mScreenHeight / 3) {
  22. mScroller.startScroll(
  23. 0, getScrollY(),
  24. 0, -dScrollY);
  25. } else {
  26. mScroller.startScroll(
  27. 0, getScrollY(),
  28. 0, -mScreenHeight - dScrollY);
  29. }
  30. }
  31. break;

通过以上操作,我们就能在onTouchEvent()中实现滚动的逻辑和“黏性”的逻辑,整个onTouchEvent()的代码如下所示。

  1. @Override
  2. public boolean onTouchEvent(MotionEvent event) {
  3. int y = (int) event.getY();
  4. switch (event.getAction()) {
  5. case MotionEvent.ACTION_DOWN:
  6. mLastY = y;
  7. mStart = getScrollY();
  8. break;
  9. case MotionEvent.ACTION_MOVE:
  10. if (!mScroller.isFinished()) {
  11. mScroller.abortAnimation();
  12. }
  13. int dy = mLastY - y;
  14. if (getScrollY() < 0) {
  15. dy = 0;
  16. }
  17. if (getScrollY() > getHeight() - mScreenHeight) {
  18. dy = 0;
  19. }
  20. scrollBy(0, dy);
  21. mLastY = y;
  22. break;
  23. case MotionEvent.ACTION_UP:
  24. mEnd = getScrollY();
  25. int dScrollY = mEnd - mStart;
  26. if (dScrollY > 0) {
  27. if (dScrollY < mScreenHeight / 3) {
  28. mScroller.startScroll(
  29. 0, getScrollY(), 0, -dScrollY); } else {
  30. mScroller.startScroll(
  31. 0, getScrollY(),
  32. 0, mScreenHeight - dScrollY);
  33. }
  34. } else {
  35. if (-dScrollY < mScreenHeight / 3) {
  36. mScroller.startScroll(
  37. 0, getScrollY(),
  38. 0, -dScrollY);
  39. } else {
  40. mScroller.startScroll(
  41. 0, getScrollY(),
  42. 0, -mScreenHeight - dScrollY);
  43. }
  44. }
  45. break;
  46. }
  47. postInvalidate();
  48. return true;
  49. }

当然,最后不要忘记加上computeScroll()的代码,如下所示。

  1. @Override
  2. public void computeScroll() {
  3. super.computeScroll();
  4. if (mScroller.computeScrollOffset()) {
  5. scrollTo(0, mScroller.getCurrY());
  6. postInvalidate();
  7. }
  8. }

程序运行效果如图3.14所示。

3.7 自定义ViewGroup - 图1 图3.14 自定义ViewGroup