当前位置: 移动技术网 > 移动技术>移动开发>Android > Android应用开发中自定义ViewGroup的究极攻略

Android应用开发中自定义ViewGroup的究极攻略

2019年07月24日  | 移动技术网移动技术  | 我要评论

支持margin,gravity以及水平,垂直排列
最近在学习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>


效果图如下:

2016520144858588.jpg (270×480)

从效果图中我们看到,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。

2016520145018412.jpg (270×480)

其实在这个基础上,我们可以很容易的添加一个方向属性,使得它可以通过设置一个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>


现在效果图如下:

2016520145127069.jpg (270×480)

重写ontouchevent()以支持滑动:

要使view滑动,我们可以通过调用scrollto()和scrollby()来实现,这里需要注意的是:要使页面向左移动,需要增加mscrollx(就是向scrollby传递一个正数),同样的,要使页面向上移动,需要增加mscrolly。

@override
public boolean ontouchevent(motionevent event) {
 final int action = event.getaction();

 if (buildconfig.debug)
  log.d("ontouchevent", "action: " + action);

 switch (action) {
 case motionevent.action_down:
  x = event.getx();
  y = event.gety();
  break;
 case motionevent.action_move:
  float mx = event.getx();
  float my = event.gety();

  //此处的moveby是根据水平或是垂直排放的方向,
  //来选择是水平移动还是垂直移动
  moveby((int) (x - mx), (int) (y - my));

  x = mx;
  y = my;
  break;
 
 }
 return true;
}

//此处的moveby是根据水平或是垂直排放的方向,
//来选择是水平移动还是垂直移动
public void moveby(int deltax, int deltay) {
 if (buildconfig.debug)
  log.d("moveby", "deltax: " + deltax + " deltay: " + deltay);
 if (orientation == orientation.horizontal) {
  if (math.abs(deltax) >= math.abs(deltay))
   scrollby(deltax, 0);
 } else {
  if (math.abs(deltay) >= math.abs(deltax))
   scrollby(0, deltay);
 }
}



好,现在我们再运行这段代码,就会发现view已经可以跟随手指移动了,但现在的问题是当手指离开屏幕后,view就立即停止滑动了,这样的体验就相当不友好,那么我们希望手指离开后,view能够以一定的阻尼满满地减速滑动。

借助scroller,并且处理action_up事件

scroller是一个用于计算位置的工具类,它负责计算下一个位置的坐标(根据时长,最小以最大移动距离,以及阻尼算法(可以使用自定义的interpolator))。

scroller有两种模式:scroll和fling。


scroll用于已知目标位置的情况(例如:viewpager中向左滑动,就是要展示右边的一页,那么我们就可以准确计算出滑动的目标位置,此时就可以使用scroller.startscroll()方法)
fling用于不能准确得知目标位置的情况(例如:listview,每一次的滑动,我们事先都不知道滑动距离,而是根据手指抬起是的速度来判断是滑远一点还是近一点,这时就可以使用scroller.fling()方法)
现在我们改一下上面的ontouchevent()方法,增加对action_up事件的处理,以及初速度的计算。

