当前位置: 移动技术网 > IT编程>移动开发>Android > Android自定义View仿腾讯TIM下拉刷新View

Android自定义View仿腾讯TIM下拉刷新View

2019年07月30日  | 移动技术网IT编程  | 我要评论

极品邪龙全文阅读,福隆平,爱情保卫战20130801

一 概述

自定义 view 是 android 开发里面的一个大学问。偶然间看到 tim 邮箱界面的刷新 view 还挺好玩的,于是就自己动手实现了一个,先看看 tim 里边的效果图:

二 需求分析

看到上面的动图,大概也知道我们需要实现的功能:

  • 根据拖动的进度来移动小球的位置
  • 小球移动过程的动画

三 功能实现

新建一个 refreshview 类继承自 view ,然后我们再在 refreshview 里面新建一个内部实体类: circle

来看一下 circle类的代码

#cirlce.java

 class circle {
 int x;
 int y;
 int r;
 int color;

 public circle(int x, int y, int r, int color) {
  this.x = x;
  this.y = y;
  this.r = r;
  this.color = color;
 }
 }

这是一个实体类,里面提供了 x , y , r , color 属性分别代表圆心坐标的 x值,y值,圆的半径 r 跟颜色。
借助此类来存储小圆球的相关属性。

接下来就是我们平时自定义 view 经常要重写的三大方法了,先看 onmeasure()

#refreshview.java

 @override
 protected void onmeasure(int widthmeasurespec, int heightmeasurespec) {
 int widthmode = measurespec.getmode(widthmeasurespec);
 int widthsize = measurespec.getsize(widthmeasurespec);
 int heightmode = measurespec.getmode(heightmeasurespec);
 int heightsize = measurespec.getsize(heightmeasurespec);
 if (widthmode == measurespec.at_most && heightmode == measurespec.exactly) {
  setmeasureddimension(mwidth, heightsize);
 } else if (widthmeasurespec == measurespec.exactly && heightmeasurespec == measurespec.at_most) {
  setmeasureddimension(widthsize, mheight);
 } else if (widthmode == measurespec.exactly && heightmode == measurespec.exactly) {
  setmeasureddimension(widthsize, heightsize);
 } else {
  setmeasureddimension(mwidth, mheight);
 }
 }

为了适配布局文件中的 wrap_content 参数,我们需要重写此方法(此方法不是本文的研究重点,不明白的可以百度或者google一下,或者参考《android开发艺术探索》里面的相关章节)。

接着看 onlayout() 方法:

#refreshview.java

 @override
 protected void onlayout(boolean changed, int left, int top, int right, int bottom) {
 super.onlayout(changed, left, top, right, bottom);
 initcontentattr(getmeasuredwidth(), getmeasuredheight());
 resetcircles();
 }

在此方法中调用了 initcontentattr() 方法来初始化内容大小与 resetcircles() 来初始化(重置)三个小球的属性。分别看下这两个方法:

#refreshview.java

 private void initcontentattr(int width, int height) {
 mcontentwidth = width - getpaddingleft() - getpaddingright();
 mcontentheight = height - getpaddingtop() - getpaddingbottom();
 }

这方法很简单,就是进行了 padding 的处理,得出真正的布局大小。如果不处理 padding 的话那么用户设置了 padding 将失效。再看 resetcircles():

#refreshview.java

 public static final int state_origin = 0;
 public static final int state_prepared = 1;
 private int moriginstate = state_origin;

 private void resetcircles() {
 if (mcircles.isempty()) {
  int x = mcontentwidth / 2;
  int y = mcontentheight / 2;
  mgap = x - mminradius; //初始化相邻圆心间的最大间距
  circle circleleft = new circle(x, y, mminradius, 0xffff7f0a);
  circle circlecenter = new circle(x, y, mmaxradius, color.red);
  circle circleright = new circle(x, y, mminradius, color.green);
  mcircles.add(left, circleleft);
  mcircles.add(right, circleright);
  mcircles.add(center, circlecenter);
 }
 if (moriginstate == state_origin) {
  int x = mcontentwidth / 2;
  int y = mcontentheight / 2;
  for (int i = 0; i < mcircles.size(); i++) {
  circle circle = mcircles.get(i);
  circle.x = x;
  circle.y = y;
  if (i == center) {
   circle.r = mmaxradius;
  } else {
   circle.r = mminradius;
  }
  }
 } else {
  preparetostart();
 }
 }

