热门标签 | HotTags
当前位置:  开发笔记 > Android > 正文

Android使用属性动画如何自定义倒计时控件详解

自Android3.0版本开始,系统给我们提供了一种全新的动画模式,属性动画(propertyanimation),它的功能非常强大,下面这篇文章主要给大家介绍了关于Android使用属性动画如何自定义倒计时控件的相关资料,需要的朋友可以参考下

为什么要引入属性动画?

Android之前的补间动画机制其实还算是比较健全的,在android.view.animation包下面有好多的类可以供我们操作,来完成一系列的动画效果,比如说对View进行移动、缩放、旋转和淡入淡出,并且我们还可以借助AnimationSet来将这些动画效果组合起来使用,除此之外还可以通过配置Interpolator来控制动画的播放速度等等等等。那么这里大家可能要产生疑问了,既然之前的动画机制已经这么健全了,为什么还要引入属性动画呢?

其实上面所谓的健全都是相对的,如果你的需求中只需要对View进行移动、缩放、旋转和淡入淡出操作,那么补间动画确实已经足够健全了。但是很显然,这些功能是不足以覆盖所有的场景的,一旦我们的需求超出了移动、缩放、旋转和淡入淡出这四种对View的操作,那么补间动画就不能再帮我们忙了,也就是说它在功能和可扩展方面都有相当大的局限性,那么下面我们就来看看补间动画所不能胜任的场景。

注意上面我在介绍补间动画的时候都有使用“对View进行操作”这样的描述,没错,补间动画是只能够作用在View上的。也就是说,我们可以对一个Button、TextView、甚至是LinearLayout、或者其它任何继承自View的组件进行动画操作,但是如果我们想要对一个非View的对象进行动画操作,抱歉,补间动画就帮不上忙了。可能有的朋友会感到不能理解,我怎么会需要对一个非View的对象进行动画操作呢?这里我举一个简单的例子,比如说我们有一个自定义的View,在这个View当中有一个Point对象用于管理坐标,然后在onDraw()方法当中就是根据这个Point对象的坐标值来进行绘制的。也就是说,如果我们可以对Point对象进行动画操作,那么整个自定义View的动画效果就有了。显然,补间动画是不具备这个功能的,这是它的第一个缺陷。

然后补间动画还有一个缺陷,就是它只能够实现移动、缩放、旋转和淡入淡出这四种动画操作,那如果我们希望可以对View的背景色进行动态地改变呢?很遗憾,我们只能靠自己去实现了。说白了,之前的补间动画机制就是使用硬编码的方式来完成的,功能限定死就是这些,基本上没有任何扩展性可言。

最后,补间动画还有一个致命的缺陷,就是它只是改变了View的显示效果而已,而不会真正去改变View的属性。什么意思呢?比如说,现在屏幕的左上角有一个按钮,然后我们通过补间动画将它移动到了屏幕的右下角,现在你可以去尝试点击一下这个按钮,点击事件是绝对不会触发的,因为实际上这个按钮还是停留在屏幕的左上角,只不过补间动画将这个按钮绘制到了屏幕的右下角而已。

也正是因为这些原因,Android开发团队决定在3.0版本当中引入属性动画这个功能,那么属性动画是不是就把上述的问题全部解决掉了?下面我们就来一起看一看。

新引入的属性动画机制已经不再是针对于View来设计的了,也不限定于只能实现移动、缩放、旋转和淡入淡出这几种动画操作,同时也不再只是一种视觉上的动画效果了。它实际上是一种不断地对值进行操作的机制,并将值赋值到指定对象的指定属性上,可以是任意对象的任意属性。所以我们仍然可以将一个View进行移动或者缩放,但同时也可以对自定义View中的Point对象进行动画操作了。我们只需要告诉系统动画的运行时长,需要执行哪种类型的动画,以及动画的初始值和结束值,剩下的工作就可以全部交给系统去完成了。

既然属性动画的实现机制是通过对目标对象进行赋值并修改其属性来实现的,那么之前所说的按钮显示的问题也就不复存在了,如果我们通过属性动画来移动一个按钮,那么这个按钮就是真正的移动了,而不再是仅仅在另外一个位置绘制了而已。

好了,介绍了这么多,相信大家已经对属性动画有了一个最基本的认识了,下面来一看看详细的介绍吧

引言

