高仿映客直播點亮功能,VectorDrawable+PropertyAnimation實戰篇

轉載請標明出處:
http://blog.csdn.net/iamzgx/article/details/51811430
本文出自:【iGoach的博客】

概括

最近項目中在做直播功能,其中有一個功能就是點亮功能,隨心而動,類似映客直播的點亮功能,先來看看映客的右下角點亮直播功能效果。

這裏寫圖片描述

看起來效果蠻炫的,怎麼實現呢?肯定是動畫實現啦,android動畫裏面有ViewAnimation、DrawableAnimation,PropertyAnimation。ViewAnimation的縮放、平移、旋轉、透明度能實現嗎?DrawableAnimation的逐幀播放能實現嗎?就運動軌跡實現起來就麻煩了,而且還不能真正改變view的位置,萬一運動的view有點擊事件呢,那不就是坑了。所以還是用android裏面更加強大的PropertyAnimation屬性動畫來實現。PropertyAnimation分兩種ObjectAnimation和ValueAnimation,那麼ObjectAnimtion能實現嗎?兩個都類似,只是根據當前動畫的計算值,來改變動畫的屬性值,貌似ValueAnimation靈活性更高。既然能實現,那現在就來動手吧!

準備資源

仔細看下心形,實心部分顏色很多種,問ui拿九妹圖,萬一有很多顏色呢,那不是很多圖。你又會說這個可以忽略,萬一心形顏色要服務端給我們呢,那就叫服務端給圖,確定他們給的圖能適配所有手機。那怎麼辦?要知道android現在是支持矢量圖的,記得前面寫過一篇android矢量圖之VectorDrawable ,自由又方便的填充色彩。寫的不怎好,很多細節還是沒有寫到,這裏就拿VectorDrawable來實現心形,實戰下。

首先我們配置下使用的環境,添加下面配置

android {
  defaultConfig {
  vectorDrawables.useSupportLibrary = true
}
}
dependencies {
    compile 'com.android.support:appcompat-v7:24.0.0'
}

其中添加

vectorDrawables.useSupportLibrary = true

主要是要生成

這裏寫圖片描述

這個jar兼容包

另外使用appcompat-v7:24以上才真正的實現了android5.0以下VectorDrawable的兼容,前面的版本各種坑,可以參考這篇博客

然後在項目drawable目錄下創建love_drawable.xml,直接拿前面那篇的代碼,再改下大小,代碼如下:

<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:height="32dp"
    android:width="32dp"
    android:viewportWidth="32"
    android:viewportHeight="32">
    <path android:fillColor="#bbb455"
        android:pathData="M20.5,9.5
                        c-1.955,0,-3.83,1.268,-4.5,3
                        c-0.67,-1.732,-2.547,-3,-4.5,-3
                        C8.957,9.5,7,11.432,7,14
                        c0,3.53,3.793,6.257,9,11.5
                        c5.207,-5.242,9,-7.97,9,-11.5
                        C25,11.432,23.043,9.5,20.5,9.5z" />
</vector>

效果如下

這裏寫圖片描述

顏色可以用上面path的android:fillColor自己改,怎麼用呢,來測試下。寫個佈局文件activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        app:srcCompat="@drawable/love_drawable"/>
</RelativeLayout>

注意上面app:srcCompat代替以前android:src設置背景,如果用android:src設置VectorDrawable會報一大堆錯誤,類似下面的錯誤

FATAL EXCEPTION: main
                                                              java.lang.IllegalStateException: Could not execute method for android:onClick
                                                              android.support.v7.app.AppCompatViewInflater$DeclaredOnClickListener.onClick(AppCompatViewInflater.java:293)
//...                                                          com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:936)
//...
android.support.v7.app.AppCompatViewInflater$DeclaredOnClickListener.onClick(AppCompatViewInflater.java:288)

佈局完後,activity裏面不需要做任何操作,拿個棒棒糖(android5.0)以上的手機運行下,嗯,沒問題,這樣就能顯示出來了。再拿個android4.2或者android4.4手機測試下,運行後,一樣沒問題。

如果上面佈局裏面的ImageView改爲TextView,然後設置TextView的drawableTop。那麼我們就要注意下面兩點。

  • 佈局代碼設置drawableTop,和ImageView的android:src一樣,不能直接引用VectorDrawable。我們要在VectorDrawable依附
