Android中PathMeasure仿支付宝支付动画

发布时间 - 2026-01-11 02:40:30    点击率:

前言

在 Android 自定义 View 中,Path 可能用的比较多,PathMeasure 可能用的比较少,就我而言,以前也没有使用过 PathMeasure 这个 api,看到别人用 PathMeasure 和 ValueAnimator 结合在一起完成了很好的动画效果,于是我也学习下 PathMeasure ,此处记录下。

PathMeasure

构造器:

forceClosed 含义:

// 创建一个 Path 对象
path = new Path();
path.moveTo(20, 20);
path.lineTo(200, 20);
path.lineTo(200, 400);

在onDraw(Canvas canvas) 中绘制 path

@Override
 protected void onDraw(Canvas canvas) {
  destPath.reset();
  destPath.lineTo(0, 0);
  pathMeasure.setPath(path, true);
  Log.e("debug", "PathMeasure.getLength() = " + pathMeasure.getLength());
  pathMeasure.getSegment(0, pathMeasure.getLength() * curValue, destPath, true);
canvas.drawPath(destPath, paint); // 绘制线段路径

 }

当 pathMeasure.setPath(path,false) 时:

当 pathMeasure.setPath(path,true) 时:

可以看到:当 forceClosed = true 时, path 进行了闭合,相应的 path 长度也变长了,即 算上了斜边的长度。

仿支付宝支付动画 View - LoadingView

效果:

思路:

绘制对号,叉号,主要是通过 ValueAnimator 结合 getSegment() 不断绘制新的弧形段,其中,叉号由两个 path 组成,在第一个 path 绘制完成时,需要调用 pathMeasure.nextContour() 跳转到另一个 path。

getSegment() 将获取的片段填充到 destPath 中,在 Android 4.4 及以下版本中,不能绘制,需要调用 destPath.reset(),destPath.line(0,0)

LoadingView 完整代码:

public class LoadingView extends View {

  private final int DEFAULT_COLOR = Color.BLACK; // 默认圆弧颜色

  private final int DEFAULT_STROKE_WIDTH = dp2Px(2); // 默认圆弧宽度

  private final boolean DEFAULT_IS_SHOW_RESULT = false; // 默认不显示加载结果

  private final int DEFAULT_VIEW_WIDTH = dp2Px(50); // 控件默认宽度

  private final int DEFAULT_VIEW_HEIGHT = dp2Px(50); // 控件默认高度

  private int color; // 圆弧颜色

  private int strokeWidth;  // 圆弧宽度

  private boolean isShowResult;  // 是否显示加载结果状态

  private Paint paint; // 画笔

  private int mWidth; // 控件宽度

  private int mHeight; // 控件高度

  private int radius;  // 圆弧所在圆的半径

  private int halfStrokeWidth; // 画笔宽度的一半


  private int rotateDelta = 4;

  private int curAngle = 0;

  private int minAngle = -90;

  private int startAngle = -90; // 上方顶点

  private int endAngle = 0;

  private RectF rectF;

  private StateEnum stateEnum = StateEnum.LOADING;

  private Path successPath;

  private Path rightFailPath;

  private Path leftFailPath;

  private ValueAnimator successAnimator;

  private ValueAnimator rightFailAnimator;

  private ValueAnimator leftFailAnimator;

  private PathMeasure pathMeasure;

  private float successValue;

  private float rightFailValue;

  private float leftFailValue;

  private Path destPath;

  private AnimatorSet animatorSet;

  public LoadingView(Context context) {
    this(context, null);
  }

  public LoadingView(Context context, AttributeSet attrs) {
    super(context, attrs);
    init(context, attrs);
  }


  private void init(Context context, AttributeSet attrs) {
    TypedArray typedArray = null;
    try {
      typedArray = context.obtainStyledAttributes(attrs, R.styleable.LoadingView);
      color = typedArray.getColor(R.styleable.LoadingView_color, DEFAULT_COLOR);
      strokeWidth = (int) typedArray.getDimension(R.styleable.LoadingView_storkeWidth, DEFAULT_STROKE_WIDTH);
      isShowResult = typedArray.getBoolean(R.styleable.LoadingView_isShowResult, DEFAULT_IS_SHOW_RESULT);
    } catch (Exception e) {
      e.printStackTrace();
    } finally {
      if (typedArray != null) {
        typedArray.recycle();
      }
    }
    paint = createPaint(color, strokeWidth, Paint.Style.STROKE);
  }