此方法用于初始化和重置小球,方法里面进行的两个大的 if...else 语句判断,第一个 if 用于判断是否应该初始化小球,第二个语句则是用于判断小球的初始化时候的形态。可以在外部调用 setoriginstate() 方法来指定小球的初始化形态,如不指定,则默认为 nomal,即三球重合。

#refreshview.java

 /**
 * 设置圆球初始状态
 * {@link #state_origin}为原始状态(三个小球重合),
 * {@link #state_prepared}为准备好可以刷新的状态,三个小球间距最大
 */
 public void setoriginstate(int state) {
 if (state == 0) {
  moriginstate = state_origin;
 } else {
  moriginstate = state_prepared;
 }
 }

最后就是最有趣的方法 ondraw() 了:

#refreshview.java

 @override
 protected void ondraw(canvas canvas) {
 for (circle circle : mcircles) {
  mpaint.setcolor(circle.color);
  canvas.drawcircle(circle.x + getpaddingleft(), circle.y + getpaddingtop(), circle.r, mpaint);
 }
 }

这方法很简单,就是将 mcircles 列表里面的圆画出来而已(里面进行了 padding 的处理)。

三大方法都讲完了,可是这只是画出了几个小圆球而已,我们需求分析里的需求还没实现呢,上面的方法已经把 view 的基础搭起来了,要实现这个也就不难了。接下来就是大家期待的需求实现了:

根据拖动的进度来移动小球的位置

实现代码如下:

#refreshview.java

 public void drag(float fraction) {
 if (moriginstate == state_prepared) {
  return;
 }
 if (manimator != null && manimator.isrunning()) {
  return;
 }
 if (fraction > 1) {
  return;
 }
 mcircles.get(left).x = (int) (mminradius + mgap * (1f - fraction));
 mcircles.get(right).x = (int) (mcontentwidth / 2 + mgap * fraction);
 postinvalidate();
 }

在方法里面进行三次判断,如果初始状态是 state_prepared (三小球距离最大,没必要再变动了)、动画正在进行或者进度大于1 都不进行移动。然后修改小球的属性,再重绘。

小球移动过程的动画

这个是这个自定义 view 最难的部分了,需要一些数学的小运算,有点繁琐。

我们先来理清实现动画的逻辑,看了开篇的gif,应该可以了解到,刚准备开始动画时,左边的小球应该是处于最左端,中间的小球处于中间,右边的处于最右端。我们一个个小球来分析。

  • 左边小球:动画开始后,左边的小球向右移动,并且逐渐变大,直到小球运动到中点,过了中点后小球继续往右移动,不过却逐渐变小,到了终点后小球将消失(消失过程为先缩小再消失,下同),接着又从左边出现(出现过程也是从小到大的渐变,下同),然后重复上述过程。
  • 中间小球:中间的小球先向右移动,逐渐缩小,然后消失,后来再从左边出现,最后移动到中间,其间逐渐变大。后面就是重复的上述动作。
  • 右边小球:右边的小球则是先消失,再从左边出现,接着移动到中间,其间逐渐变大,然后再从中点移动到末端,其间逐渐缩小。

理清小球的移动过程对代码的实现很有帮助,我们可以分析出:

1)每个小球对于坐标系的移动特点是一样的。

2)每个小球对于动画的进度的移动特点是不一样的。

听起来好像有点拗口,我们用人话来解释一下:

