Android原生动画概述:
对于APP开发中涉及到的一些动画基本上都可以用Android提供的各种原生动画类来实现,所以在学习自定义动画之前首先来对原生动画进行一个基本的了解,这里不详细对每一个原生动画进行深入学习,因为重点是学会如何自定义动画,其Android支持的原生动画主要有以下三类:
①、补间动画【View Animation】:
- 平移动画:TranslateAnimation
- 旋转动画:RotateAnimation
- 缩放动画:ScaleAnimation
- 渐变动画:AlphaAnimation
②、属性动画【Property Animation】:
这个动画在实际中用得比较多的是这两个类:ObjectAnimator和ValueAnimator,而ObjectAnimator是继承自ValueAnimator,如下:
其中ValueAnimator在之前的学习中也已经用过了,它可以对值进行变化。
对于上面两种动画其实是有一个比较大的区别的,下面来做一个小实验来直观的感受一下两者的区别:
新建一个工程,先准备布局:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context="com.animationdemo.test.MainActivity"> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:onClick="traslate1" android:text="点我平移补间动画" /> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:onClick="traslate2" android:text="点我平移属性动画" /> </LinearLayout>
先看一下补间动画的效果:
public class MainActivity extends AppCompatActivity { private boolean isTranslate; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } //补间动画平移 public void traslate1(View view) { if (!isTranslate) { TranslateAnimation translateAnimation = new TranslateAnimation(TranslateAnimation.ABSOLUTE, 0, TranslateAnimation.ABSOLUTE, 100, TranslateAnimation.ABSOLUTE, 0, TranslateAnimation.ABSOLUTE, 0); translateAnimation.setDuration(1000); translateAnimation.setFillAfter(true); view.startAnimation(translateAnimation); isTranslate = true; } else { Toast.makeText(this, "点我了", Toast.LENGTH_SHORT).show(); } } //属性动画平移 public void traslate2(View view) { } }
主要是看一下事件的点击效果,编译运行:
从运行的结果来看,补间动画的平移并非真正的将View给移动了,也就是本尊未动,只是它的影子动了,接着再来看一下属性动画对于同样效果的实现,如下:
public class MainActivity extends AppCompatActivity { private boolean isTranslate; private boolean isTranslate2; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } //补间动画平移 public void traslate1(View view) { if (!isTranslate) { TranslateAnimation translateAnimation = new TranslateAnimation(TranslateAnimation.ABSOLUTE, 0, TranslateAnimation.ABSOLUTE, 100, TranslateAnimation.ABSOLUTE, 0, TranslateAnimation.ABSOLUTE, 0); translateAnimation.setDuration(1000); translateAnimation.setFillAfter(true); view.startAnimation(translateAnimation); isTranslate = true; } else { Toast.makeText(this, "点我了", Toast.LENGTH_SHORT).show(); } } //属性动画平移 @SuppressLint("ObjectAnimatorBinding") public void traslate2(View view) { if (!isTranslate2) { ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(view, "translationX", 0, 100); objectAnimator.setDuration(1000); objectAnimator.start(); isTranslate2 = true; } else { Toast.makeText(this, "点我了2", Toast.LENGTH_SHORT).show(); } } }
编译运行:
很明显这次本尊和影子都一起动了,也就是真正的移动,这也就是两者之间的一个很明显的区别。
③、帧动画【Drawable Animation】:
这个就比较简单了,其实也就是动画是由系列的图片按一定的速度进行切换而实现滴,而在Android中叫AnimationDrawable。
自定义动画引入:
在上面已经介绍了Android提供的三种原生动画,这个在之后还会具体去应用到的,但是现实应用中可能会碰到原生动画满足不需求的情况,所以此时自定义动画就派上用场了,先来看一下如下系列动画效果:
它是来自于https://github.com/81813780/AVLoadingIndicatorView这个开源项目,接下来会挑几个效果进行学习,会搭配的Android的原生动画最终实现特定的效果,首先咱们要实现一个WIFI效果,如下:
而这个是通过单纯的canvas的绘制功能来实现动画效果而并未采用Android的原生动画,所以下面开始来一步步实现这个WIFI动画效果吧!
WIFI思路整理:
在正式编码实现之前,先来对效果进行一个思路整理, 先拿一个静态图来分析:
很明显该动画是由一个扇形和三个弧形组成,而且共用一个圆心,如下:
而且该圆心并非是自定义的中心点,另外需要控制绘制格数,当绘制满格信号的时候,又得回到一格信号,所以这里存在一上计数的逻辑控制,所以大概了解了之后接下来咱们就可以具体来动手将它一一实现啦。
WIFI图形绘制:
在加入动画之前先来在咱们自定义的View上绘制出一个WIFI的静态图形,所以先新建一个自定义View:
首先先画一个一格信号的扇形,而怎么绘制扇形在之前已经学习过了【http://www.cnblogs.com/webor2006/p/7341697.html】,可以用canvas.drawArc()进行,而扇形绘制需要一个外切矩形,外切矩形的大小决定了扇形的大小,如下:
而总共有四格信号,每格信号其实就都一个弧形,所以需要对应四个外切矩形,到底对应几个可以将其定义成一个常量,如下:
接着问题的焦点就回到了如何来确定每个RectF中的left、top、right、bottom的值了,那怎么确定呢?先来分析一下效果图:
再详细一点说:
所以说,咱们首先得要计算出这个总的半径长度,而这个长度应该是依赖于整个View的长宽,直径应该是取View宽高的较小值,所以:
有了直径之后,半径不就除以2嘛,这就是最大的半径,最后还得将其均分,所以:
所以每循环一次,则将这个基本半径进行成倍增加,如下:
这样就可以来确定外切距形的左、上、右、下的值啦,如下:
关于这四个参数为啥这样传,先不用过脑想,等绘出来之后再来理解,所以下面将这些矩形绘制出来看一下效果:
为了看到效果,在布局中定义这个View,如下:
编译运行:
呃~~乌黑一片~~什么鬼~~原因是得设置一下画笔的样式,如下:
编译运行:
嗯~~为了更进一步理解这个外切矩形,咱们拆解一个个来绘制,如下:
运行:
运行:
运行:
编译运行:
理解了这个外切矩形之后,下面再将这个rectF传给画布的drawArc方法中进行弧形的绘制,如下:
那为啥startAngle是-135,sweepAngle是90呢? 其中startAngle表示弧的起始角度,而我们想要绘制的起始角度应该是在坐标系中的如下位置:
而sweepAngle表示弧的弧度,那很明显咱们要绘制的弧度的结束位置应该在坐标系的如下位置:
所以这两个值为啥这样写的原因已经说明,接下来则开始运行看一下效果:
呃~未啥是长这个样子呢?其实是因为画笔样式的原因,下面修改如下:
编译运行:
嗯~~样子有了,但是弧条不够粗,所以改下画笔的粗度呗:
编译运行:
嗯~~够粗了,但是!!目前内容显示贴着顶边的,不太好看,此时应该将其往下移动一点,具体如下:
编译运行:
但是!!关于WIFI图形的绘制这块还差最后一个细节:第一格的应该是一个扇形,其它格数都是弧,而扇形与弧的决定是由这个参数来决定的:
所以此时需要加入条件判断来着情处理,如下:
编译运行:
WIFI动画实现:
有了静态图之后,接下来就是想办法将它动起来了,这里不采用任何Android提供的原生动画来弄,而是通过纯代码,所以需要解决两个问题:
1、不断的让界面重绘,那很简单,直接用invalidate();
2、得按一定的规律一格格的往上绘制,达到最大格数之后则需要又回到第一格,很显然需要用一个变量来控制当前绘制的格数,如下:
而每次绘制都是有一个for循环,所以需要通过一定的算法来控制咱们的绘制,具体如何做呢?下面直接给出:
另外每次绘制还得更改一下shouldExistSigalSize的大小,所以:
接下来得用一个定时器不断去让View重绘,有很多方法,这里直接用Handler来弄,可以这样做,如下:
此时编译运行:
嗯~~貌似不错~~但是还是存在一个小BUG的,第一次进来就绘制了两格,所以解决它很解决,只要改变初始化的格数既可,如下:
为何要将其改成4既可呢?因为:
所以,此时再编译运行就木问题了:
嗯~~不过差完美还有一步,资源的回收,此时如果将Activity退出,其刷新线程还在不断进行,如下:
所以接下来对资源进行回收,比较简单:
/** * WIFI动画:采用自定义View的方式来实现,而非用Android提供的原生动画 */ public class WiFi extends View { //constants /* 总共有几格信号 */ private static final float SIGNAL_SIZE = 4F; //variables private Paint paint; /* 绘制的基准长度:取屏幕宽高的较少值 */ private int baseLength; /* 当前信号存在的数量,默认从1格信号开始 */ private float shouldExistSinalSize = 4; private Handler handler = new Handler(); private boolean isExit; public WiFi(Context context) { this(context, null); } public WiFi(Context context, @Nullable AttributeSet attrs) { this(context, attrs, -1); } public WiFi(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { paint = new Paint(); paint.setColor(Color.BLACK); paint.setAntiAlias(true); paint.setStrokeWidth(4); //不断的进行绘制 handler.postDelayed(new Runnable() { @Override public void run() { if (isExit) return; Log.e("cexo", "handler run()..."); invalidate(); handler.postDelayed(this, 1000); } }, 1000); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); //绘制应该是在屏幕较小的长度来进行,所以先取出最小值,在绘制时需要参考该值 baseLength = Math.min(w, h); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); shouldExistSinalSize++; if (shouldExistSinalSize > 4) { shouldExistSinalSize = 1;//如果已经绘制四格信号了,那么下一次则又回到一格信号状态 } canvas.save(); canvas.translate(0, baseLength / SIGNAL_SIZE);//将画布往下移动一点,以防绘制太贴边 //根据信号的格数来进行相应的绘制 RectF rectF; //计算出一个基准圆半径 float baseRadius = baseLength / 2 / SIGNAL_SIZE;//算出平均的半径 for (int i = 0; i < SIGNAL_SIZE; i++) { if (i >= SIGNAL_SIZE - shouldExistSinalSize) { float radius = baseRadius * i; rectF = new RectF(radius, radius, baseLength - radius, baseLength - radius); if (i < SIGNAL_SIZE - 1) {//此时需绘制一个弧形 paint.setStyle(Paint.Style.STROKE);//设置画笔样式为空心样式 canvas.drawArc(rectF, -135, 90, false, paint); } else {//最下面则需要绘制一个扇形 paint.setStyle(Paint.Style.FILL);//设置画笔样式为实心样式 canvas.drawArc(rectF, -135, 90, true, paint); } } } canvas.restore(); } public void onDestroy() { isExit = true; } }
public class MainActivity extends AppCompatActivity { private WiFi wifi; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); wifi = findViewById(R.id.wifi); } @Override protected void onDestroy() { super.onDestroy(); wifi.onDestroy(); } }
至此wifi信号的效果就完美实现,看似简单的效果其实现并非简单,可见也花的篇幅也不小。