StateListDrawable,InsetDrawable,LayerDrawable,LevelListDrawable,RotateDrawable

裏面的其中一種。比如:

love_selector.xml

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@drawable/love_drawable"/>
</selector>

然後用drawableTop引用love_selector。

  • 僅僅依賴於上一點還不行,google對於不是ImageView的控件沒有開啓兼容功能,所以我們要在activity裏面添加代碼開啓
static {
        AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
    }

當我們設置完

setCompatVectorFromResourcesEnabled(true)

之後,按理說ImageView的setImageResource和TextView的setCompoundDrawablesWithIntrinsicBounds應該一樣要依附

StateListDrawable,InsetDrawable,LayerDrawable,LevelListDrawable,RotateDrawable

裏面的其中一種。測試發現,我們可以直接引用,比如

setImageDrawable(getResources().getDrawable(R.drawable.love_drawable));

//測試動態創建TextView直接設置VectorDrawable
TextView textView = new TextView(this);
textView.setText("我是代碼創建的TextView");
        textView.setCompoundDrawablesWithIntrinsicBounds(getResources().getDrawable(R.drawable.love_drawable),null,null,null);
mRootView.addView(textView);

都沒有問題的。

資源準備的差不多了,還有一個問題,就是上面的心形是不同顏色的,不可能讓我們有多少種顏色就創建多少xml吧,而且萬一顏色值來源於服務端那就完蛋了,所以我們還是要獲取love_drawable,然後動態設置它的顏色。

動態設置VectorDrawable的fillColor和strokeColor

查看源碼我們會發現VectorDrawableCompat這個類,它有一個

public void setTint(int tint)

方法讓我們去設置fillColor,或者我們也可以通過

DrawableCompat.setTint(a,colors[round]);

來設置fillColor,隨便一種方式都行。

這樣我們就可以定義幾種顏色值,然後隨機選取一種

 private int[] colors = new int[]{Color.YELLOW,Color.BLACK,Color.BLUE,Color.RED,Color.GREEN};

然後我們把顏色值設置進去,得到一個繼承Drawable的VectorDrawableCompat 對象

 VectorDrawableCompat a = VectorDrawableCompat.create(getResources(), R.drawable.love_drawable,
                getResources().newTheme());
        Random random = new Random();
        int round =  random.nextInt(5);
        a.setTint(colors[round]);

如果你不需要映客直播心形的邊界包裹顏色,那隻要

setImageDrawable(a);

就可以了。

如果需要邊界值,那VectorDrawableCompat 有沒有設置strokeColor的邊界顏色的呢,查找源碼,發現設置strokeColor在一個VFullPath靜態內部類。而且是unused的。

@SuppressWarnings("unused")
 void setStrokeColor(int strokeColor) {
     mStrokeColor = strokeColor;
 }

所以我們根本沒法使用,那怎麼辦?放棄嗎?不,這裏說一個實現方法,上面我們不是說過VectorDrawable要用一層Drawable包裹嗎,那我們也可以通過LayerDrawable來實現呀,裏面一層實心的心形,外面一層空心的心形。這樣我們就可以實現了。所以,我們再定義一個空心的drawable。代碼和love_drawable差不多,

border_drawable.xml

<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:height="32dp"
    android:width="32dp"
    android:viewportWidth="32"
    android:viewportHeight="32">
    <path
        android:strokeWidth="2"
        android:strokeColor="#aaaaaa"
        android:pathData="M20.5,9.5
                        c-1.955,0,-3.83,1.268,-4.5,3
                        c-0.67,-1.732,-2.547,-3,-4.5,-3
                        C8.957,9.5,7,11.432,7,14
                        c0,3.53,3.793,6.257,9,11.5
                        c5.207,-5.242,9,-7.97,9,-11.5
                        C25,11.432,23.043,9.5,20.5,9.5z" />
</vector>

效果如下

這裏寫圖片描述

不設置fillColor,設置strokeColor,這樣內部就是透明的了。然後我們再兩層包裹起來

