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 !!!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM