前言
最近玩守望屁股,看到它的加载loading效果不错,于是便想把它实现到Android上。经过实践,这个小项目已初步完成,全部内容只有一个类文件,可以拷贝到任何Android项目中使用,非常方便。
正文
效果:
守望先锋的效果:
项目demo的效果:
效果还是不错哒!
相关资源
项目托管在github上,获取地址为:https://github.com/zhangyuChen1991/OverWatchLoading 欢迎前去下载。
实现过程及原理
先梳理一下整体流程吧,其实整个view就是在不断的重绘,一共有7个六边形,我们不断的重置每个六边形的大小和透明度然后绘制整个图案,因为刷新频率达到了一定的高度,所以就等于一帧一帧的放出画面,就有了最后的动画效果了。从头到尾我们所做的工作就是不断的画7个大小和透明度不同的正六边形,要做这些工作我们必须要先计算出一些需要的数据——绘制所用的基准数据。第一步,为了获取这些数据,我们得先画出完整大小的7个六边形的图案。
1.绘制正六边形
首先我们来看看loading的全部图案都显示的情况是什么样的吧。
一共是七个正六边形,而且排列非常规律,各个六边形之间有相同的间隔距离。如果没有这个间隔,它们边与边之间就是完全重合的,只要确定了整个图案中心点坐标,就可以确定所有需要的点的坐标位置,进而画出整个图案。现在多了一个间隔,稍微增加了一点计算的复杂度,暂时先不管它。
我们一步一步来看,首先,画一个六边形的关键就是确定它的中心点和半径,有了这两个值,就可以确定其余六个顶点的位置。如下图所示:
图中示例了一个点的计算方式,其余各点类同。
有了中心点坐标位置和它的半径就可以计算出它每个顶点的坐标位置进而绘制出完整的正六边形图案。
所以我们第一步要做的就是确定所有七个六边形的中心点坐标。首先,最中心那个最容易确定,就把它设定在整个view的中心,并确立为基准,以此来计算其他周围六个六边形的中心点坐标。
如果没有那个间隙,其余六个中心点也很好找,但是麻烦就在这个间隙。我们把六边形之间的间隙分开来看,即横向间隙和纵向间隙。如下图:
图中由于做了圆角处理,所以纵向间隙标的不准确,其实就是两个顶点之间的垂直距离了。 我们在计算的时候,设定水平间距与竖直间距相等,由自己设置一个合适的距离。然后根据中心点坐标、六边形半径和间距计算边围六边形的中心点。如下图:
我们获得R和r的关系过后,就可以根据根据r、中心点坐标和间距来计算各个边围六边形的中心点了,也就是图中蓝色六边形的顶点。原理同上。
计算出各六边形的中心点之后,我们计算出它们的顶点也就手到擒来了。这里要注意的是,计算顶点的时候,我们要添加一个缩放值作为参数,以方便以后实现动画效果做计算的时候用。至此,所有我们需要的数据都已经获取完毕。
在代码实现时,首先获取整个view的宽高,确定中心点。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
viewHeight = getMeasuredHeight();
viewWidth = getMeasuredWidth();
if (viewWidth != 0 && viewHeight != 0) {
center.x = viewWidth / 2f;
center.y = viewHeight / 2f;
...
...
}
}
计算7个六边形的中心点:
private void initHexagonCenters() {
float bigR = (float) ((1.5 * hexagonRadius + space) / cos30);
hexagonCenters[0] = new Point(center.x - bigR * sin30, center.y - bigR * cos30);
hexagonCenters[1] = new Point(center.x + bigR * sin30, center.y - bigR * cos30);
hexagonCenters[2] = new Point(center.x + bigR, center.y);
hexagonCenters[3] = new Point(center.x + bigR * sin30, center.y + bigR * cos30);
hexagonCenters[4] = new Point(center.x - bigR * sin30, center.y + bigR * cos30);
hexagonCenters[5] = new Point(center.x - bigR, center.y);
//左上角为第0个,中心六边形为第6个
for (int i = 0; i < 6; i++) {
hexagons[i] = new Hexagon(hexagonCenters[i], hexagonRadius);
}
hexagons[6] = new Hexagon(center, hexagonRadius);
}
创建Point类和一个六边形的类,来储存相关数据。
private class Point {
public float x, y;
public Point(float x, float y) {
this.x = x;
this.y = y;
}
public Point() {
}
}
/**
* 六边形
*/
private class Hexagon {
//缩放值
private float scale = 1;
//透明度
private int alpha = 255;
public Point centerPoint;
public float radius;
//六个顶点
private Point[] vertexs = new Point[6];
public Hexagon(Point centerPoint, float radius) {
this.centerPoint = centerPoint;
this.radius = radius;
calculatePointsPosition();
}
public void drawHexagon(Canvas canvas, Paint paint) {
paint.setAlpha(alpha);
canvas.drawPath(getPath(), paint);
}
private int calculatePointsPosition() {
if (centerPoint == null) {
return -1;
}
//从最上方顺时针数1-6给各顶点标序号 共6个点,scale作为动态参数,用以控制六边形的大小,围绕中心点做缩放变化
vertexs[0] = new Point(centerPoint.x, centerPoint.y - radius * scale);
vertexs[1] = new Point(centerPoint.x + radius * cos30 * scale, centerPoint.y - radius * sin30 * scale);
vertexs[2] = new Point(centerPoint.x + radius * cos30 * scale, centerPoint.y + radius * sin30 * scale);
vertexs[3] = new Point(centerPoint.x, centerPoint.y + radius * scale);
vertexs[4] = new Point(centerPoint.x - radius * cos30 * scale, centerPoint.y + radius * sin30 * scale);
vertexs[5] = new Point(centerPoint.x - radius * cos30 * scale, centerPoint.y - radius * sin30 * scale);
return 1;
}
//根据六个顶点绘制path
private Path getPath() {
Path path = new Path();
for (int i = 0; i < 6; i++) {
if (i == 0)
path.moveTo(vertexs[i].x, vertexs[i].y);
else
path.lineTo(vertexs[i].x, vertexs[i].y);
}
path.close();
return path;
}
...
...
}
2.动画实现
到上一步,六边形的绘制就算基本完成了,现在我们要对它们做动画。由效果图中我们可以看见loading的动画流程是依次渐变出现,再依次渐变消失,循环往复。这里有一个细节要主意,各个六边形并不是等上一个完全出现之后才开始动画的,而是上一个还没有完全完成动画时这一个就开始自己的动画过程了,这样看起来才有连贯流畅的动画效果,实现的时候是需要注意的。
理清了流程,我们就开始想办法实现吧。首先,从第一个开始,缩放值(scale)由原始值的0倍变大到1倍,透明度(alpha)由0变大到255(由paint.setAlpha(alpha)参数值决定)。然后,当第一个放大到一定大小时,第二个开始动画,依次类推,直到第七个完全出现时,第一个开始缩小并逐步变透明,缩小到一定大小时,第二个开始缩小...依次类推,然后循环往复。
在代码实现上,我们利用一个线程来更新数据,由它来触发每一次的变化,并在变化完成后刷新整个view完成重绘。
//更新数据的线程
private Runnable animRunnable = new Runnable() {
@Override
public void run() {
try {
while (runAnim) {
Thread.sleep(2);
flush();
draw();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
/**
* 更新数据
*/
private void flush() {
if (nowAnimatorFlag == ShowAnimatorFlag) {//逐个显示出来
hexagons[0].addScale();
hexagons[0].addAlpha();
for (int i = 0; i < hexagons.length - 1; i++) {
if (hexagons[i].getScale() >= scaleCritical) {
hexagons[i + 1].addScale();
hexagons[i + 1].addAlpha();
}
}
if (hexagons[6].getScale() == 1) {//当最后一个六边形都完全显示时,切换模式,下一轮逐个消失
nowAnimatorFlag = HideAnimatorFlag;
}
} else {//逐个消失
hexagons[0].subScale();
hexagons[0].subAlpha();
for (int i = 0; i < hexagons.length - 1; i++) {
if (hexagons[i].getScale() <= 1 - scaleCritical) {
hexagons[i + 1].subScale();
hexagons[i + 1].subAlpha();
}
}
if (hexagons[6].getScale() == 0) {//当最后一个六边形都完全消失时,切换模式,下一轮逐个开始显示
nowAnimatorFlag = ShowAnimatorFlag;
}
}
}
最后,在draw()方法里面,绘制六边形就可以了
/**
* 绘制图案
*/
private void draw() {
Canvas canvas = surfaceHolder.lockCanvas();
paint.setXfermode(new PorterDuffXfermode(android.graphics.PorterDuff.Mode.CLEAR));
canvas.drawPaint(paint);
paint.setXfermode(new PorterDuffXfermode(android.graphics.PorterDuff.Mode.DST_OVER));
for (int i = 0; i < 7; i++) {
hexagons[i].drawHexagon(canvas, paint);
}
surfaceHolder.unlockCanvasAndPost(canvas);
}
所以到这里,整个实现的流程就都捋了一遍了,你清楚了吗?
尾声
最后,再贴一次项目地址吧 https://github.com/zhangyuChen1991/OverWatchLoading
有兴趣的童鞋可以前去下载,欢迎提出意见!且如发现问题,请斧正!非常感谢!