@override
public boolean ontouchevent(motionevent event) {
 final int action = event.getaction();

 if (buildconfig.debug)
  log.d("ontouchevent", "action: " + action);

 //将事件加入到velocitytracker中,用于计算手指抬起时的初速度
 if (velocitytracker == null) {
  velocitytracker = velocitytracker.obtain();
 }
 velocitytracker.addmovement(event);

 switch (action) {
 case motionevent.action_down:
  x = event.getx();
  y = event.gety();
  if (!mscroller.isfinished())
   mscroller.abortanimation();
  break;
 case motionevent.action_move:
  float mx = event.getx();
  float my = event.gety();

  moveby((int) (x - mx), (int) (y - my));

  x = mx;
  y = my;
  break;
 case motionevent.action_up:
  //maxflingvelocity是通过viewconfiguration来获取的初速度的上限
  //这个值可能会因为屏幕的不同而不同
  velocitytracker.computecurrentvelocity(1000, maxflingvelocity);
  float velocityx = velocitytracker.getxvelocity();
  float velocityy = velocitytracker.getyvelocity();

  //用来处理实际的移动
  completemove(-velocityx, -velocityy);
  if (velocitytracker != null) {
   velocitytracker.recycle();
   velocitytracker = null;
  }
  break;
 return true;
}


我们在computemove()中调用scroller的fling()方法,顺便考虑一下滑动方向问题

private void completemove(float velocityx, float velocityy) {
 if (orientation == orientation.horizontal) {
  int mscrollx = getscrollx();
  int maxx = desirewidth - getwidth();// - math.abs(mscrollx);

  if (math.abs(velocityx) >= minflingvelocity && maxx > 0) {
   
   mscroller.fling(mscrollx, 0, (int) velocityx, 0, 0, maxx, 0, 0);
   invalidate();
  }
 } else {
  int mscrolly = getscrolly();
  int maxy = desireheight - getheight();// - math.abs(mscrolly);

  if (math.abs(velocityy) >= minflingvelocity && maxy > 0) {
   
   mscroller.fling(0, mscrolly, 0, (int) velocityy, 0, 0, 0, maxy);
   invalidate();
  }
 }
}


好了,现在我们再运行一遍,问题又来了,手指抬起后,页面立刻又停了下来,并没有实现慢慢减速的滑动效果。

其实原因就是上面所说的,scroller只是帮助我们计算位置的,并不处理view的滑动。我们要想实现连续的滑动效果,那就要在view绘制完成后,再通过scroller获得新位置,然后再重绘,如此反复,直至停止。

重写computescroll(),实现view的连续绘制

@override
public void computescroll() {
 if (mscroller.computescrolloffset()) {
  if (orientation == orientation.horizontal) {
   scrollto(mscroller.getcurrx(), 0);
   postinvalidate();
  } else {
   scrollto(0, mscroller.getcurry());
   postinvalidate();
  }
 }
}


computescroll()是在viewgroup的drawchild()中调用的,上面的代码中,我们通过调用computescrolloffset()来判断滑动是否已停止,如果没有,那么我们可以通过getcurrx()和getcurry()来获得新位置,然后通过调用scrollto()来实现滑动,这里需要注意的是postinvalidate()的调用,它会将重绘的这个event加入ui线程的消息队列,等scrollto()执行完成后,就会处理这个事件,然后再次调用viewgroup的draw()-->drawchild()-->computescroll()-->scrollto()如此就实现了连续绘制的效果。

现在我们再重新运行一下app,终于可以持续滑动了:),不过,当我们缓慢地拖动view,慢慢抬起手指,我们会发现通过这样的方式,可以使得所有的子view滑到屏幕之外,(所有的子view都消失了:()。

问题主要是出在completemove()中,我们只是判断了初始速度是否大于最小阈值,如果小于这个最小阈值的话就什么都不做,缺少了边界的判断,因此修改computemove()如下:

private void completemove(float velocityx, float velocityy) {
 if (orientation == orientation.horizontal) {
  int mscrollx = getscrollx();
  int maxx = desirewidth - getwidth();
  if (mscrollx > maxx) {
   // 超出了右边界,弹回
   mscroller.startscroll(mscrollx, 0, maxx - mscrollx, 0);
   invalidate();
  } else if (mscrollx < 0) {
   // 超出了左边界,弹回
   mscroller.startscroll(mscrollx, 0, -mscrollx, 0);
   invalidate();
  } else if (math.abs(velocityx) >= minflingvelocity && maxx > 0) {
   mscroller.fling(mscrollx, 0, (int) velocityx, 0, 0, maxx, 0, 0);
   invalidate();
  }
 } else {
  int mscrolly = getscrolly();
  int maxy = desireheight - getheight();

  if (mscrolly > maxy) {
   // 超出了下边界,弹回
   mscroller.startscroll(0, mscrolly, 0, maxy - mscrolly);
   invalidate();
  } else if (mscrolly < 0) {
   // 超出了上边界,弹回
   mscroller.startscroll(0, mscrolly, 0, -mscrolly);
   invalidate();
  } else if (math.abs(velocityy) >= minflingvelocity && maxy > 0) {
   mscroller.fling(0, mscrolly, 0, (int) velocityy, 0, 0, 0, maxy);
   invalidate();
  }
 }
}

ok,现在当我们滑出边界,松手后,会自动弹回。

处理action_pointer_up事件,解决多指交替滑动跳动的问题

现在viewgroup可以灵活的滑动了,但是当我们使用多个指头交替滑动时,就会产生跳动的现象。原因是这样的:

我们实现ontouchevent()的时候,是通过event.getx(),以及event.gety()来获取触摸坐标的,实际上是获取的手指索引为0的位置坐标,当我们放上第二个手指后,这第二个手指的索引为1,此时我们同时滑动这两个手指,会发现没有问题,因为我们追踪的是手指索引为0的手指位置。但是当我们抬起第一个手指后,问题就出现了, 因为这个时候原本索引为1的第二个手指的索引变为了0,所以我们追踪的轨迹就出现了错误。

简单来说,跳动就是因为追踪的手指的改变,而这两个手指之间原本存在间隙,而这个间隙的距离就是我们跳动的距离。

其实问题产生的根本原因就是手指的索引会变化,因此我们需要记录被追踪手指的id,然后当有手指离开屏幕时,判断离开的手指是否是我们正在追踪的手指:

如果不是,忽略;
如果是,则选择一个新的手指作为被追踪手指,并且调整位置记录。
还有一点就是,要处理action_pointer_up事件,就需要给action与上一个掩码:event.getaction()&motionevent.action_mask 或者使用 event.getactionmasked()方法。

更改后的ontouchevent()的实现如下:

@override
public boolean ontouchevent(motionevent event) {
 final int action = event.getactionmasked();

 if (velocitytracker == null) {
  velocitytracker = velocitytracker.obtain();
 }
 velocitytracker.addmovement(event);

 switch (action) {
 case motionevent.action_down:
  // 获取索引为0的手指id
  mpointerid = event.getpointerid(0);
  x = event.getx();
  y = event.gety();
  if (!mscroller.isfinished())
   mscroller.abortanimation();
  break;
 case motionevent.action_move:
  // 获取当前手指id所对应的索引,虽然在action_down的时候,我们默认选取索引为0
  // 的手指,但当有第二个手指触摸,并且先前有效的手指up之后,我们会调整有效手指

  // 屏幕上可能有多个手指,我们需要保证使用的是同一个手指的移动轨迹,
  // 因此此处不能使用event.getactionindex()来获得索引
  final int pointerindex = event.findpointerindex(mpointerid);
  float mx = event.getx(pointerindex);
  float my = event.gety(pointerindex);

  moveby((int) (x - mx), (int) (y - my));

  x = mx;
  y = my;
  break;
 case motionevent.action_up:
  velocitytracker.computecurrentvelocity(1000, maxflingvelocity);
  float velocityx = velocitytracker.getxvelocity(mpointerid);
  float velocityy = velocitytracker.getyvelocity(mpointerid);

  completemove(-velocityx, -velocityy);
  if (velocitytracker != null) {
   velocitytracker.recycle();
   velocitytracker = null;
  }
  break;
 
 case motionevent.action_pointer_up:
  // 获取离开屏幕的手指的索引
  int pointerindexleave = event.getactionindex();
  int pointeridleave = event.getpointerid(pointerindexleave);
  if (mpointerid == pointeridleave) {
   // 离开屏幕的正是目前的有效手指,此处需要重新调整,并且需要重置velocitytracker
   int reindex = pointerindexleave == 0 ? 1 : 0;
   mpointerid = event.getpointerid(reindex);
   // 调整触摸位置,防止出现跳动
   x = event.getx(reindex);
   y = event.gety(reindex);
   if (velocitytracker != null)
    velocitytracker.clear();
  }
   break;
  }
 return true;
}


好了,现在我们用多个手指交替滑动就很正常了。
我们解决了多个手指交替滑动带来的页面的跳动问题。但同时也还遗留了两个问题。

我们自定义的这个viewgroup本身还不支持onclick, onlongclick事件。
当我们给子view设置click事件后,我们的viewgroup居然不能滑动了。
相对来讲,第一个问题稍稍容易处理一点,这里我们先说一下第二个问题。

onintercepttouchevent()的作用以及何时会被调用

onintercepttouchevent()是用来给viewgroup自己一个拦截事件的机会,当viewgroup意识到某个touch事件应该由自己处理,那么就可以通过此方法来阻止事件被分发到子view中。

为什么onintercepttouchevent()方法只接收到来action_down事件??需要处理action_move,action_up等等事件吗??

按照google官方文档的说明:

如果onintercepttouchevent方法返回true,那么它将不会收到后续事件,事件将会直接传递给目标的ontouchevent方法(其实会先传给目标的ontouch方法);
如果onintercepttouchevent方法返回false,那么所有的后续事件都会先传给onintercepttouchevent,然后再传给目标的ontouchevent方法。
但是,为什么我们在onintercepttouchevent方法中返回false之后,却收不到后续的事件呢??通过实验以及stackoverflow上面的一些问答得知,当我们在onintercepttouchevent()方法中返回false,且子view的ontouchevent返回true的情况下,onintercepttouchevent方法才会收到后续的事件。

虽然这个结果与官方文档的说法有点不同,但实验说明是正确的。仔细想想这样的逻辑也确实非常合理:因为onintercepttouchevent方法是用来拦截触摸事件,防止被子view捕获。那么现在子view在ontouchevent中返回false,明确声明自己不会处理这个触摸事件,那么这个时候还需要拦截吗?当然就不需要了,因此onintercepttouchevent不需要拦截这个事件,那也就没有必要将后续事件再传给它了。

还有就是onintercepttouchevent()被调用的前提是它的子view没有调用requestdisallowintercepttouchevent(true)方法(这个方法用于阻止viewgroup拦截事件)。

viewgroup的onintercepttouchevent方法,ontouchevent方法以及view的ontouchevent方法之间的事件传递流程

画了一个简单的图,如下:

2016520145306416.png (612×539)

其中:intercept指的是onintercepttouchevent()方法,touch指的是ontouchevent()方法。

好了,现在我们可以解决博客开头列出的第二个问题了,之所以为子view设置click之后,我们的viewgroup方法无法滑动,是因为,子view在接受到action_down事件后返回true,并且viewgroup的onintercepttouchevent()方法的默认实现是返回false(就是完全不拦截),所以后续的action_move,action_up事件都传递给了子view,因此我们的viewgroup自然就无法滑动了。

解决方法就是重写onintercepttouchevent方法:

/**
  * onintercepttouchevent()用来询问是否要拦截处理。 ontouchevent()是用来进行处理。
  * 
  * 例如:parentlayout----childlayout----childview 事件的分发流程:
  * parentlayout::onintercepttouchevent()---false?--->
  * childlayout::onintercepttouchevent()---false?--->
  * childview::ontouchevent()---false?--->
  * childlayout::ontouchevent()---false?---> parentlayout::ontouchevent()
  * 
  * 
  * 
  * 如果onintercepttouchevent()返回false,且分发的子view的ontouchevent()中返回true,
  * 那么onintercepttouchevent()将收到所有的后续事件。
  * 
  * 如果onintercepttouchevent()返回true,原本的target将收到action_cancel,该事件
  * 将会发送给我们自己的ontouchevent()。
  */
 @override
 public boolean onintercepttouchevent(motionevent ev) {
  final int action = ev.getactionmasked();
  if (buildconfig.debug)
   log.d("onintercepttouchevent", "action: " + action);

  if (action == motionevent.action_down && ev.getedgeflags() != 0) {
   // 该事件可能不是我们的
   return false;
  }

  boolean isintercept = false;
  switch (action) {
  case motionevent.action_down:
   // 如果动画还未结束,则将此事件交给ontouchevet()处理,
   // 否则,先分发给子view
   isintercept = !mscroller.isfinished();
   // 如果此时不拦截action_down时间,应该记录下触摸地址及手指id,当我们决定拦截action_move的event时,
   // 将会需要这些初始信息(因为我们的ontouchevent将可能接收不到action_down事件)
   mpointerid = ev.getpointerid(0);
// if (!isintercept) {
   downx = x = ev.getx();
   downy = y = ev.gety();
// }
   break;
  case motionevent.action_move:
   int pointerindex = ev.findpointerindex(mpointerid);
   if (buildconfig.debug)
    log.d("onintercepttouchevent", "pointerindex: " + pointerindex
      + ", pointerid: " + mpointerid);
   float mx = ev.getx(pointerindex);
   float my = ev.gety(pointerindex);

   if (buildconfig.debug)
    log.d("onintercepttouchevent", "action_move [touchslop: "
      + mtouchslop + ", deltax: " + (x - mx) + ", deltay: "
      + (y - my) + "]");

   // 根据方向进行拦截,(其实这样,如果我们的方向是水平的,里面有一个scrollview,那么我们是支持嵌套的)
   if (orientation == orientation.horizontal) {
    if (math.abs(x - mx) >= mtouchslop) {
     // we get a move event for ourself
     isintercept = true;
    }
   } else {
    if (math.abs(y - my) >= mtouchslop) {
     isintercept = true;
    }
   }

   //如果不拦截的话,我们不会更新位置,这样可以通过累积小的移动距离来判断是否达到可以认为是move的阈值。
   //这里当产生拦截的话,会更新位置(这样相当于损失了mtouchslop的移动距离,如果不更新,可能会有一点点跳的感觉)
   if (isintercept) {
    x = mx;
    y = my;
   }
   break;
  case motionevent.action_cancel:
  case motionevent.action_up:
   // 这是触摸的最后一个事件,无论如何都不会拦截
   if (velocitytracker != null) {
    velocitytracker.recycle();
    velocitytracker = null;
   }
   break;
  case motionevent.action_pointer_up:
   solvepointerup(ev);
   break;
  }
  return isintercept;
 }

private void solvepointerup(motionevent event) {
  // 获取离开屏幕的手指的索引
  int pointerindexleave = event.getactionindex();
  int pointeridleave = event.getpointerid(pointerindexleave);
  if (mpointerid == pointeridleave) {
   // 离开屏幕的正是目前的有效手指,此处需要重新调整,并且需要重置velocitytracker
   int reindex = pointerindexleave == 0 ? 1 : 0;
   mpointerid = event.getpointerid(reindex);
   // 调整触摸位置,防止出现跳动
   x = event.getx(reindex);
   y = event.gety(reindex);
   if (velocitytracker != null)
    velocitytracker.clear();
  }
 }


现在再运行app,问题应该解决了。
ontouchevent收到action_down,是否一定能收到action_move,action_up...???     收到了action_move,能否说明它已经收到过action_down???

其实根据上面所说的onintercepttouchevent方法与ontouchevent方法之间事件传递的过程,我们知道这两个问题的答案都是否定的。

对于第一个,收到action_down事件后,action_move事件可能会被拦截,那么它将只能够再收到一个action_cancel事件。

对于第二个,是基于上面的这一个情况,action_down传递给了子view,而onintercepttouchevent拦截了action_move事件,所以我们的ontouchevent方法将会收到action_move,而不会收到action_down。(这也是为什么我在onintercepttouchevent方法的action_down中记录下位置信息的原因)

还有一个问题就是,如果我们单纯的在ontouchevent中: 对于action_down返回true,在接收到action_move事件后返回false,那么这个时候事件会重新寻找能处理它的view吗?不会,所有的后续事件依然会发给这个ontouchevent方法。


让viewgroup支持click事件

这里我们是在ontouchevent中对于action_up多做了一些处理:

判断从按下时的位置到现在的移动距离是否小于可被识别为move的阈值。
根据action_down和action_up之间的时间差,判断是click,还是long click(这里当没有设置long click的话,我们也可将其认为是click)

case motionevent.action_up:
   //先判断是否是点击事件
   final int pi = event.findpointerindex(mpointerid);
   
   if((isclickable() || islongclickable()) 
     && ((event.getx(pi) - downx) < mtouchslop || (event.gety(pi) - downy) < mtouchslop)) {
    //这里我们得到了一个点击事件
    if(isfocusable() && isfocusableintouchmode() && !isfocused())
     requestfocus();
    if(event.geteventtime() - event.getdowntime() >= viewconfiguration.getlongpresstimeout() && islongclickable()) {
     //是一个长按事件
     performlongclick();
    } else {
     performclick();
    }
   } else {
    velocitytracker.computecurrentvelocity(1000, maxflingvelocity);
    float velocityx = velocitytracker.getxvelocity(mpointerid);
    float velocityy = velocitytracker.getyvelocity(mpointerid);
 
    completemove(-velocityx, -velocityy);
    if (velocitytracker != null) {
     velocitytracker.recycle();
     velocitytracker = null;
    }
   }
   break;

如对本文有疑问, 点击进行留言回复!!

相关文章:

验证码:
移动技术网