本文介绍一下利用属性动画(未使用Timer,通过动画执行次数控制倒计时)自定义一个圆形倒计时控件,比较简陋,仅做示例使用,如有需要,您可自行修改以满足您的需求。控件中所使用的素材及配色均是笔者随意选择,导致效果不佳,先上示例图片


示例中进度条底色、渐变色(仅支持两个色值)、字体大小、图片、进度条宽度及是否显示进度条等可通过xml修改,倒计时时间可通过代码设置。如果您感兴趣,可修改代码设置更丰富的渐变色值及文字变化效果,本文仅仅提供设计思路。

笔者利用属性动画多次执行实现倒计时,执行次数即为倒计时初始数值。对上述示例做一下拆解,会发现实现起来还是很容易的,需要处理的主要是以下几部分

1.绘制外部环形进度条

2.绘制中央旋转图片

3.绘制倒计时时间

一.绘制外部环形进度条,分为两部分:

1.环形背景 canvas.drawCircle方法绘制

2.扇形进度 canvas.drawArc方法绘制,弧度通过整体倒计时执行进度控制

二.绘制中央旋转图片:

前置描述:外层圆形直径设为d1;中央旋转图片直径设为d2;进度条宽度设为d3

1.将设置的图片进行剪切缩放处理(也可不剪切,笔者有强迫症),使其宽高等于d1 - 2 * d3,即d2 = d1 - 2 * d3;

2.利用Matrix将Bitmap平移至中央;

3.利用Matrix旋转Bitmap

三.绘制倒计时时间:

通过每次动画执行进度,控制文本位置

下面上示例代码:

public class CircleCountDownView extends View {
 private CountDownListener countDownListener;

 private int width;
 private int height;
 private int padding;
 private int borderWidth;
 // 根据动画执行进度计算出来的插值,用来控制动画效果,建议取值范围为0到1
 private float currentAnimationInterpolation;
 private boolean showProgress;
 private float totalTimeProgress;
 private int processColorStart;
 private int processColorEnd;
 private int processBlurMaskRadius;

 private int initialCountDownValue;
 private int currentCountDownValue;

 private Paint circleBorderPaint;
 private Paint circleProcessPaint;
 private RectF circleProgressRectF;

 private Paint circleImgPaint;
 private Matrix circleImgMatrix;
 private Bitmap circleImgBitmap;
 private int circleImgRadius;
 private AnimationInterpolator animationInterpolator;
 private BitmapShader circleImgBitmapShader;
 private float circleImgTranslationX;
 private float circleImgTranslationY;
 private Paint valueTextPaint;

 private ValueAnimator countDownAnimator;

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

 public CircleCountDownView(Context context, @Nullable AttributeSet attrs) {
 this(context, attrs, 0);
 }

