Android -- 自定义ViewGroup+贝塞尔+属性动画实现仿QQ点赞效果

1,昨天我们写了篇简单的贝塞尔曲线的应用,今天和大家一起写一个QQ名片上常用的给别人点赞的效果,实现效果图如下:

  红心的图片比较丑,见谅见谅(哈哈哈哈哈哈)。。。。

2,实现的思路和原理

  从上面的效果图我们可以看到,实现基本上可以分为两部分:

  ①点击红心的时候底部出现ImageView的颜色是随机的

  ②等生成ImageView之后,执行动画往上升,轨迹是一条曲线,且每一个Imageview的轨迹都是不相同的(这里主要用到随机贝塞尔曲线的知识)

  ok,既然知道怎么做了,开撸开撸.......

  • 创建类HeartStar类,继承自ViewGroup,绘制我们底部的ImageView

  首先我们这里为什么选择ViewGroup而不选择View呢?因为考虑到我们的每次点击的都是生成新的ImageView,所以这里选择ViewGroup。

  重写构造方法,在构造方法中初始化ImageView

public class LikeStar extends ViewGroup {    
    private List<Drawable> mStarDrawable;
    private int mWidth; //整个控件的宽度
    private int mHeight; //整个控件的高度
 private Random random = new Random();

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

    public LikeStar(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public LikeStar(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }

     private void init(final Context context) {
        mStarDrawable = new ArrayList<>();
    //初始化图片资源
    mStarDrawable.add(getResources().getDrawable(R.mipmap.heart_red));
    ImageView image_heard = new ImageView(context);
    image_heard.setImageDrawable(mStarDrawable.get(0));
   image_heard.setLayoutParams(newLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
    addView(image_heard);
    }
    
}

  重写OnSizeChange()方法,获取整个控件的宽高

     @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mWidth = getMeasuredWidth();
        mHeight = getMeasuredHeight();
    }

  重写onMeasure方法,测量子控件高度并保存

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        measureChildren(widthMeasureSpec, heightMeasureSpec);

        //获取view的宽高测量模式
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);

        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        //保存测量高度
        setMeasuredDimension(widthSize, heightSize);
    }

  重写onLayout方法,摆放圆心控件

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        Log.i("wangjitao", "l:" + l + ",t:" + t + ",r:" + r + ",b:" + b);
            View child = getChildAt(0);
            int childW = child.getMeasuredWidth();
            int childH = child.getMeasuredHeight();
            child.layout((mWidth - childW) / 2, (mHeight - childH), (mWidth - childW) / 2 + childW, mHeight);
        
    }

  布局文件中引用

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.qianmo.beziertest.MainActivity">

    <com.qianmo.beziertest.view.LikeStar
        android:id="@+id/likestar"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        />

    <Button
        android:id="@+id/btn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="翻滚吧,贝塞尔"
        android:visibility="gone"
        />
</RelativeLayout>

  ok,看一下运行的效果

  没问题,我们绘制的红心就在我们父控件的底部,这里可能有同学会有疑问,为什么我布局文件中设置的宽高是wrap_content,而我们自定义的ViewGroup却充满了屏幕,这个我以前写过原因,不理解的同学可以在这一篇去理解一下。

  • 属性动画和插补器了解

  由于这里要用到属性动画和插补器,后面打算自己写一下这个专题,这里就简单的举一个属性动画加自定义的TypeEvaluator一个简单的效果。

  首先创建两个点,即我们的我们动画的其实点和结束点,然后自定义TypeEvaluator,重写evaluate方法,实时的返回我们ImageView移动的点,代码如下

............省略代码
    //申明属性
    private PointF mStartPoint, mEndPoint,  
     @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);

        mWidth = getMeasuredWidth();
        mHeight = getMeasuredHeight();


        // 初始化各个点

        //借用子view控件中的宽高
        View child = getChildAt(0);
        int childW = child.getMeasuredWidth();
        int childH = child.getMeasuredHeight();

        mStartPoint.x = (mWidth - childW) / 2;
        mStartPoint.y = mHeight - childH;
        mEndPoint.x = (mWidth - childW) / 2;
        mEndPoint.y = 0 - childH;
    }
    
   //自定义TypeEvaluator
public class BezierTypeEvaluator implements TypeEvaluator<PointF> {