//layerDrawable的第一層
   VectorDrawableCompat a = VectorDrawableCompat.create(getResources(), R.drawable.love_drawable,getResources().newTheme());
   Random random = new Random();
   int round =  random.nextInt(5);
   a.setTint(colors[round]);
   //第二種實現設置fillcolor的方式
   //DrawableCompat.setTint(a,colors[round]);
   //layerDrawable的第二層
   VectorDrawableCompat boardVdc = VectorDrawableCompat.create(getResources(), R.drawable.border_drawable,getResources().newTheme());
   Drawable[] drawable = new Drawable[2];
   drawable[0] = a;
   drawable[1] = boardVdc;
   LayerDrawable layerDrawable = new LayerDrawable(drawable);
   setImageDrawable(layerDrawable);

這樣資源的使用我們可以了。

PropertyAnimation的使用

資源使用好了。那麼我們就來通過PropertyAnimation的ValueAnimation來實現動畫,在這個動畫裏面我們要知道每時每刻的運動位置,然後來決定它的運動軌跡,於是我們就要使用ValueAnimation的TypeEvaluator。那怎麼去獲取它每時每刻的運動位置呢。仔細看,我們會發現,這個動畫使用的運動軌跡是使用二次貝塞爾曲線實現的。
什麼叫二次貝塞爾曲線?
先來看一張圖
這裏寫圖片描述

定義AD/AB = BE/BC = DF/DE ,這樣F就是在貝塞爾曲線上面的其中的一個點,這樣主成的所有點就是一條貝塞爾曲線。詳情原理請看博客 Iwfu-貝塞爾曲線。自定義軌跡可以查看工具。起點A和終點C和B點我們可以自己控制,那麼求F點我們只是要用到一個公式

(1 - t)^2 P0 + 2 t (1 - t) P1 + t^2 P2;

知道這個公式了,我們就可以自定義TypeEvaluator,點的座標用Point來記錄,代碼如下:

public class LoveEvaluator implements TypeEvaluator<Point> {
    private Point dirPoint;
    public LoveEvaluator(Point dirPoint){
        this.dirPoint = dirPoint ;
    }
    @Override
    public Point evaluate(float t, Point startValue, Point endValue) {
        //(1 - t)^2 P0 + 2 t (1 - t) P1 + t^2 P2;
        int x = (int)(Math.pow((1-t),2)*startValue.x+2*t*(1-t)*dirPoint.x+Math.pow(t,2)*endValue.x);
        int y = (int)(Math.pow((1-t),2)*startValue.y+2*t*(1-t)*dirPoint.y+Math.pow(t,2)*endValue.y);
        return new Point(x,y);
    }
}

接下來我們就可以使用ValueAnimation.ofObject來實現動畫效果了,下面我們結合上面的VectorDrawable來自定義一個ImageView,代碼如下:

//實現了ValueAnimator.AnimatorUpdateListener接口,主要是用來設置View的移動位置以及透明度
public class LoveAnimView extends ImageView implements ValueAnimator.AnimatorUpdateListener{
    //起點座標
    private Point mStartPoint;
    //終點座標
    private Point mEndPoint;
    //定義的一組顏色
    private int[] colors = new int[]{Color.YELLOW,Color.BLACK,Color.BLUE,Color.RED,Color.GREEN};
    public LoveAnimView(Context context) {
        this(context,null);
    }
    public LoveAnimView(Context context, AttributeSet attrs) {
        this(context, attrs,0);
    }
    public LoveAnimView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        //引用資源,獲取心形資源
        VectorDrawableCompat a = VectorDrawableCompat.create(getResources(), R.drawable.love_drawable,
                getResources().newTheme());
        Random random = new Random();
        int round =  random.nextInt(5);
        a.setTint(colors[round]);
        //DrawableCompat.setTint(a,colors[round]);
        VectorDrawableCompat boardVdc = VectorDrawableCompat.create(getResources(), R.drawable.border_drawable,
                getResources().newTheme());
        Drawable[] drawable = new Drawable[2];
        drawable[0] = a;
        drawable[1] = boardVdc;
        LayerDrawable layerDrawable = new LayerDrawable(drawable);
        //把LayerDrawable做爲當前view的背景
        setImageDrawable(layerDrawable);
    }
    //設置起點的位置
    public void setStartPosition(Point startPosition) {
        this.mStartPoint = startPosition;
    }
    //設置終點的位置
    public void setEndPosition(Point endPosition) {
        this.mEndPoint = endPosition;
    }
    //設置動畫
    public void startLoveAnimation(){
        if(mStartPoint==null||mEndPoint==null)
            throw new IllegalArgumentException("mStartPoint is not null or mEndPoint is not null");
    //中間的指向點的座標,這裏我們x軸座標爲在0-330之內生成隨機數,可自己結合上面工具調節生成想要的效果
        int dirPointX = (int)(Math.random()*330);
   //中間的指向點的座標,這裏我們y軸座標取起點和終點的中間值,可自己調節生成想要的效果
        int dirPointY = (mStartPoint.y+mEndPoint.y)/2;
        Point dirPoint = new Point(dirPointX,dirPointY);
        //創建自定義的TypeEvaluator
        LoveEvaluator loveEvaluator = new LoveEvaluator(dirPoint);
        //然後獲取ValueAnimator對象
        ValueAnimator animator = ValueAnimator.ofObject(loveEvaluator,mStartPoint,mEndPoint);
        //實現onAnimationUpdate方法監聽每時每刻的座標位置,然後設置view的座標
        animator.addUpdateListener(this);
        //設置動畫時間爲2s
        animator.setDuration(2000);
        //動畫結束要移除view,同時設置alpha爲透明
        animator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
                ViewGroup viewGroup = (ViewGroup) getParent();
                setAlpha(0f);
                viewGroup.removeView(LoveAnimView.this);
            }
        });
        //常素執行動畫
        animator.setInterpolator(new LinearInterpolator());
        //啓動動畫
        animator.start();
    }
    @Override
    public void onAnimationUpdate(ValueAnimator valueAnimator) {
        Point point = (Point) valueAnimator.getAnimatedValue();
        //設置當前view的座標
        setX(point.x);
        setY(point.y);
        float value = point.y*1.0f/mStartPoint.y;
        //設置透明度慢慢變透明
        setAlpha(value);
        invalidate();
    }
}

再來看下MainActivity

public class MainActivity extends AppCompatActivity {
    static {
        AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
    }
    private RelativeLayout mRootView;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mRootView = (RelativeLayout) findViewById(R.id.id_root_view);

        //測試動態創建TextView直接設置VectorDrawable
        TextView textView = new TextView(this);
        textView.setText("我是代碼創建的TextView");
        textView.setCompoundDrawablesWithIntrinsicBounds(getResources().getDrawable(R.drawable.love_drawable),null,null,null);
        mRootView.addView(textView);
    }
    public void startAnim(View view){
    //點擊按鈕生成一個心形狀態並執行動畫
        RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
                ViewGroup.LayoutParams.WRAP_CONTENT);
            LoveAnimView loveAnimView = new LoveAnimView(this);
            loveAnimView.setLayoutParams(params);
            //座標位置可自己手動設置結合上面工具調節,這裏以映客效果爲參考點
            loveAnimView.setStartPosition(new Point(530,712));
            loveAnimView.setEndPosition(new Point(530-(int)(Math.random()*200),712-((int)(Math.random()*500)+200)));
            //開始動畫
            loveAnimView.startLoveAnimation();
            //把view加入到根佈局裏面,生成動畫
            mRootView.addView(loveAnimView);
    }
}

最後的佈局文件

<?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"
    android:id="@+id/id_root_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@android:color/white">
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:gravity="center"
        android:layout_centerInParent="true">
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="我是佈局裏面的TextView"
            android:drawableLeft="@drawable/love_selector"/>
        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:srcCompat="@drawable/love_drawable"/>
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="10dp"
            android:text="隨心而動"
            android:onClick="startAnim"/>
    </LinearLayout>
</RelativeLayout>

最後來看下動畫效果

這裏寫圖片描述

效果就是點擊一次按鈕就會生成一個心,映客的就是點擊屏幕生成心,一樣的。使用貝塞爾曲線可以實現我們很多動畫效果。這裏僅僅只是一種,以後像這種的動畫,我們也可以同樣的這樣來實現。

下載代碼

發佈了56 篇原創文章 · 獲贊 50 · 訪問量 18萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章