自定义View+属性动画实战 — 灵动的锦鲤

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了自定义View+属性动画实战 — 灵动的锦鲤相关的知识,希望对你有一定的参考价值。

参考技术A

通过自定义View+属性动画 实现一个会动鱼

分析:
1.画一条鱼
2.鱼原地动
3.鱼向点击处游动

鱼分为:鱼头(圆) + 身体(两条直线+两条贝塞尔曲线) + 鱼鳍(一条直线+一个贝塞尔)+尾巴(两三角)+节肢*2 (梯形+两圆)

先把鱼水平朝右,画一个坐标系,鱼的重心为坐标系中心

头圆半径的4.19倍,这个其实是自己定的,5f,6f都行,就是只改变鱼的长度,用鱼头半径做初始单位有利于改变整个鱼的大小。

初中知识:

所以可以得出: 入参一个点、两点长度、对于x轴的角度。返回值 一个坐标

定义画笔
mPaint.setDither(true);防抖动
mPaint.setAntiAlias(true); 抗锯齿

画鱼头:找到鱼头圆心,入参:重心、鱼身长一半、鱼的朝向(默认180跟重心一个方向)

画鱼鳍

鱼鳍是一个直线+一个二阶贝塞尔曲线,所以重点就是求出三个点:鱼鳍左点、右点、贝塞尔控制点

通过鱼头的圆心求,距离 0.9 * R ,角度110 。通过那个公式就能求出来 = 右鱼鳍点
左鱼鳍点 = 右鱼鳍点、距离、角度-180
贝塞尔控制点 = (这个完全靠自己试,只影响鱼鳍的胖瘦) 右鱼鳍点、距离 * 1.8f、角度 115
试贝塞尔的网站: cubic-bezier.com/#.17,.67,.8…

三个点都有了,绘制鱼鳍:
绘制之前要将其他绘制重置: mPath.reset();
然后mPath.moveTo()移动到第一个点
mPath.lineTo()画直线
mPath.quadTo()画二阶贝塞尔曲线,入参第二个点、第三个点
最后 canvas.drawPath(mPath,mPaint);直接画出来
最后一个点不用封闭,系统自动会封闭。

别的部分也都差不多,根据那个公式,通过参考点求出另一个点,然后求出各个部分的点,最后连线画就行

然后
mPath.reset()、 mPath.moveTo、 mPath.lineTo、canvas.drawPath

画身体

求出这6个点就行

鱼原地摆动需要属性动画ValueAnimator

属性动画,给一个值,一直变,
如: ValueAnimator.ofFloat(0,1f); 就是将一个float值 从0f变到1f

setRepeatCount设置重复次数
setRepeatMode设置重复模式
setInterpolator设置插值器
Interpolator(插值器)系统提供了很多插值器:先加速在减速、加速、匀速、周期运动、先回退再加速、最后弹一下等等。如果不够用可以自定义。

在监听里面能取到当前的值,然后通过 invalidateSelf();刷新重绘

通过这个变化的值,去改变鱼头的角度,这能实现鱼的摆动(比如鱼头摆动5度,节肢1摆动10度,节肢2摆动20度)这样不同的幅度就让鱼动起来。
鱼尾是通过改变三角的大小来实现的,幅度规律应该跟节肢2一样。
鱼尾是上节(节肢1)带动(节肢2)来运动的,而且是周期规律性的运动
说到周期性运动,又很平滑。sin、cos刚好是这样的

所以可以用sin代替0~1f的动画,这样更加平滑。cos刚好比sin多一个象限,那么节肢2刚好可以被节肢1带着。

鱼整条都是定义在FishDrawable的,用FishRelativeLayout,addView去将ImageView加进来FishDrawable

在onTouchEvent里记录下点击(X,Y),然后根据属性动画去改变圆半径和透明度

ObjectAnimator

ObjectAnimator extends ValueAnimator
效果是一样的,将一个属性从 X值改到Y值。可以不用写监听了addListener
入参1.要改变的对象,2.要改变的值(必须实现set方法),3.初始值,4.终点值 -- (或者路径Path)

本质其实是利用反射将 第二个参数的set、get方法里的值给改了。所以一定得实现set、get,不然就报错

鱼的运行轨迹为 3阶贝塞尔曲线,所以关键就是确定4个点

重点就是控制点2的角度

求出夹角,还得算出与X轴的夹角也就是
// AB连线与X的夹角的tan值 - OB与x轴的夹角的tan值 float direction = (A.y - B.y) / (A.x - B.x) - (O.y - B.y) / (O.x - B.x);

完整各个点:

最后游动:
path.cubicTo()三阶贝塞尔曲线公式
通过ObjectAnimator属性动画改变ImageView的xy坐标。
通过fishDrawable.setFrequence(3f) 让鱼在游动的时候运动的更快

鱼头的朝向,转过来

鱼头的方向 = 运动轨迹的切线的方向,这样就能平滑的转过来了。
通过取到曲线执行的百分比得到
pathMeasure.getPosTan()
tanα值,然后转换成角度

完成

Android自定义View实战之仿百度加载动画,一种优雅的Loading方式

转载请注明出处:http://blog.csdn.net/linglongxin24/article/details/53470872
本文出自【DylanAndroid的博客】


【Android自定义View实战】之仿百度加载动画,一种优雅的Loading方式

无意中看到了百度的加载动画,看起来非常优雅,打算亲手造一个。

仿百度加载动画的重要思路:当第一遍执行完毕后就让第一个停下来在中间位置,换原来中间位置的第三个开始执行动画,
以此类推,当第二遍执行完毕后第二个停下来,中间位置的开始执行动画。

第一个:仿百度加载动画,用ObjectAnimator属性动画操作ImageView的属性方法实现:

  • 1.布局文件
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="500px"
    android:layout_height="500px"
    android:orientation="vertical">

    <ImageView
        android:id="@+id/iv_blue"
        android:layout_width="wrap_content"
        android:scaleType="matrix"
        android:layout_height="wrap_content"
        android:src="@mipmap/dot_blue"
        android:layout_gravity="center" />
    <ImageView
        android:id="@+id/iv_yellow"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:scaleType="matrix"
        android:src="@mipmap/dot_yellow"
        android:layout_gravity="center" />
    <ImageView
        android:id="@+id/iv_red"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:scaleType="matrix"
        android:src="@mipmap/dot_red"
        android:layout_gravity="center" />
</FrameLayout>
  • 2.代码
package cn.bluemobi.dylan.baiduprogressbar;

import android.animation.Animator;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.animation.LinearInterpolator;
import android.widget.FrameLayout;
import android.widget.ImageView;

import java.util.ArrayList;
import java.util.List;

/**
 * 仿百度优雅的加载动画
 * Created by dylan on 2016-12-04.
 */

public class BaiduProgressBar extends FrameLayout 
    /**
     * 开始执行的第一个动画的索引,
     * 由于第一个和第二个同时当执行,
     * 当第一遍执行完毕后就让第一个停下来在中间位置,换原来中间位置的第三个开始执行动画,
     * 以此类推,当第二遍执行完毕后第二个停下来,中间位置的开始执行动画。
     */
    private int startIndex = 0;
    /**
     * 交换执行动画的源图片数组
     */
    private int[] src = new int[]R.mipmap.dot_yellow, R.mipmap.dot_red, R.mipmap.dot_blue;
    /**
     * 存放三个ImageView的的集合
     */
    private List<ImageView> views = new ArrayList<>();
    /**
     * 让左边和右边动画同时执行的AnimatorSet对象
     */
    private AnimatorSet animatorSet;

    /**
     * 动画所执行的最大半径(即中间点和最左边的距离)
     */
    private int maxRadius = 200;

    public BaiduProgressBar(Context context) 
        super(context);
        init();
    

    public BaiduProgressBar(Context context, AttributeSet attrs) 
        super(context, attrs);
        init();
    

    public BaiduProgressBar(Context context, AttributeSet attrs, int defStyleAttr) 
        super(context, attrs, defStyleAttr);
        init();
    

    /**
     * 查找布局控件
     */
    private void assignViews() 
        ImageView iv_blue = (ImageView) findViewById(R.id.iv_blue);
        ImageView iv_yellow = (ImageView) findViewById(R.id.iv_yellow);
        ImageView iv_red = (ImageView) findViewById(R.id.iv_red);
        views.add(iv_yellow);
        views.add(iv_red);
        views.add(iv_blue);
    

    /**
     * 初始化
     */
    private void init() 
        LayoutInflater.from(getContext()).inflate(R.layout.baidu_progress_bar, this, true);
        assignViews();
        startAnimator();
    

    /**
     * 开始执行动画
     */
    private void startAnimator() 
        /**向左来回移动的X位移动画**/
        ObjectAnimator objectAnimatorLeft = ObjectAnimator.ofFloat(views.get(0), "translationX", 0, -maxRadius, 0);
        objectAnimatorLeft.setRepeatCount(-1);
        objectAnimatorLeft.setDuration(1000);

        /**向右来回移动的X位移动画**/
        ObjectAnimator  objectAnimatorRight = ObjectAnimator.ofFloat(views.get(1), "translationX", 0, maxRadius, 0);
        objectAnimatorRight.setRepeatCount(-1);
        objectAnimatorRight.setDuration(1000);

        /**动画组合->让左右同时执行**/
        animatorSet = new AnimatorSet();
        animatorSet.play(objectAnimatorRight).with(objectAnimatorLeft);
        animatorSet.setInterpolator(new LinearInterpolator());
        animatorSet.start();

        /**动画监听**/
        objectAnimatorLeft.addListener(new Animator.AnimatorListener() 
            @Override
            public void onAnimationStart(Animator animation) 

            

            @Override
            public void onAnimationEnd(Animator animation) 
            

            @Override
            public void onAnimationCancel(Animator animation) 

            

            @Override
            public void onAnimationRepeat(Animator animation) 
                /**每次记录一下下次应该停止在中间的Image索引,然后和中间的交换**/
                if (startIndex == 0) 
                    sweep(0, 2);
                    startIndex = 1;
                 else 
                    sweep(1, 2);
                    startIndex = 0;
                
            
        );

    

    /**
     * 每次让先执行动画的目标和中间停止的动画目标交换
     *
     * @param a 最先执行的动画的索引
     * @param b 在中间动画的索引
     */
    private void sweep(int a, int b) 
        views.get(a).setImageResource(src[b]);
        views.get(b).setImageResource(src[a]);
        int temp = src[b];
        src[b] = src[a];
        src[a] = temp;
    

    /**
     * 在View销毁时停止动画
     */
    @Override
    protected void onDetachedFromWindow() 
        super.onDetachedFromWindow();
        animatorSet.cancel();
    

第二个:仿百度加载动画第二种实现方式,用ValueAnimator+原生的ondraw()方法实现:

package cn.bluemobi.dylan.baiduprogressbar;

import android.animation.Animator;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.animation.LinearInterpolator;
import android.widget.FrameLayout;
import android.widget.ImageView;

import java.util.ArrayList;
import java.util.List;

/**
 * 仿百度优雅的加载动画
 * Created by dylan on 2016-12-04.
 */

public class BaiduProgressBar2 extends View 
    /**
     * 开始执行的第一个动画的索引,
     * 由于第一个和第二个同时当执行,
     * 当第一遍执行完毕后就让第一个停下来在中间位置,换原来中间位置的第三个开始执行动画,
     * 以此类推,当第二遍执行完毕后第二个停下来,中间位置的开始执行动画。
     */
    private int sweepIndex = 0;
    /**
     * 交换执行动画的颜色数组
     */
    private int[] colors = new int[]getResources().getColor(R.color.colorYellow),
            getResources().getColor(R.color.colorRed),
            getResources().getColor(R.color.colorBlue);

    /**
     * 动画所执行的最大偏移量(即中间点和最左边的距离)
     */
    private Float maxWidth = 200f;

    /**
     * 三个圆的半径
     */
    private Float radius = 30f;

    /**
     * 当前偏移的X坐标
     */
    private Float currentX = 0f;
    /**
     * 画笔
     */
    private Paint paint;
    /**
     * 属性动画
     */
    private ValueAnimator valueAnimator;

    public BaiduProgressBar2(Context context) 
        super(context);
        startAnimator();
    

    public BaiduProgressBar2(Context context, AttributeSet attrs) 
        super(context, attrs);
        startAnimator();
    

    public BaiduProgressBar2(Context context, AttributeSet attrs, int defStyleAttr) 
        super(context, attrs, defStyleAttr);
        startAnimator();
    

    /**
     * 用属性动画实现位移动画
     */
    private void startAnimator() 
        valueAnimator = ValueAnimator.ofFloat(0f, maxWidth, 0);
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() 
            @Override
            public void onAnimationUpdate(ValueAnimator animation) 
                currentX = (Float) animation.getAnimatedValue();
                invalidate();
            
        );
        valueAnimator.addListener(new Animator.AnimatorListener() 
            @Override
            public void onAnimationStart(Animator animation) 

            

            @Override
            public void onAnimationEnd(Animator animation) 

            

            @Override
            public void onAnimationCancel(Animator animation) 

            

            @Override
            public void onAnimationRepeat(Animator animation) 
                sweep(sweepIndex);
            
        );
        valueAnimator.setInterpolator(new LinearInterpolator());
        paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        valueAnimator.setRepeatCount(-1);
        valueAnimator.setRepeatMode(ValueAnimator.REVERSE);
        valueAnimator.setDuration(1000);
        valueAnimator.start();
    

    @Override
    protected void onDraw(Canvas canvas) 
        super.onDraw(canvas);
        int centerX = getWidth() / 2;
        int centerY = getHeight() / 2;

        /**画左边的圆**/
        paint.setColor(colors[0]);
        canvas.drawCircle(centerX - currentX, centerY, radius, paint);

        /**画右边的圆**/
        paint.setColor(colors[1]);
        canvas.drawCircle(centerX + currentX, centerY, radius, paint);

        /**画中间的圆**/
        paint.setColor(colors[2]);
        canvas.drawCircle(centerX, centerY, radius, paint);

    

    /**
     * 每次让先执行动画的目标和中间停止的动画目标交换
     *
     * @param a 最先执行的动画的索引
     */
    private void sweep(int a) 
        int temp = colors[2];
        colors[2] = colors[a];
        colors[a] = temp;

        if (a == 0) 
            sweepIndex = 1;
         else 
            sweepIndex = 0;
        
    

    /**
     * 在View销毁时停止动画
     */
    @Override
    protected void onDetachedFromWindow() 
        super.onDetachedFromWindow();
        valueAnimator.cancel();
    

在经过以上的动画之后,突然在Loading设计思路分享中看到了两个比较酷炫的动画
主要思路图如下



第三个:扔球动画->水平旋转动画

package cn.bluemobi.dylan.baiduprogressbar;

import android.animation.Animator;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.PropertyValuesHolder;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.animation.LinearInterpolator;
import android.widget.FrameLayout;
import android.widget.ImageView;

import java.util.ArrayList;
import java.util.List;

/**
 * Created by dylan on 2016-12-04.
 */

