服务器之家:专注于VPS、云服务器配置技术及软件下载分享
分类导航

PHP教程|ASP.NET教程|Java教程|ASP教程|编程技术|正则表达式|C/C++|IOS|C#|Swift|Android|VB|R语言|JavaScript|易语言|vb.net|

服务器之家 - 编程语言 - Android - Android自定义View仿腾讯TIM下拉刷新View

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

2022-09-14 16:31芦苇科技App技术团队 Android

这篇文章主要给大家介绍了关于Android自定义View仿腾讯TIM下拉刷新View的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

一 概述

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

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

二 需求分析

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

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

三 功能实现

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

来看一下 circle类的代码

#cirlce.java

?
1
2
3
4
5
6
7
8
9
10
11
12
13
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

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@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

?
1
2
3
4
5
6
@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

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

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

#refreshview.java

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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

?
1
2
3
4
5
6
7
8
9
10
11
12
/**
* 设置圆球初始状态
* {@link #state_origin}为原始状态(三个小球重合),
* {@link #state_prepared}为准备好可以刷新的状态,三个小球间距最大
*/
public void setoriginstate(int state) {
if (state == 0) {
 moriginstate = state_origin;
} else {
 moriginstate = state_prepared;
}
}

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

#refreshview.java

?
1
2
3
4
5
6
7
@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

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 个动画周期。

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

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

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

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

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

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

#refreshview.java

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@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;
  }
 }

效果图:

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

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

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

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

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

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

总结

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

原文链接:https://juejin.im/post/5c307ba751882522c03e6b9d

延伸 · 阅读

精彩推荐