        @Override
        public PointF evaluate(float fraction, PointF startValue, PointF endValue) {
            PointF pointCur = new PointF();
            pointCur.x = mStartPoint.x + fraction * (endValue.x - mStartPoint.x);
            pointCur.y = mStartPoint.y + fraction * (endValue.y - mStartPoint.y);    
            return pointCur;
        }
    }

   //向外部提供方法,用于点击事件触发动画发生
    /**
     * 开始动画
     */
    public void startRunning() {
        BezierTypeEvaluator bezierTypeEvaluator = new BezierTypeEvaluator();
        ValueAnimator valueAnimator = ValueAnimator.ofObject(bezierTypeEvaluator, mStartPoint, mEndPoint);
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                PointF pointF = (PointF) animation.getAnimatedValue();
                getChildAt(0).setX(pointF.x);
                getChildAt(0).setY(pointF.y);
            }
        });

        valueAnimator.setDuration(3000);
        valueAnimator.start();
    }

  在Activity监听点击事件,并开启动画

package com.qianmo.beziertest;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;

import com.qianmo.beziertest.view.LikeStar;
import com.qianmo.beziertest.view.MyView1;
import com.qianmo.beziertest.view.MyViewCircle;

public class MainActivity extends AppCompatActivity {
    private MyView1 myview;
    private Button btn;
    private MyViewCircle myViewCircle;
    private LikeStar mLikeStar;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //点赞效果
        mLikeStar = (LikeStar) findViewById(R.id.likestar);
        btn = (Button) findViewById(R.id.btn);
        btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mLikeStar.startRunning();
            }
        });
    }
}

  效果图如下:

  • 实现贝塞尔曲线上升

  我们上面实现的只是一个简单的直线上升,但是我们最终的效果图是每个圆心都是做无规则的曲线上升,这里我们就要使用贝塞尔三阶曲线公式了,这里不了解的童鞋可以去这一篇弄懂,不然后面的计算你是看不懂怎么来的。

  使用我们的三阶曲线我们知道需要两个数据点和两个控制点,数据点我们继续使用上面的mStartPoint、mEndPoint,然后在创建两个随机的控制点(但大方向不随机),最后在我们的evaluate()方法中套用贝塞尔三阶公式,公式、代码如下:

  

     //初始化属性 
    private PointF  mControllPointOne, mControllPointTwo;
    private Random random = new Random();

    //设置值
      mControllPointOne.x = random.nextInt(mWidth / 2);
        mControllPointOne.y = random.nextInt(mHeight / 2) + mHeight / 2;

        mControllPointTwo.x = random.nextInt(mWidth / 2) + mWidth / 2;
        mControllPointTwo.y = random.nextInt(mHeight / 2);

  OK,让我们重写写一下自定义TypeEvaluator的evaluate方法,代码如下:

public class BezierTypeEvaluator implements TypeEvaluator<PointF> {
        private PointF mControllPoint1, mControllPoint2;

        public BezierTypeEvaluator(PointF mControllPointOne, PointF mControllPointTwo) {
            mControllPoint1 = mControllPointOne;
            mControllPoint2 = mControllPointTwo;
        }

