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的巩固还是挺有帮助的,下一次则来实现如下两个效果进一步对它进行巩固,稍稍要麻烦一些:

划船这个来自于网上的一个开源项目,用作学习还是很不错的~~

posted on 2021-12-02 14:56  cexo  阅读(167)  评论(0编辑  收藏  举报

导航