转载请标明出处:
http://blog.youkuaiyun.com/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>
最后来看下动画效果
效果就是点击一次按钮就会生成一个心,映客的就是点击屏幕生成心,一样的。使用贝塞尔曲线可以实现我们很多动画效果。这里仅仅只是一种,以后像这种的动画,我们也可以同样的这样来实现。