        @Override
        public PointF evaluate(float fraction, PointF startValue, PointF endValue) {
            PointF pointCur = new PointF();
            pointCur.x = mStartPoint.x * (1 - fraction) * (1 - fraction) * (1 - fraction) + 3
                    * mControllPoint1.x * fraction * (1 - fraction) * (1 - fraction) + 3
                    * mControllPoint2.x * (1 - fraction) * fraction * fraction + endValue.x * fraction * fraction * fraction;// 实时计算最新的点X坐标
            pointCur.y = mStartPoint.y * (1 - fraction) * (1 - fraction) * (1 - fraction) + 3
                    * mControllPoint1.y * fraction * (1 - fraction) * (1 - fraction) + 3
                    * mControllPoint2.y * (1 - fraction) * fraction * fraction + endValue.y * fraction * fraction * fraction;// 实时计算最新的点Y坐标
            return pointCur;
        }
    }



    //动画中调用
    /**
     * 开始动画
     */
    public void startRunning() {
        BezierTypeEvaluator bezierTypeEvaluator = new BezierTypeEvaluator(mControllPointOne, mControllPointTwo);
        ValueAnimator valueAnimator = ValueAnimator.ofObject(bezierTypeEvaluator, mStartPoint, mEndPoint);
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                PointF pointF = (PointF) animation.getAnimatedValue();
                getChildAt(0).setX(pointF.x);
                getChildAt(0).setY(pointF.y);
            }
        });

        valueAnimator.setDuration(3000);
        valueAnimator.start();
    }

  再看看运行效果

  • 添加多个红心飞升

  我们上面实现了一个红心的上飞,这里我们想点击红心,从而有多个红心飞上去,并且每次飞上来的圆心的颜色不一样,我这里实现的是每点击一次就往viewGroup中已添加一个view,并且开始动画,,每次产生的都是随机的颜色图片,主要代码如下:

 private void init(final Context context) {
        mStarDrawable = new ArrayList<>();
        mInterpolators = new ArrayList<>();
        mStartPoint = new PointF();
        mEndPoint = new PointF();
        mControllPointOne = new PointF();
        mControllPointTwo = new PointF();

        //初始化图片资源
        mStarDrawable.add(getResources().getDrawable(R.mipmap.heart_red));
        mStarDrawable.add(getResources().getDrawable(R.mipmap.heart_blue));
        mStarDrawable.add(getResources().getDrawable(R.mipmap.heart_yellow));
        mStarDrawable.add(getResources().getDrawable(R.mipmap.heart_green));

        //初始化插补器
        mInterpolators.add(new LinearInterpolator());
        mInterpolators.add(new AccelerateDecelerateInterpolator());
        mInterpolators.add(new AccelerateInterpolator());
        mInterpolators.add(new DecelerateInterpolator());

        ImageView image_heard = new ImageView(context);
        image_heard.setImageDrawable(mStarDrawable.get(0));

        image_heard.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT,
                LayoutParams.WRAP_CONTENT));
        image_heard.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                //点击之后开始动画,添加红心到布局文件并开始动画
                final ImageView image_random = new ImageView(context);
                image_random.setImageDrawable(mStarDrawable.get(random.nextInt(4)));

                image_random.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT,
                        LayoutParams.WRAP_CONTENT));
                addView(image_random);

                invalidate();

                //开始做动画效果
                
                BezierTypeEvaluator bezierTypeEvaluator = new BezierTypeEvaluator(mControllPointOne, mControllPointTwo);
            
                ValueAnimator valueAnimator = ValueAnimator.ofObject(bezierTypeEvaluator, mStartPoint, endPointRandom);
                valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                    @Override
                    public void onAnimationUpdate(ValueAnimator animation) {
                        PointF pointF = (PointF) animation.getAnimatedValue();
                        image_random.setX(pointF.x);
                        image_random.setY(pointF.y);
                    }
                });

                valueAnimator.setDuration(2000);
                valueAnimator.start();

            }
        });
        addView(image_heard);
    }

  OK,这样就实现了多个不一样的红心上升了,效果如下:

  • 实现每个红心上升的曲线路径不同

  从上面的效果我们可以看到我们的红心都是按照一种曲线路径上升的,现在想变成随机上升的。所以这里要改变数据点中结束点的X坐标,所以代码修改为如下

 image_heard.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                //点击之后开始动画,添加红心到布局文件并开始动画
                final ImageView image_random = new ImageView(context);
                image_random.setImageDrawable(mStarDrawable.get(random.nextInt(4)));

                image_random.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT,
                        LayoutParams.WRAP_CONTENT));
                addView(image_random);

                invalidate();

                //开始做动画效果
                //这里修改了代码
                PointF endPointRandom = new PointF(random.nextInt(mWidth), mEndPoint.y);
//                BezierTypeEvaluator bezierTypeEvaluator = new BezierTypeEvaluator(mControllPointOne, mControllPointTwo);
                BezierTypeEvaluator bezierTypeEvaluator = new BezierTypeEvaluator(mControllPointOne,mControllPointTwo);
                ValueAnimator valueAnimator = ValueAnimator.ofObject(bezierTypeEvaluator, mStartPoint, endPointRandom);
                valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                    @Override
                    public void onAnimationUpdate(ValueAnimator animation) {
                        PointF pointF = (PointF) animation.getAnimatedValue();
                        image_random.setX(pointF.x);
                        image_random.setY(pointF.y);
                    }
                });

                valueAnimator.setDuration(2000);
                valueAnimator.start();

            }
        });    

  效果图如下:

  • 修改控制点,实现每次开始的动画的方向不一样

  从上面的效果可以看到我们的动画总是从左边开始往上升,这是应为我们之前说过的我们的两个控制点虽然是随机的,但是大体坐标是左右一个,现在我们想实现红心随机从左右两边开始动画上升,所以我们这里要把两个控制点的坐标完全随机,,只需要修改如下代码:

 BezierTypeEvaluator bezierTypeEvaluator = new BezierTypeEvaluator(new PointF( random.nextInt(mWidth ),random.nextInt(mHeight)), new PointF( random.nextInt(mWidth),random.nextInt(mHeight)));
               

  效果如下:

  ok,这样我们就完全实现QQ名片点赞效果了,这是项目Github的源码地址,需要的同学可以去下载一下,See You Next Time !!!

原文地址:https://www.cnblogs.com/wjtaigwh/p/6657005.html