 public CircleCountDownView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
 super(context, attrs, defStyleAttr);
 setLayerType(View.LAYER_TYPE_SOFTWARE, null);
 init(attrs);
 }

 private void init(AttributeSet attrs) {
 circleImgPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
 circleImgPaint.setStyle(Paint.Style.FILL);
 circleImgMatrix = new Matrix();
 valueTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);

 TypedArray typedArray = getContext().obtainStyledAttributes(attrs, R.styleable.CircleCountDownView);
 // 控制外层进度条的边距
 padding = typedArray.getDimensionPixelSize(R.styleable.CircleCountDownView_padding, DisplayUtil.dp2px(5));
 // 进度条边线宽度
 borderWidth = typedArray.getDimensionPixelSize(R.styleable.CircleCountDownView_circleBorderWidth, 0);
 if (borderWidth > 0) {
  circleBorderPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
  circleBorderPaint.setStyle(Paint.Style.STROKE);
  circleBorderPaint.setStrokeWidth(borderWidth);
  circleBorderPaint.setColor(typedArray.getColor(R.styleable.CircleCountDownView_circleBorderColor, Color.WHITE));

  showProgress = typedArray.getBoolean(R.styleable.CircleCountDownView_showProgress, false);
  if (showProgress) {
  circleProcessPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
  circleProcessPaint.setStyle(Paint.Style.STROKE);
  circleProcessPaint.setStrokeWidth(borderWidth);
  // 进度条渐变色值
  processColorStart = typedArray.getColor(R.styleable.CircleCountDownView_processColorStart, Color.parseColor("#00ffff"));
  processColorEnd = typedArray.getColor(R.styleable.CircleCountDownView_processColorEnd, Color.parseColor("#35adc6"));
  // 进度条高斯模糊半径
  processBlurMaskRadius = typedArray.getDimensionPixelSize(R.styleable.CircleCountDownView_processBlurMaskRadius, DisplayUtil.dp2px(5));
  }
 }


 int circleImgSrc = typedArray.getResourceId(R.styleable.CircleCountDownView_circleImgSrc, R.mipmap.ic_radar);
 // 图片剪裁成正方形
 circleImgBitmap = ImageUtil.cropSquareBitmap(BitmapFactory.decodeResource(getResources(), circleImgSrc));

 valueTextPaint.setColor(typedArray.getColor(R.styleable.CircleCountDownView_valueTextColor, Color.WHITE));
 valueTextPaint.setTextSize(typedArray.getDimensionPixelSize(R.styleable.CircleCountDownView_valueTextSize, DisplayUtil.dp2px(13)));

 typedArray.recycle();

 // 初始化属性动画,周期为1秒
 countDownAnimator = ValueAnimator.ofFloat(0, 1).setDuration(1000);
 countDownAnimator.setInterpolator(new LinearInterpolator());
 countDownAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
  @Override
  public void onAnimationUpdate(ValueAnimator animation) {
  if (countDownListener != null) {
   // 监听剩余时间
   long restTime = (long) ((currentCountDownValue - animation.getAnimatedFraction()) * 1000);
   countDownListener.restTime(restTime);
  }
  // 整体倒计时进度
  totalTimeProgress = (initialCountDownValue - currentCountDownValue + animation.getAnimatedFraction()) / initialCountDownValue;
  if (animationInterpolator != null) {
   currentAnimatiOnInterpolation= animationInterpolator.getInterpolation(animation.getAnimatedFraction());
  } else {
   currentAnimatiOnInterpolation= animation.getAnimatedFraction();
   currentAnimationInterpolation *= currentAnimationInterpolation;
  }
  invalidate();
  }
 });
 countDownAnimator.addListener(new AnimatorListenerAdapter() {
  @Override
  public void onAnimationRepeat(Animator animation) {
  currentCountDownValue--;
  }

  @Override
  public void onAnimationEnd(Animator animation) {
  if (countDownListener != null) {
   countDownListener.onCountDownFinish();
  }
  }
 });
 }

 // 设置倒计时初始时间
 public void setStartCountValue(int initialCountDownValue) {
 this.initialCountDownValue = initialCountDownValue;
 this.currentCountDownValue = initialCountDownValue;
 // 设置重复执行次数,共执行initialCountDownValue次,恰好为倒计时总数
 countDownAnimator.setRepeatCount(currentCountDownValue - 1);
 invalidate();
 }

 public void setAnimationInterpolator(AnimationInterpolator animationInterpolator) {
 if (!countDownAnimator.isRunning()) {
  this.animatiOnInterpolator= animationInterpolator;
 }
 }

 // 重置
 public void reset() {
 countDownAnimator.cancel();
 lastAnimatiOnInterpolation= 0;
 totalTimeProgress = 0;
 currentAnimatiOnInterpolation= 0;
 currentCountDownValue = initialCountDownValue;
 circleImgMatrix.setTranslate(circleImgTranslationX, circleImgTranslationY);
 circleImgMatrix.postRotate(0, width / 2, height / 2);
 invalidate();
 }

 public void restart() {
 reset();
 startCountDown();
 }

 public void pause() {
 countDownAnimator.pause();
 }

 public void setCountDownListener(CountDownListener countDownListener) {
 this.countDownListener = countDownListener;
 }

 // 启动倒计时
 public void startCountDown() {
 if (countDownAnimator.isPaused()) {
  countDownAnimator.resume();
  return;
 }
 if (currentCountDownValue > 0) {
  countDownAnimator.start();
 } else if (countDownListener != null) {
  countDownListener.onCountDownFinish();
 }
 }

 @Override
 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
 width = getMeasuredWidth();
 height = getMeasuredHeight();
 if (width > 0 && height > 0) {
  doCalculate();
 }
 }

 private void doCalculate() {
 circleImgMatrix.reset();
 // 圆形图片绘制区域半径
 circleImgRadius = (Math.min(width, height) - 2 * borderWidth - 2 * padding) / 2;
 float actualCircleImgBitmapWH = circleImgBitmap.getWidth();
 float circleDrawingScale = circleImgRadius * 2 / actualCircleImgBitmapWH;
 // bitmap缩放处理
 Matrix matrix = new Matrix();
 matrix.setScale(circleDrawingScale, circleDrawingScale, actualCircleImgBitmapWH / 2, actualCircleImgBitmapWH / 2);
 circleImgBitmap = Bitmap.createBitmap(circleImgBitmap, 0, 0, circleImgBitmap.getWidth(), circleImgBitmap.getHeight(), matrix, true);
 // 绘制圆形图片使用
 circleImgBitmapShader = new BitmapShader(circleImgBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
 // 平移至中心
 circleImgTranslatiOnX= (width - circleImgRadius * 2) / 2;
 circleImgTranslatiOnY= (height - circleImgRadius * 2) / 2;
 circleImgMatrix.setTranslate(circleImgTranslationX, circleImgTranslationY);

 if (borderWidth > 0) {
  // 外层进度条宽度(注意:需要减掉画笔宽度)
  float circleProgressWH = Math.min(width, height) - borderWidth - 2 * padding;
  float left = (width > height ? (width - height) / 2 : 0) + borderWidth / 2 + padding;
  float top = (height > width ? (height - width) / 2 : 0) + borderWidth / 2 + padding;
  float right = left + circleProgressWH;
  float bottom = top + circleProgressWH;
  circleProgressRectF = new RectF(left, top, right, bottom);
  if (showProgress) {
  // 进度条渐变及边缘高斯模糊处理
  circleProcessPaint.setShader(new LinearGradient(left, top, left + circleImgRadius * 2, top + circleImgRadius * 2, processColorStart, processColorEnd, Shader.TileMode.MIRROR));
  circleProcessPaint.setMaskFilter(new BlurMaskFilter(processBlurMaskRadius, BlurMaskFilter.Blur.SOLID)); // 设置进度条阴影效果
  }
 }
 }

 private float lastAnimationInterpolation;

 @Override
 protected void onDraw(Canvas canvas) {
 if (width == 0 || height == 0) {
  return;
 }
 int centerX = width / 2;
 int centerY = height / 2;
 if (borderWidth > 0) { 
  // 绘制外层圆环
  canvas.drawCircle(centerX, centerY, Math.min(width, height) / 2 - borderWidth / 2 - padding, circleBorderPaint);
  if (showProgress) {
  // 绘制整体进度
  canvas.drawArc(circleProgressRectF, 0, 360 * totalTimeProgress, false, circleProcessPaint);
  }

 }
 // 设置图片旋转角度增量
 circleImgMatrix.postRotate((currentAnimationInterpolation - lastAnimationInterpolation) * 360, centerX, centerY);
 circleImgBitmapShader.setLocalMatrix(circleImgMatrix);
 circleImgPaint.setShader(circleImgBitmapShader);
 canvas.drawCircle(centerX, centerY, circleImgRadius, circleImgPaint);
 lastAnimatiOnInterpolation= currentAnimationInterpolation;

 // 绘制倒计时时间
 // current
 String currentTimePoint = currentCountDownValue + "s";
 float textWidth = valueTextPaint.measureText(currentTimePoint);
 float x = centerX - textWidth / 2;
 Paint.FontMetrics fOntMetrics= valueTextPaint.getFontMetrics();
 // 文字绘制基准线(圆形区域正中央)
 float verticalBaseline = (height - fontMetrics.bottom - fontMetrics.top) / 2;
 // 随动画执行进度而更新的y轴位置
 float y = verticalBaseline - currentAnimationInterpolation * (Math.min(width, height) / 2);
 valueTextPaint.setAlpha((int) (255 - currentAnimationInterpolation * 255));
 canvas.drawText(currentTimePoint, x, y, valueTextPaint);

 // next
 String nextTimePoint = (currentCountDownValue - 1) + "s";
 textWidth = valueTextPaint.measureText(nextTimePoint);
 x = centerX - textWidth / 2;
 y = y + (Math.min(width, height)) / 2;
 valueTextPaint.setAlpha((int) (currentAnimationInterpolation * 255));
 canvas.drawText(nextTimePoint, x, y, valueTextPaint);
 }

 public interface CountDownListener {
 /**
  * 倒计时结束
  */
 void onCountDownFinish();

 /**
  * 倒计时剩余时间
  *
  * @param restTime 剩余时间,单位毫秒
  */
 void restTime(long restTime);
 }

 public interface AnimationInterpolator {
 /**
  * @param inputFraction 动画执行时间因子,取值范围0到1
  */
 float getInterpolation(float inputFraction);
 }
}

