动画学习之WIFI图形绘制

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信号的效果就完美实现,看似简单的效果其实现并非简单,可见也花的篇幅也不小。

原文地址:https://www.cnblogs.com/webor2006/p/8322387.html