public class BaiduLoadingView extends FrameLayout 
    /**
     * 存放三个小球的集合
     */
    private List<ImageView> views = new ArrayList<>();
    /**
     * 同时播放动画的对象
     */
    private AnimatorSet animatorSet;

    public BaiduLoadingView(Context context) 
        super(context);
        init();
    

    public BaiduLoadingView(Context context, AttributeSet attrs) 
        super(context, attrs);
        init();
    

    public BaiduLoadingView(Context context, AttributeSet attrs, int defStyleAttr) 
        super(context, attrs, defStyleAttr);
        init();
    

    private void assignViews() 
        ImageView iv_blue = (ImageView) findViewById(R.id.iv_blue);
        ImageView iv_yellow = (ImageView) findViewById(R.id.iv_yellow);
        ImageView iv_red = (ImageView) findViewById(R.id.iv_red);
        views.add(iv_yellow);
        views.add(iv_red);
        views.add(iv_blue);
    

    /**
     * 初始化
     */
    private void init() 
        LayoutInflater.from(getContext()).inflate(R.layout.baidu_progress_bar, this, true);
        assignViews();
        startAnimator();
    

    private void startAnimator() 
        /**动画组合->让左右同时执行**/
        animatorSet = new AnimatorSet();
        animatorSet.play(startAnimator1()).with(startAnimator2()).with(startAnimator3());
        animatorSet.setInterpolator(new LinearInterpolator());
        animatorSet.start();
    

    private ObjectAnimator startAnimator1() 
        /**对象的不同属性组合**/
        PropertyValuesHolder objectAnimatorTranslation = PropertyValuesHolder.ofFloat("translationX", -100, -200, -100, 0, 100, 200, 100, 0, -100);
        PropertyValuesHolder objectAnimatorScale = PropertyValuesHolder.ofFloat("scaleX", 0.5f, 1, 1.5f, 1, 0.5f, 1, 1.5f, 1, 0.5f);
        PropertyValuesHolder objectAnimatorScaleY = PropertyValuesHolder.ofFloat("scaleY", 0.5f, 1, 1.5f, 1, 0.5f, 1, 1.5f, 1, 0.5f);
        /**同时操作对象的两个属性动画**/
        ObjectAnimator objectAnimator = ObjectAnimator.ofPropertyValuesHolder(views.get(0), objectAnimatorTranslation, objectAnimatorScale, objectAnimatorScaleY);
        objectAnimator.setRepeatCount(-1);
        objectAnimator.setInterpolator(new LinearInterpolator());
        objectAnimator.setDuration(2000);
        objectAnimator.start();
        return objectAnimator;

    

    private ObjectAnimator startAnimator2() 
        /**对象的不同属性组合**/
        PropertyValuesHolder objectAnimatorTranslation = PropertyValuesHolder.ofFloat("translationX", 0, 100, 200, 100, 0, -100, -200, -100, 0);
        PropertyValuesHolder objectAnimatorScale = PropertyValuesHolder.ofFloat("scaleX", 1, 0.5f, 1, 1.5f, 1, 0.5f, 1, 1.5f, 1);
        PropertyValuesHolder objectAnimatorScaleY = PropertyValuesHolder.ofFloat("scaleY", 1, 0.5f, 1, 1.5f, 1, 0.5f, 1, 1.5f, 1);
        /**同时操作对象的两个属性动画**/
        ObjectAnimator objectAnimator = ObjectAnimator.ofPropertyValuesHolder(views.get(1), objectAnimatorTranslation, objectAnimatorScale, objectAnimatorScaleY);
        objectAnimator.setRepeatCount(-1);
        objectAnimator.setInterpolator(new LinearInterpolator());
        objectAnimator.setDuration(2000);
        objectAnimator.start();
        return objectAnimator;

    

    private ObjectAnimator startAnimator3() 
        /**对象的不同属性组合**/
        PropertyValuesHolder objectAnimatorTranslation = PropertyValuesHolder.ofFloat("translationX", 100, 0, -100, -200, -100, 0, 100, 200, 100);
        PropertyValuesHolder objectAnimatorScale = PropertyValuesHolder.ofFloat("scaleX", 1.5f, 1f, 0.5f, 1, 1.5f, 1, 0.5f, 1, 1.5f);
        PropertyValuesHolder objectAnimatorScaleY = PropertyValuesHolder.ofFloat("scaleY", 1.5f, 1f, 0.5f, 1, 1.5f, 1, 0.5f, 1, 1.5f);
        /**同时操作对象的两个属性动画**/
        ObjectAnimator objectAnimator = ObjectAnimator.ofPropertyValuesHolder(views.get(2), objectAnimatorTranslation, objectAnimatorScale, objectAnimatorScaleY);
        objectAnimator.setRepeatCount(-1);
        objectAnimator.setInterpolator(new LinearInterpolator());
        objectAnimator.setDuration(2000);
        objectAnimator.start();
        return objectAnimator;
    

    /**
     * 在View销毁时停止动画
     */
    @Override
    protected void onDetachedFromWindow() 
        super.onDetachedFromWindow();
        animatorSet.cancel();
    

第四个:扔球动画->垂直旋转动画

package cn.bluemobi.dylan.baiduprogressbar;

import android.animation.Animator;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.PropertyValuesHolder;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.PointF;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.animation.LinearInterpolator;
import android.widget.FrameLayout;
import android.widget.ImageView;

import java.util.ArrayList;
import java.util.List;

/**
 * Created by dylan on 2016-12-04.
 */

public class BaiduProgressLoading extends FrameLayout 

    /**
     * 存放三个小球的集合
     */
    private List<ImageView> views = new ArrayList<>();

    public BaiduProgressLoading(Context context) 
        super(context);
        init();
    

    public BaiduProgressLoading(Context context, AttributeSet attrs) 
        super(context, attrs);
        init();
    

    public BaiduProgressLoading(Context context, AttributeSet attrs, int defStyleAttr) 
        super(context, attrs, defStyleAttr);
        init();
    

    private void assignViews() 
        ImageView iv_blue = (ImageView) findViewById(R.id.iv_blue);
        ImageView iv_yellow = (ImageView) findViewById(R.id.iv_yellow);
        ImageView iv_red = (ImageView) findViewById(R.id.iv_red);
        views.add(iv_yellow);
        views.add(iv_red);
        views.add(iv_blue);
    

    private void init() 
        LayoutInflater.from(getContext()).inflate(R.layout.baidu_progress_bar, this, true);
        assignViews();
        startAnimator1();
        startAnimator2();
        startAnimator3();
    

    PointF point = new PointF();

    private void startAnimator1() 
        ValueAnimator valueAnimator = ValueAnimator.ofFloat(90, 360);
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() 
            @Override
            public void onAnimationUpdate(ValueAnimator animation) 
                float value = (Float) animation.getAnimatedValue();
                /**
                 * 
                 * 圆点坐标:(x0,y0)
                 * 半径:r
                 * 角度:a0
                 * 则圆上任一点为:(x1,y1)
                 * x1   =   x0   +   r   *   cos(ao   *   3.14   /180   )
                 * y1   =   y0   +   r   *   sin(ao   *   3.14   /180   )
                 */
                /**第四步,根据每个菜单真实角度计算其坐标值**/
                point.x = (float) Math.cos(value * (Math.PI / 180)) * 100 - 100;
                point.y = (float) -Math.sin(value * (Math.PI / 180)) * 100;
                views.get(0).setTranslationX(point.x);
                views.get(0).setTranslationY(point.y);
            
        );
        valueAnimator.setInterpolator(new LinearInterpolator());
        valueAnimator.setDuration(750);
        valueAnimator.start();

        ValueAnimator valueAnimator2 = ValueAnimator.ofFloat(180, 0, -180);
        valueAnimator2.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() 
            @Override
            public void onAnimationUpdate(ValueAnimator animation) 
                float value = (Float) animation.getAnimatedValue();
                /**
                 * 
                 * 圆点坐标:(x0,y0)
                 * 半径:r
                 * 角度:a0
                 * 则圆上任一点为:(x1,y1)
                 * x1   =   x0   +   r   *   cos(ao   *   3.14   /180   )
                 * y1   =   y0   +   r   *   sin(ao   *   3.14   /180   )
                 */
                /**第四步,根据每个菜单真实角度计算其坐标值**/
                point.x = (float) Math.cos(value * (Math.PI / 180)) * 100 + 100;
                point.y = (float) -Math.sin(value * (Math.PI / 180)) * 100;
                views.get(0).setTranslationX(point.x);
                views.get(0).setTranslationY(point.y);
            
        );
        valueAnimator2.setInterpolator(new LinearInterpolator());
        valueAnimator2.setDuration(1000);
        valueAnimator2.setStartDelay(750);
        valueAnimator2.start();


        ValueAnimator valueAnimator3 = ValueAnimator.ofFloat(0, 90);
        valueAnimator3.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() 
            @Override
            public void onAnimationUpdate(ValueAnimator animation) 
                float value = (Float) animation.getAnimatedValue();
                /**
                 * 
                 * 圆点坐标:(x0,y0)
                 * 半径:r
                 * 角度:a0
                 * 则圆上任一点为:(x1,y1)
                 * x1   =   x0   +   r   *   cos(ao   *   3.14   /180   )
                 * y1   =   y0   +   r   *   sin(ao   *   3.14   /180   )
                 */
                /**第四步,根据每个菜单真实角度计算其坐标值**/
                point.x = (float) Math.cos(value * (Math.PI / 180)) * 100 - 100;
                point.y = (float) -Math.sin(value * (Math.PI / 180)) * 100;
                views.get(0).setTranslationX(point.x);
                views.get(0).setTranslationY(point.y);
            
        );
        valueAnimator3.setInterpolator(new LinearInterpolator());
        valueAnimator3.setDuration(250);
        valueAnimator3.setStartDelay(1750);
        valueAnimator3.start();
        valueAnimator3.addListener(new Animator.AnimatorListener() 
            @Override
            public void onAnimationStart(Animator animation) 

            

            @Override
            public void onAnimationEnd(Animator animation) 
                startAnimator1();
            

            @Override
            public void onAnimationCancel(Animator animation) 

            

            @Override
            public void onAnimationRepeat(Animator animation) 

            
        );
    

    private void startAnimator2() 

        ValueAnimator valueAnimator2 = ValueAnimator.ofFloat(180, 0, -180);
        valueAnimator2.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() 
            @Override
            public void onAnimationUpdate(ValueAnimator animation) 
                float value = (Float) animation.getAnimatedValue();
                /**
                 * 
                 * 圆点坐标:(x0,y0)
                 * 半径:r
                 * 角度:a0
                 * 则圆上任一点为:(x1,y1)
                 * x1   =   x0   +   r   *   cos(ao   *   3.14   /180   )
                 * y1   =   y0   +   r   *   sin(ao   *   3.14   /180   )
                 */
                /**第四步,根据每个菜单真实角度计算其坐标值**/
                point.x = (float) Math.cos(value * (Math.PI / 180)) * 100 + 100;
                point.y = (float) -Math.sin(value * (Math.PI / 180)) * 100;
                views.get(1).setTranslationX(point.x);
                views.get(1).setTranslationY(point.y);
            
        );
        valueAnimator2.setInterpolator(new LinearInterpolator());
        valueAnimator2.setDuration(1000);
        valueAnimator2.start();

        ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 360);
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() 
            @Override
            public void onAnimationUpdate(ValueAnimator animation) 
                float value = (Float) animation.getAnimatedValue();
                /**
                 * 
                 * 圆点坐标:(x0,y0)
                 * 半径:r
                 * 角度:a0
                 * 则圆上任一点为:(x1,y1)
                 * x1   =   x0   +   r   *   cos(ao   *   3.14   /180   )
                 * y1   =   y0   +   r   *   sin(ao   *   3.14   /180   )
                 */
                /**第四步,根据每个菜单真实角度计算其坐标值**/
                point.x = (float) Math.cos(value * (Math.PI / 180)) * 100 - 100;
                point.y = (float) -Math.sin(value * (Math.PI / 180)) * 100;
                views.get(1).setTranslationX(point.x);
                views.get(1).setTranslationY(point.y);
            
        );
        valueAnimator.setInterpolator(new LinearInterpolator());
        valueAnimator.setDuration(1000);
        valueAnimator.setStartDelay(1000);
        valueAnimator.start();
        valueAnimator.addListener(new Animator.AnimatorListener() 
            @Override
            public void onAnimationStart(Animator animation) 

            

            @Override
            public void onAnimationEnd(Animator animation) 
                startAnimator2();
            

            @Override
            public void onAnimationCancel(Animator animation) 

            

            @Override
            public void onAnimationRepeat(Animator animation) 

            
        );
    

    private void startAnimator3() 

        ValueAnimator valueAnimator = ValueAnimator.ofFloat(270, 180);
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() 
            @Override
            public void onAnimationUpdate(ValueAnimator animation) 
                float value = (Float) animation.getAnimatedValue();
                /**
                 * 
                 * 圆点坐标:(x0,y0)
                 * 半径:r
                 * 角度:a0
                 * 则圆上任一点为:(x1,y1)
                 * x1   =   x0   +   r   *   cos(ao   *   3.14   /180   )
                 * y1   =   y0   +   r   *   sin(ao   *   3.14   /180   )
                 */
                /**第四步,根据每个菜单真实角度计算其坐标值**/
                point.x = (float) Math.cos(value * (Math.PI / 180)) * 100 + 100;
                point.y = (float) -Math.sin(value * (Math.PI / 180)) * 100;
                views.get(2).setTranslationX(point.x);
                views.get(2).setTranslationY(point.y);
            
        );
        valueAnimator.setInterpolator(new LinearInterpolator());
        valueAnimator.setDuration(250);
        valueAnimator.start();

        ValueAnimator valueAnimator2 = ValueAnimator.ofFloat(0, 360);
        valueAnimator2.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() 
            @Override
            public void onAnimationUpdate(ValueAnimator animation) 
                float value = (Float) animation.getAnimatedValue();
                /**
                 * 
                 * 圆点坐标:(x0,y0)
                 * 半径:r
                 * 角度:a0
                 * 则圆上任一点为:(x1,y1)
                 * x1   =   x0   +   r   *   cos(ao   *   3.14   /180   )
                 * y1   =   y0   +   r   *   sin(ao   *   3.14   /180   )
                 */
                /**第四步,根据每个菜单真实角度计算其坐标值**/
                point.x = (float) Math.cos(value * (Math.PI / 180)) * 100 - 100;
                point.y = (float) -Math.sin(value * (Math.PI / 180)) * 100;
                views.get(2).setTranslationX(point.x);
                views.get(2).setTranslationY(point.y);
            
        );
        valueAnimator2.setInterpolator(new LinearInterpolator());
        valueAnimator2.setDuration(1000);
        valueAnimator2.setStartDelay(250);
        valueAnimator2.start();


        ValueAnimator valueAnimator3 = ValueAnimator.ofFloat(180, -90);
        valueAnimator3.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() 
            @Override
            public void onAnimationUpdate(ValueAnimator animation) 
                float value = (Float) animation.getAnimatedValue();
                /**
                 * 
                 * 圆点坐标:(x0,y0)
                 * 半径:r
                 * 角度:a0
                 * 则圆上任一点为:(x1,y1)
                 * x1   =   x0   +   r   *   cos(ao   *   3.14   /180   )
                 * y1   =   y0   +   r   *   sin(ao   *   3.14   /180   )
                 */
                /**第四步,根据每个菜单真实角度计算其坐标值**/
                point.x = (float) Math.cos(value * (Math.PI / 180)) * 100 + 100;
                point.y = (float) -Math.sin(value * (Math.PI / 180)) * 100;
                views.get(2).setTranslationX(point.x);
                views.get(2).setTranslationY(point.y);
            
        );
        valueAnimator3.setInterpolator(new LinearInterpolator());
        valueAnimator3.setDuration(750);
        valueAnimator3.setStartDelay(1250);
        valueAnimator3.start();
        valueAnimator3.addListener(new Animator.AnimatorListener() 
            @Override
            public void onAnimationStart(Animator animation) 

            

            @Override
            public void onAnimationEnd(Animator animation) 
                startAnimator3();
            

            @Override
            public void onAnimationCancel(Animator animation) 

            

            @Override
            public void onAnimationRepeat(Animator animation) 

            
        );
    

GitHub

以上是关于自定义View+属性动画实战 — 灵动的锦鲤的主要内容,如果未能解决你的问题,请参考以下文章

Android自定义View实战之仿百度加载动画,一种优雅的Loading方式

Android自定义View实战之仿百度加载动画,一种优雅的Loading方式

Android自定义View和属性动画完美结合,创造出酷炫圆环动画,带标尺和进度

自定义UI 属性动画

自定义UI 属性动画

自定义UI 属性动画