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);
 }
}

自定义属性如下

<declare-styleable name="CircleCountDownView">
 <!--控件中间图片资源-->
 <attr name="circleImgSrc" format="reference" />
 <attr name="circleBorderColor" format="color" />
 <attr name="circleBorderWidth" format="dimension" />
 <attr name="valueTextSize" format="dimension" />
 <attr name="valueTextColor" format="color" />
 <attr name="padding" format="dimension" />
 <attr name="showProgress" format="boolean" />
 <attr name="processColorStart" format="color" />
 <attr name="processColorEnd" format="color" />
 <attr name="processBlurMaskRadius" format="dimension" />
 </declare-styleable>

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

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

总结

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

(0)

相关推荐

  • Android自带倒计时控件Chronometer使用方法详解

    公司的以前的项目,看到使用了这个Android自带的倒计时控件Chronometer,现在整合了一下 先看看效果: <Chronometer android:id="@+id/chronometer" android:layout_width="wrap_content" android:layout_height="30dp" /> <Button android:onClick="start" andro

  • Android仿Keep运动休息倒计时圆形控件

    仿Keep运动休息倒计时控件,供大家参考,具体内容如下 源码 控件本身非常非常简单,唯一难点在于倒计时期间动态增减时长,如果说动态增减时长是瞬间完成的,倒也没什么难度,但是如果是需要花一定时间做动画的话(见效果图),考虑的逻辑就变多了,这也是我写这个的目的,对应源码中就是plus这个方法.地址: KeepCountdownView 效果 使用方法 xml: <com.KeepCountdownView.KeepCountdownView android:id="@+id/keep1&quo

  • Android自定义DigitalClock控件实现商品倒计时

    本文实例为大家分享了DigitalClock实现商品倒计时的具体代码,供大家参考,具体内容如下 自定义DigitalClock控件: package com.veally.timesale; import java.util.Calendar; import android.content.Context; import android.database.ContentObserver; import android.os.Handler; import android.os.SystemClo

  • android自定义倒计时控件示例

    自定义TextView控件TimeTextView代码: 复制代码 代码如下: import android.content.Context;import android.content.res.TypedArray;import android.graphics.Paint;import android.text.Html;import android.util.AttributeSet;import android.widget.TextView; import com.new0315.R;

  • android倒计时控件示例

    本文为大家分享了android倒计时控件,供大家参考,具体代码如下 /* * Copyright (C) 2012 The * Project * All right reserved. * Version 1.00 2012-2-11 * Author veally@foxmail.com */ package com.ly.sxh.view; import android.content.Context; import android.database.ContentObserver; im

  • Android自定义控件实现验证码倒计时

    今天给大家带来一个新的控件--验证码倒计时,先看下效果图 1 效果演示 2 使用方式 <com.landptf.view.CountDownM android:id="@+id/cdm_identifying_code" android:layout_width="wrap_content" android:layout_height="50dp" android:layout_alignParentRight="true&quo

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

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

  • Android自定义view实现滚动选择控件详解

    目录 前言 需求 编写代码 主要问题 前言 上篇文章通过一个有header和footer的滚动控件(Viewgroup)学了下MeasureSpec.onMeasure以及onLayout,接下来就用一个滚动选择的控件(View)来学一下onDraw的使用,并且了解下在XML自定义控件参数. 需求 这里就是一个滚动选择文字的控件,还是挺常见的,之前用别人的,现在选择手撕一个,核心思想如下: 1.有三层不同大小及透明度的选项,选中项放在中间 2.接受一个列表的数据,静态时显示三个值,滚动时显示四个

  • Android ToolBar控件详解及实例

    ToolBar控件详解 在Activity中添加ToolBar 1.添加库 dependencies { ... compile "com.android.support:appcompat-v7:18.0.+" } 2.Activity要继承AppCompatActivity 3.设置主题 使用ToolBar,要将系统默认的ActionBar隐藏掉 <application android:theme="@style/Theme.AppCompat.Light.NoA

  • Android Tab 控件详解及实例

    Android Tab 控件详解及实例 在桌面应用中Tab控件使用得非常普遍,那么我们经常在Android中也见到以Tab进行布局的客户端.那么Android中的Tab是如何使用的呢? 1.Activity package com.wicresoft.activity; import com.wicresoft.myandroid.R; import android.app.TabActivity; import android.os.Bundle; import android.util.Lo

  • Android Studio 中aidl的自定义类的使用详解

    自己折腾了好久,记录一下. service端: 1:创建类Dog,需要实现Parcelable接口: 2:aidl下创建 Dog.aidl,里面两句话就可以了 (1)package s包名; (2)parcelable Dog; 3:interface.aidl引入Dog类, import s包名.Dog; Client 端: 1:创建类Dog,需要实现Parcelable接口: 2:aidl下创建 Dog.aidl, (1)package c包名; (2)parcelable Dog; 注意:

  • Android实现可滑动的自定义日历控件

    最近用到的一个日历控件,记录下,效果如图 代码下载地址:点击打开链接 布局文件 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical&q

  • Android实现自定义轮播图片控件详解

    首先上效果图 实现原理 要完成一个轮播图片,首先想到的应该是使用ViewPager来实现.ViewPager已经有了滑动的功能,我们只要让它自己滚动.再加上下方的小圆点就行了.所以我们本次的自定义控件就是由ViewPager和LinearLayout叠加起来组成的. 一.创建一个自定义的ViewPager 先上完整的代码 package com.kcode.autoscrollviewpager.view; import android.content.Context; import andro

  • Android实现旋转动画的两种方式案例详解

    目录 练习案例 效果展示 前期准备 自定义 View java代码编写 方法一 方法二 易错点总结: 练习案例 视差动画 - 雅虎新闻摘要加载 效果展示 前期准备 第一步:准备好颜色数组 res => values => colors.xml <color name="orange">#FF9600</color> <color name="aqua">#02D1AC</color> <color n

  • WPF自定义选择年月控件详解

    本文实例为大家分享了WPF自定义选择年月控件的具体代码,供大家参考,具体内容如下 封装了一个选择年月的控件,XAML代码: <UserControl x:Class="SunCreate.CombatPlatform.Client.DateMonthPicker" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.micr

随机推荐