  @Override
  protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);
    mWidth = w;
    mHeight = h;
    Log.i("debug", "getMeasureWidth() = " + getMeasuredWidth());
    Log.i("debug", "getMeasureHeight() = " + getMeasuredHeight());

    radius = Math.min(mWidth, mHeight) / 2;
    halfStrokeWidth = strokeWidth / 2;

    rectF = new RectF(halfStrokeWidth - radius, halfStrokeWidth - radius,
        radius - halfStrokeWidth, radius - halfStrokeWidth);
    // success path
    successPath = new Path();
    successPath.moveTo(-radius * 2 / 3f, 0f);
    successPath.lineTo(-radius / 8f, radius / 2f);
    successPath.lineTo(radius / 2, -radius / 3);
    // fail path ,right top to left bottom
    rightFailPath = new Path();
    rightFailPath.moveTo(radius / 3f, -radius / 3f);
    rightFailPath.lineTo(-radius / 3f, radius / 3f);

    // fail path, left top to right bottom
    leftFailPath = new Path();
    leftFailPath.moveTo(-radius / 3f, -radius / 3f);
    leftFailPath.lineTo(radius / 3f, radius / 3f);

    pathMeasure = new PathMeasure();

    destPath = new Path();

    initSuccessAnimator();
    initFailAnimator();
  }

  private void initSuccessAnimator() {
//    pathMeasure.setPath(successPath, false);
    successAnimator = ValueAnimator.ofFloat(0, 1f);
    successAnimator.setDuration(1000);
    successAnimator.setInterpolator(new LinearInterpolator());
    successAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {

      @Override
      public void onAnimationUpdate(ValueAnimator animation) {
        successValue = (float) animation.getAnimatedValue();
        invalidate();
      }
    });
  }


  private void initFailAnimator() {
//    pathMeasure.setPath(rightFailPath, false);
    rightFailAnimator = ValueAnimator.ofFloat(0, 1f);
    rightFailAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {

      @Override
      public void onAnimationUpdate(ValueAnimator animation) {
        rightFailValue = (float) animation.getAnimatedValue();
        invalidate();
      }
    });

//    pathMeasure.setPath(leftFailPath, false);
    leftFailAnimator = ValueAnimator.ofFloat(0, 1f);
    leftFailAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
      @Override
      public void onAnimationUpdate(ValueAnimator animation) {
        leftFailValue = (float) animation.getAnimatedValue();
        invalidate();
      }
    });

    animatorSet = new AnimatorSet();
    animatorSet.play(leftFailAnimator).after(rightFailAnimator);
    animatorSet.setDuration(500);
    animatorSet.setInterpolator(new LinearInterpolator());


  }


  /**
   * 测量控件的宽高,当测量模式不是精确模式时,设置默认宽高
   *
   * @param widthMeasureSpec
   * @param heightMeasureSpec
   */
  @Override
  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    if (widthMode == MeasureSpec.AT_MOST || widthMode == MeasureSpec.UNSPECIFIED) {
      widthMeasureSpec = MeasureSpec.makeMeasureSpec(DEFAULT_VIEW_WIDTH, MeasureSpec.EXACTLY);
    }
    if (heightMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.UNSPECIFIED) {
      heightMeasureSpec = MeasureSpec.makeMeasureSpec(DEFAULT_VIEW_HEIGHT, MeasureSpec.EXACTLY);

    }
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
  }


  @Override
  protected void onDraw(Canvas canvas) {
    canvas.save();
    canvas.translate(mWidth / 2, mHeight / 2);
    destPath.reset();
    destPath.lineTo(0, 0);  // destPath
    if (stateEnum == StateEnum.LOADING) {
      if (endAngle >= 300 || startAngle > minAngle) {
        startAngle += 6;
        if (endAngle > 20) {
          endAngle -= 6;
        }
      }
      if (startAngle > minAngle + 300) {
        minAngle = startAngle;
        endAngle = 20;
      }
      canvas.rotate(curAngle += rotateDelta, 0, 0);//旋转rotateDelta=4的弧长
      canvas.drawArc(rectF, startAngle, endAngle, false, paint);
      // endAngle += 6 放在 drawArc()后面,是防止刚进入时,突兀的显示了一段圆弧
      if (startAngle == minAngle) {
        endAngle += 6;
      }
      invalidate();
    }
    if (isShowResult) {
      if (stateEnum == StateEnum.LOAD_SUCCESS) {
        pathMeasure.setPath(successPath, false);
        canvas.drawCircle(0, 0, radius - halfStrokeWidth, paint);
        pathMeasure.getSegment(0, successValue * pathMeasure.getLength(), destPath, true);
        canvas.drawPath(destPath, paint);
      } else if (stateEnum == StateEnum.LOAD_FAILED) {
        canvas.drawCircle(0, 0, radius - halfStrokeWidth, paint);
        pathMeasure.setPath(rightFailPath, false);
        pathMeasure.getSegment(0, rightFailValue * pathMeasure.getLength(), destPath, true);
        if (rightFailValue == 1) {
          pathMeasure.setPath(leftFailPath, false);
          pathMeasure.nextContour();
          pathMeasure.getSegment(0, leftFailValue * pathMeasure.getLength(), destPath, true);
        }
        canvas.drawPath(destPath, paint);
      }
    }
    canvas.restore();

  }


  public void updateState(StateEnum stateEnum) {
    this.stateEnum = stateEnum;
    if (stateEnum == StateEnum.LOAD_SUCCESS) {
      successAnimator.start();
    } else if (stateEnum == StateEnum.LOAD_FAILED) {
      animatorSet.start();
    }
  }


  public enum StateEnum {
    LOADING, // 正在加载
    LOAD_SUCCESS,  // 加载成功,显示对号
    LOAD_FAILED   // 加载失败,显示叉号
  }


  /**
   * 创建画笔
   *
   * @param color    画笔颜色
   * @param strokeWidth 画笔宽度
   * @param style    画笔样式
   * @return
   */
  private Paint createPaint(int color, int strokeWidth, Paint.Style style) {
    Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
    paint.setStrokeCap(Paint.Cap.ROUND);
    paint.setStrokeJoin(Paint.Join.ROUND);
    paint.setColor(color);
    paint.setStrokeWidth(strokeWidth);
    paint.setStyle(style);
    return paint;
  }


  /**
   * dp 转换成 px
   *
   * @param dpValue
   * @return
   */
  private int dp2Px(int dpValue) {
    return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpValue, getResources().getDisplayMetrics());
  }

}

