android高级UI之PathMeasure<二>--Path测量实战(各种Loading效果)
在上一次https://www.cnblogs.com/webor2006/p/15488224.html已经学习上PathMeasure的基础了,这次则针对它进行一些实际效果的操练加以巩固。
实战:各种Loading效果:
Loading一:让箭头图片沿圆轨迹走
效果:
首先来实现在上篇开头所展示的这个效果:
如果对于上一篇的整体基础都了解之后,实现这样沿路径轨迹的效果那就很轻松了。
实现:
1、新建View:
这里还是基于上一次学习的工程往上继续垒代码:
2、 画园:
首先先准备一个中心的圆,待会它是就图片需要沿着运转的轨迹:
package com.cexo.pathmeasurestudy; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Path; import android.util.AttributeSet; import android.view.View; import androidx.annotation.Nullable; /** * Loading效果一:箭头图片沿圆路径旋转 */ public class LoadingView extends View { private int viewWidth; private int viewHeight; private Paint circlePaint; public LoadingView(Context context) { this(context, null); } public LoadingView(Context context, @Nullable AttributeSet attrs) { this(context, attrs, -1); } public LoadingView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context); } private void init(Context context) { circlePaint = new Paint(); circlePaint.setColor(Color.RED); circlePaint.setStrokeWidth(5); circlePaint.setStyle(Paint.Style.STROKE); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.drawColor(Color.WHITE); canvas.translate(viewWidth / 2, viewHeight / 2); Path path = new Path(); //Path.Direction.CW顺时针方向画圆 path.addCircle(0, 0, 200, Path.Direction.CCW); canvas.drawPath(path, circlePaint); } protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); viewWidth = w; viewHeight = h; } }
比较简单,效果如下:
3、导入图片:
接下来则将需要沿这个圆轨迹移动的图片导进来:
4、将图片绘制在圆轨迹上:
此时的效果:
此时图片的绘制点则是在圆心上,不是咱们所预期的效果,应该是绘制在圆的轨迹上才行,此时很自然的就可以借助上一次学习的Path的测量,可以得到这俩值:
其中测量时的forceClosed参数的含义就不过多说明了,可以参考上一个基础篇,接下来的重点就是如何借助这个测量得到的pos和tan来将咱们的图片绘制在圆轨迹上了,这里可以将图片绘制的left和top改为它试试:
此时运行看一下:
嗯,基本上是已经在圆轨迹上了,但是应该是让整个图片的中心在轨迹上,而不是图片的(0,0)坐标点:
这个简单,减去掉图片的宽高的一半既可:
此时再运行:
5、让图片进行角度旋转:
接下来重点就是要来处理图片的角度旋转了,很明显目前图片的角度是不对的,需要让箭头沿着圆的轨迹走才可以,那首先得要算出圆指定位置的角度才行对吧,这个简单,在上一次基础篇中已经打好这块的基础了:
这里就不过多说明,先来得到角度:
而要想让图片进行指定角度的旋转,此时在绘制图片时就得用另一个API了,这个也在上一篇有介绍过:
这里就直接上代码了:
此时运行:
貌似平移的效果木有了,原来咱们平移不是用的它么?
而目前绘制图片用的是matrix的api了,如何对它进行平移呢?其实matrix里有专门的api了,如下:
不过,发现貌似这个箭头是要往逆时针旋转呀,这是因为我们在绘制圆时就是指定的逆时针:
所以,咱们改一下它,用逆时针旋转貌似更加自然一些:
6、让箭头沿轨迹动起来:
那接下来还差最后一步,如何让箭头动起来,这块就不难了,因为目前我们已经处理好圆的起始位置了:
我们只要让这个位置不断的进行增加,那不就可以实现一个动态的效果么?具体做法如下:
运行效果:
至此,整个效果完成,是不是在打好基础之后,实现这样的效果非常之顺其自然?
Loading二:
效果:
接下来实现第二个Loading效果,也是上一篇中提到过的:
而要实现这样的效果,就得用这个API了:
掌握基础之后其实现也不难。
实现:
1、新建View:
2、绘制圆:
跟上一个Loading效果一样,先在View的中心绘一个一模一样的圆:
package com.cexo.pathmeasurestudy; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Path; import android.util.AttributeSet; import android.view.View; import androidx.annotation.Nullable; /** * Loading效果一:利用getSegment() api实现Path的截取 */ public class LoadingView2 extends View { private int viewWidth; private int viewHeight; private Paint circlePaint; //region 构造 public LoadingView2(Context context) { this(context, null); } public LoadingView2(Context context, @Nullable AttributeSet attrs) { this(context, attrs, -1); } public LoadingView2(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context); } //endregion private void init(Context context) { circlePaint = new Paint(); circlePaint.setColor(Color.RED); circlePaint.setStrokeWidth(5); circlePaint.setStyle(Paint.Style.STROKE); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.drawColor(Color.WHITE); canvas.translate(viewWidth / 2, viewHeight / 2); Path path = new Path(); //Path.Direction.CW顺时针方向画圆 path.addCircle(0, 0, 200, Path.Direction.CW); canvas.drawPath(path, circlePaint); } protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); viewWidth = w; viewHeight = h; } }
效果就不贴出来了。
2、截取Path指定位置进行显示:
这里先实现一个Path截取的静态效果:
运行看一下:
看到了么,是不是显示的就是其圆中截取的从100到200长度的位置?
3、将截段动态化:
接下来要实现目标动态的效果,则就需要来动态控制从Path中哪截取对吧,也就是:
那这里可以使用一个属性动画ValueAnimator来弄一个系数,然后再不断进行界面的刷新,如下:
接下来则就根据这个系数来动态算一下开始位置和结束位置,首先咱们结束位置这么来定:
这个容易理解,那。。对于start这个参数如何确定呢?它肯定是要小于stop值的,所以它肯定是这样的:
其中标问号的这个值的计算就是最关键的,这里先来看一下预期的效果,等于是路径先走一半圆:
接着再超过一半圆的场景下,则开始让整个圆路径变短直到消失为止:
这个用文字可能不太好描述,还是以一个慢动作再来体会一下这个效果背后的逻辑:
具体这块的公式就不推导了,一句代码,如下:
此时运行看一下效果:
看着有点卡对吧,是因为录屏软件的原因。。
4、调试理解代码:
对于这个效果的实现,其实最难理解的就是这个Path开始点位置的确定,也就是:
难理解也得想办法理解它,通常当你有段代码死活都无法理解时,此时如果知道答案时最好的方式就是自己通过调试一点点来理解,这里以四个条件来进行分析,为了便于计算,这里假设整个圆Path测量的总长度为100。
a、animatorValue=0.3:
stop=100 * 0.3 = 30;
start=stop - ((0.5 - 0.2) * 100) = 30 - 30 = 0;
所以此时绘制的路径区域为:
b、animatorValue=0.5:
stop=100 * 0.5 = 50;
start=stop - ((0.5 - 0) * 100) = 50 - 50 = 0;
所以此时绘制的路径区域为:
c、animatorValue=0.7:
stop=100 * 0.7 = 70;
start=stop - ((0.5 - 0.2) * 100) = 70 - 30 = 40;
所以此时绘制的路径区域为:
也就是,当走过半圆的路径之后,start的开始截取的位置也开始变化了。
d、animatorValue=1.0:
stop=100 * 1.0 = 100;
start=stop - ((0.5 - 0.5) * 100) = 100 - 0 = 100;
所以此时绘制的路径区域为:
通过这么几个条件的分析,是不是对这句代码的理解就会更加的简单了?
5、优化代码:
对于目前的代码实现上其实有一些不太好的地方,就是:
对于整个圆的测量其实只要测一次就可以了,没必要放在onDraw()中每次都测量,这样性能也不是太好,所以这里抽离一下:
此时运行,你会发现出bug了。。
卡住了。。另外你再点back键时,发现有时还会anr:
这是为啥呢?其实是我们目标的这个Path在绘制时木有进行重置造成,修改如下:
为啥呢?通过Android Studio的Profile app发现,内存爆增。。
而看一下Path.reset()方法的说明:
哦,大致明白了,也就是由于onDraw()方法执行非常之频繁,而Path如果你不主动reset()的话,那么你之前绘制的元素肯定是还在的,那么当onDraw()函数不断执行很明显内存只会爆增的,最终达到界面卡死从而有ANR的情况出现了,这一点必须要注意。另外这里还有一个细节就是关于硬件加速的场景需要加这么一句话:
关于这块我木有测出来,先这么写吧,待之前遇到了这种问题到时再回头来理解它。
Loading三:
效果:
接下来再来实现这样的一个Loading效果:
有了上一个Loading的基础,是不是实现这个就不难了。
实现:
1、新建View:
2、画个圆:
package com.cexo.pathmeasurestudy; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Path; import android.util.AttributeSet; import android.view.View; /** * Loading效果三:继续利用getSegment() api实现Path的截取 */ public class LoadingView3 extends View { private int viewWidth; private int viewHeight; private Paint circlePaint; public LoadingView3(Context context) { this(context, null); } public LoadingView3(Context context, AttributeSet attrs) { this(context, attrs, -1); } public LoadingView3(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { circlePaint = new Paint(); circlePaint.setColor(Color.RED); circlePaint.setStrokeWidth(5); circlePaint.setStyle(Paint.Style.STROKE); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.drawColor(Color.WHITE); canvas.translate(viewWidth / 2, viewHeight / 2); Path path = new Path(); //Path.Direction.CW顺时针方向画圆 path.addCircle(0, 0, 200, Path.Direction.CW); canvas.drawPath(path, circlePaint); } protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); viewWidth = w; viewHeight = h; } }
3、绘制圆一圈:
其实整个效果就是由两个状态组成,一个是绘制圆路径,一个是由整个圆路径慢慢转变消失,所以这里首先先沿圆的轨迹绘制一圈,比较简单:
是不是跟咱们上面实现的代码基本雷同对吧,运行一下:
4、让圆消失一圈:
接下来则反着进行动态处理,也就是让满圆归0,实现起来也非常简单,先来增加两个状态:
然后在属性动画结束时,主动切换一个当前的绘制状态:
接下来在绘制这块也需要根据状态改变进行一下处理:
此时运行效果就如开始看到的了,如果你对于PathMeasure基础打牢之后,对于这样的效果实现还是比较简单的了。
总结:
整体来说这次的操练都不是太难,但是对于PathMeasure的巩固还是挺有帮助的,下一次则来实现如下两个效果进一步对它进行巩固,稍稍要麻烦一些:
划船这个来自于网上的一个开源项目,用作学习还是很不错的~~