轉載請標明出處:
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>
最後來看下動畫效果
效果就是點擊一次按鈕就會生成一個心,映客的就是點擊屏幕生成心,一樣的。使用貝塞爾曲線可以實現我們很多動畫效果。這裏僅僅只是一種,以後像這種的動畫,我們也可以同樣的這樣來實現。