github : https://github.com/xing16/LoadingView

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。


# Android  # PathMeasure  # 支付宝  # Android中RecyclerView布局代替GridView实现类似支付宝的界面  # Android波纹扩散效果之仿支付宝咻一咻功能实现波纹扩散特效  # Android app第三方支付宝支付接入教程  # Android支付宝和微信支付集成  # Android仿支付宝支付从底部弹窗效果  # Android支付宝支付封装代码  # 新版Android studio导入微信支付和支付宝官方Demo问题解决大全  # Android开发之实现GridView支付宝九宫格  # Android自定义View仿支付宝输入六位密码功能  # android仿微信支付宝的支付密码输入框示例  # 加载  # 我也  # 很好  # 放在  # 第一个  # 上了  # 可以看到  # 自定义  # 比较多  # 转换成  # 长了  # 使用过  # 创建一个  # 正在加载  # 就我  # 比较少  # 大家多多  # 进行了  # 刚进 


相关栏目: 【 网站优化151355 】 【 网络推广146373 】 【 网络技术251813 】 【 AI营销90571


相关推荐: 如何在Windows虚拟主机上快速搭建网站?  Claude怎样写约束型提示词_Claude约束提示词写法【教程】  php json中文编码为null的解决办法  如何用wdcp快速搭建高效网站?  头像制作网站在线观看,除了站酷,还有哪些比较好的设计网站?  如何选择PHP开源工具快速搭建网站?  Linux安全能力提升路径_长期防护思维说明【指导】  Laravel怎么定时执行任务_Laravel任务调度器Schedule配置与Cron设置【教程】  php中::能调用final静态方法吗_final修饰静态方法调用规则【解答】  悟空识字如何进行跟读录音_悟空识字开启麦克风权限与录音  北京的网站制作公司有哪些,哪个视频网站最好?  SQL查询语句优化的实用方法总结  Laravel如何发送系统通知_Laravel Notifications实现多渠道消息通知  如何快速建站并高效导出源代码?  Laravel项目如何进行性能优化_Laravel应用性能分析与优化技巧大全  详解ASP.NET 生成二维码实例(采用ThoughtWorks.QRCode和QrCode.Net两种方式)  小视频制作网站有哪些,有什么看国内小视频的网站,求推荐?  宙斯浏览器视频悬浮窗怎么开启 边看视频边操作其他应用教程  Android自定义listview布局实现上拉加载下拉刷新功能  如何生成腾讯云建站专用兑换码?  长沙企业网站制作哪家好,长沙水业集团官方网站?  Python3.6正式版新特性预览  Laravel Octane如何提升性能_使用Laravel Octane加速你的应用  浅谈Javascript中的Label语句  ,网页ppt怎么弄成自己的ppt?  如何快速搭建高效香港服务器网站?  Laravel怎么返回JSON格式数据_Laravel API资源Response响应格式化【技巧】  个人网站制作流程图片大全,个人网站如何注销?  怎么用AI帮你设计一套个性化的手机App图标?  移动端脚本框架Hammer.js  潮流网站制作头像软件下载,适合母子的网名有哪些?  敲碗10年!Mac系列传将迎来「触控与联网」双革新  网站建设要注意的标准 促进网站用户好感度!  Windows10如何更改计算机工作组_Win10系统属性修改Workgroup  网站制作壁纸教程视频,电脑壁纸网站?  1688铺货到淘宝怎么操作 1688一键铺货到自己店铺详细步骤  Swift中swift中的switch 语句  Python结构化数据采集_字段抽取解析【教程】  Laravel请求验证怎么写_Laravel Validator自定义表单验证规则教程  网站制作软件有哪些,制图软件有哪些?  如何在阿里云ECS服务器部署织梦CMS网站?  Python文件异常处理策略_健壮性说明【指导】  如何在宝塔面板创建新站点?  iOS正则表达式验证手机号、邮箱、身份证号等  如何用5美元大硬盘VPS安全高效搭建个人网站?  Laravel如何配置和使用队列处理异步任务_Laravel队列驱动与任务分发实例  Laravel如何实现数据导出到CSV文件_Laravel原生流式输出大数据量CSV【方案】  Laravel Docker环境搭建教程_Laravel Sail使用指南  如何在VPS电脑上快速搭建网站?  Laravel如何使用Passport实现OAuth2?(完整配置步骤)