1)每个小球对于坐标系的移动特点是一样的:左边的小球在坐标的最左边是先出现,然后再向右移动,那么中间和右边的小球呢?其实是同样的,它们在坐标轴最左边的时候都是先出现,再向右移动,无论哪个小球,它们在坐标轴的同一点上的动作和形态应该是一致的。

2)每个小球对于动画的进度的移动特点是不一样的:左边的小球在动画刚开始时是处于最左端,而中间的小球却在中间位置,右边的则在最右端。当动画开始后,比如进行了一半,这时候左边的小球应该移动到了中点附近,而中间的确是在末端(消失),右边的小球就会出现在中间附近。

按照上面分析的逻辑,我把动画的总进度分为6份,为什么是6份呢?通过上面的动画分析,知道小球应该经历一下过程(不分时间先后):

  • 出现 (从无渐变到初始大小)
  • 从最左端移动到中点(期间变大)
  • 从中点移动到末端(期间缩小)
  • 消失 (从初始大小渐变到消失)

为了让小球之间的间隔保持一个优美的状态(动画开始后小球间不会重叠,相邻小球的间隔基本一致),就把1、4出现和消失阶段分别设为 1/6 的动画周期,中间2、3两个阶段分别占用 1/3 个动画周期。

这样一来,出现跟消失占用了 1/3 动画进度,其他两个部分分别占用了 1/3 动画进度。举个例子:刚开始动画时,设最左边的小球为 1,中间的小球为 2,最右端的小球为 3 。

当 小球1 移动到中点时,这时动画进行了 1/3 ,那么此时的 小球2 就应该移动到末端,小球3 则刚好经历消失和出现过程,于是应该出现于坐标轴的起点。

由此可以看到又恢复到了刚开始时候的情况(一个小球在最左,一个在中,一个在最右),只不过是颜色不同了而已。以此类推,无限循环,就可以形成优美的动画了。

分析出这些有什么用呢?我发现用坐标来确定小球的移动实现起来会有点小问题,所以就用动画的进度来实现,下面看具体实现。

需要实现小球的无限运动,最实用的就是用动画来实现,这里我用了属性动画。先初始化 animotor 类:

#refreshview.java

 private void initanimator() {
 valueanimator animator = valueanimator.offloat(0f, 1f);
 animator.setduration(1500);
 animator.setrepeatcount(-1);
 animator.setrepeatmode(valueanimator.restart);
 animator.setinterpolator(new linearinterpolator());
 animator.addlistener(new animator.animatorlistener() {
  @override
  public void onanimationstart(animator animation) {
  preparetostart(); //确保view达到可以刷新的状态
  }

  @override
  public void onanimationend(animator animation) {

  }

  @override
  public void onanimationcancel(animator animation) {
  }

  @override
  public void onanimationrepeat(animator animation) {
  }
 });
 animator.addupdatelistener(new valueanimator.animatorupdatelistener() {

  @override
  public void onanimationupdate(valueanimator animation) {
  for (circle circle : mcircles) {
   updatecircle(circle, mcircles.indexof(circle), animation.getanimatedfraction());
  }
  postinvalidate();
  }
 });
 manimator = animator;
 }

可以看到,这是一个无限循环的动画,如果不手动停止,它就会一直循环下去。对于 manimator ,还添加了一个监听器,当开始动画是就调用 preparetostart() 方法,这个方法看起来是不是有点眼熟,没错,它就是我们上面 resetcircles() 里面判断小球形态为 state_prepared 是调用过,此方法将确保小球达到刷新的临界点。我们主要看看 updatelisener 中的 onanimationupdate() 方法里面的 updatecircle() 方法:

#refreshview

 private void updatecircle(circle circle, int index, float fraction) {
 float progress = fraction; //真实进度
 float virtualfraction; //每个小球内部的虚拟进度
 switch (index) {
  case left:
  if (fraction < 5f / 6f) {
   progress = progress + 1f / 6f;
  } else {
   progress = progress - 5f / 6f;
  }
  break;
  case center:
  if (fraction < 0.5f) {
   progress = progress + 0.5f;
  } else {
   progress = progress - 0.5f;
  }
  break;
  case right:
  if (fraction < 1f / 6f) {
   progress += 5f / 6f;
  } else {
   progress -= 1f / 6f;
  }
  break;
 }
 if (progress <= 1f / 6f) {
  virtualfraction = progress * 6;
  appear(circle, virtualfraction);
  return;
 }
 if (progress >= 5f / 6f) {
  virtualfraction = (progress - 5f / 6f) * 6;
  disappear(circle, virtualfraction);
  return;
 }
 virtualfraction = (progress - 1f / 6f) * 3f / 2f;
 move(circle, virtualfraction);
 }

我用了一个 virtualfraction 来表示每个小球的虚拟进度(相当于上面坐标图中的下值,即坐标百分比),例如当动画的总进度为 0 时,左小球的虚拟进度就应该是 1/6+0 (默认已经经过了出现过程,消耗了 1/6),中间小球的虚拟进度为 1/6+1/3+0 = 1/2 (默认经历了出现,移动到中间过程),最右边小球的虚拟进度为 1/6+1/3+1/3+0 = 5/6 。然后动画的总进度到 1/3 时,左小球的虚拟进度就为 1/2 (中间位置)......

下面再看下 move() 、appear()、disapear() 方法:

#refreshview

 private void appear(circle circle, float fraction) {
 circle.r = (int) (mminradius * fraction);
 circle.x = mminradius;
 }

 private void disappear(circle circle, float fraction) {
 circle.r = (int) (mminradius * (1 - fraction));
 }

 private void move(circle circle, float fraction) {
 int difference = mmaxradius - mminradius;
 if (fraction < 0.5) {
  circle.r = (int) (mminradius + difference * fraction * 2);
 } else {
  circle.r = (int) (mmaxradius - difference * (fraction - 0.5) * 2);
 }
 circle.x = (int) (mminradius + mgap * 2 * fraction);
 }

这个三个方法都很简单,根据坐标的占比来计算出小球的坐标跟大小。

以上就是整个 refershview 的实现了,如果需要看源码的可以拉到文末。

四 使用及效果

看下怎么使用:

#mainactivity

 @override
 protected void oncreate(bundle savedinstancestate) {
  super.oncreate(savedinstancestate);
  setcontentview(r.layout.activity_main);
  mrefreshview = findviewbyid(r.id.refresh_view);
//  mrefreshview.setoriginstate(refreshview.state_prepared);
  button start = findviewbyid(r.id.start);
  button stop = findviewbyid(r.id.stop);
  seekbar seekbar = findviewbyid(r.id.seek_bar);
  seekbar.setonseekbarchangelistener(new seekbar.onseekbarchangelistener() {
   @override
   public void onprogresschanged(seekbar seekbar, int progress, boolean fromuser) {
    mrefreshview.drag(progress / 100f);
   }

   @override
   public void onstarttrackingtouch(seekbar seekbar) {

   }

   @override
   public void onstoptrackingtouch(seekbar seekbar) {

   }
  });
  start.setonclicklistener(this);
  stop.setonclicklistener(this);
 }

 @override
 public void onclick(view v) {
  switch (v.getid()) {
   case r.id.start:
    mrefreshview.start();
    break;
   case r.id.stop:
    mrefreshview.stop();
    break;
  }
 }

效果图:

由于录制软件的问题,绿色的小球显示效果不太好,在手机或虚拟机上显示是正常的。再看个项目里的实际运用效果:

录屏软件对绿色好像过敏,将就看一下吧。

此文到此就结束了,感谢阅读,喜欢的动动小手点个赞。

demo 地址:https://github.com/gminibird/refreshviewtest ()

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对移动技术网的支持。

如对本文有疑问,请在下面进行留言讨论,广大热心网友会与你互动!! 点击进行留言回复

相关文章:

验证码:
移动技术网