自定义属性如下


 
 
 
 
 
 
 
 
 
 
 
 

代码比较简单,如有疑问欢迎留言

完整代码:https://github.com/670832188/TestApp (本地下载)

总结

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


推荐阅读
  • RecyclerView初步学习(一)
    RecyclerView初步学习(一)ReCyclerView提供了一种插件式的编程模式,除了提供ViewHolder缓存模式,还可以自定义动画,分割符,布局样式,相比于传统的ListVi ... [详细]
  • Android LED 数字字体的应用与实现
    本文介绍了一种适用于 Android 应用的 LED 数字字体(digital font),并详细描述了其在 UI 设计中的应用场景及其实现方法。这种字体常用于视频、广告倒计时等场景,能够增强视觉效果。 ... [详细]
  • 解决微信电脑版无法刷朋友圈问题:使用安卓远程投屏方案
    在工作期间想要浏览微信和朋友圈却不太方便?虽然微信电脑版目前不支持直接刷朋友圈,但通过远程投屏技术,可以轻松实现在电脑上操作安卓设备的功能。 ... [详细]
  • 资源推荐 | TensorFlow官方中文教程助力英语非母语者学习
    来源:机器之心。本文详细介绍了TensorFlow官方提供的中文版教程和指南,帮助开发者更好地理解和应用这一强大的开源机器学习平台。 ... [详细]
  • 构建基于BERT的中文NL2SQL模型:一个简明的基准
    本文探讨了将自然语言转换为SQL语句(NL2SQL)的任务,这是人工智能领域中一项非常实用的研究方向。文章介绍了笔者在公司举办的首届中文NL2SQL挑战赛中的实践,该比赛提供了金融和通用领域的表格数据,并标注了对应的自然语言与SQL语句对,旨在训练准确的NL2SQL模型。 ... [详细]
  • 数据库内核开发入门 | 搭建研发环境的初步指南
    本课程将带你从零开始,逐步掌握数据库内核开发的基础知识和实践技能,重点介绍如何搭建OceanBase的开发环境。 ... [详细]
  • 本文介绍如何使用 Sortable.js 库实现元素的拖拽和位置交换功能。Sortable.js 是一个轻量级、无依赖的 JavaScript 库,支持拖拽排序、动画效果和多种插件扩展。通过简单的配置和事件处理,可以轻松实现复杂的功能。 ... [详细]
  • 探讨一个显示数字的故障计算器,它支持两种操作:将当前数字乘以2或减去1。本文将详细介绍如何用最少的操作次数将初始值X转换为目标值Y。 ... [详细]
  • 本文详细介绍了Java编程语言中的核心概念和常见面试问题,包括集合类、数据结构、线程处理、Java虚拟机(JVM)、HTTP协议以及Git操作等方面的内容。通过深入分析每个主题,帮助读者更好地理解Java的关键特性和最佳实践。 ... [详细]
  • XNA 3.0 游戏编程:从 XML 文件加载数据
    本文介绍如何在 XNA 3.0 游戏项目中从 XML 文件加载数据。我们将探讨如何将 XML 数据序列化为二进制文件,并通过内容管道加载到游戏中。此外,还会涉及自定义类型读取器和写入器的实现。 ... [详细]
  • 扫描线三巨头 hdu1928hdu 1255  hdu 1542 [POJ 1151]
    学习链接:http:blog.csdn.netlwt36articledetails48908031学习扫描线主要学习的是一种扫描的思想,后期可以求解很 ... [详细]
  • 本文详细介绍了如何在 Spring Boot 应用中通过 @PropertySource 注解读取非默认配置文件,包括配置文件的创建、映射类的设计以及确保 Spring 容器能够正确加载这些配置的方法。 ... [详细]
  • This document outlines the recommended naming conventions for HTML attributes in Fast Components, focusing on readability and consistency with existing standards. ... [详细]
  • 本文详细介绍了 Java 中 org.apache.xmlbeans.SchemaType 类的 getBaseEnumType() 方法,提供了多个代码示例,并解释了其在不同场景下的使用方法。 ... [详细]
  • 本文详细记录了在银河麒麟操作系统和龙芯架构上使用 Qt 5.15.2 进行项目打包时遇到的问题及解决方案,特别关注于 linuxdeployqt 工具的应用。 ... [详细]
author-avatar
智颢Tannerfm_937
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有