android自定义动画专题二
在上篇文章中给大家介绍了android自定义动画的第一种表现形式:view的绘制;不过这只是一种单纯利用自定义控件绘制的方式去实现;这篇文章会给大家演示如何通过自定义控件(测量,排版,绘制)+android原生动画的方式一起实现一些比较酷炫复杂的效果。
1.卫星菜单demo
该demo实现的主要核心技术点是:继承ViewGroup实现子控件的测量,排版,以及通过Animation动画控制子控件的动画效果。
首先来看下该demo的效果展示:
看到效果图后我们就要思考通过什么样的方式才能实现这样的效果?不难看出该效果不仅仅只有一个控件而是由多个控件组成并且带有动画效果。那么从产品设计上来看,最左下角的加号控制按钮位置一直没有动过,只是做了一个旋转操作;而其他五个飞出来的按钮带有功能特色可以具备点击实现的操作。并且在实际需求中很可能不止五个按钮或者少于五个按钮,又甚者按钮的样式改变了等等的需求变化,这些都是我们需要提前考虑进来的。所以,针对该效果我们必须要自己定义一个ViewGroup容器来管理这些变化的子控件,对它们进行需求的排版以及动画处理。
1)视图效果的处理-布局
首先定义一个自定义控件ArcMenu去继承ViewGroup,复写构造函数和onLayout();不过先不着急实现具体的代码,现在相当于有了一个没有任何规则的空的容器,那么我们可以先将布局造出来,代码如下:
<com.zphuan.animationproject.view.ArcMenu
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<RelativeLayout
android:id="@+id/rl"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/composer_button" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:src="@drawable/composer_icn_plus" />
</RelativeLayout>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/composer_camera" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/composer_music" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/composer_place" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/composer_sleep" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/composer_sun" />
</com.zphuan.animationproject.view.ArcMenu>
也就是将我们需要展示的六个按钮控件引进来了,其中第一个加号按钮是由两个图片重叠显示的,其余都是ImageView控件。
2)控件的测量
由于当前自定义控件继承ViewGroup,需要对子视图做测量处理。代码如下:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int count = getChildCount();
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
child.measure(0, 0);
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
这里并没有对当前控件设置测量宽高,而是走的super,根据布局文件中写的宽高来决定整个控件的大小。
3)视图的摆放处理
这里的视图摆放是一个重点,也是一个难点,由于我们的其余子控件是围绕着第一个子控件进行排列的,并且每个子控件距离第一个子控件的距离是相等的,所以这个地方最好是画图思考推算每一个子控件的左上右下,否则很容易思路就乱掉了,图示如下:
这里我们默认图形初始化摆放的时候就是展开的。
具体代码如下:
/**
* 此数据为每个子视图距离第一个控件的距离
*/
private static final int RADIUS = (int) MyApplication.getContext().getResources().getDimension(R.dimen.ArcMenu_Radius);
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
child0 = getChildAt(0);
child0.setOnClickListener(this);
//将第一个控件摆放在左下角
child0.layout(0, height - child0.getMeasuredHeight(), child0.getMeasuredWidth(), height);
int count = getChildCount();
for (int i = 0; i < count - 1; i++) {
double angle = Math.PI / 2 / (count - 2) * i;
View child = getChildAt(i + 1);
int left = (int) (child0.getLeft() + RADIUS * Math.sin(angle));
int top = (int) (child0.getTop() - RADIUS * Math.cos(angle));
int right = left + child.getMeasuredWidth();
int bottom = top + child.getMeasuredHeight();
child.layout(left, top, right, bottom);
child.setVisibility(INVISIBLE);
}
}
4)控件的平移和旋转
最后就是实现控件的平移和旋转,这里旋转就不说了很简单围绕自身控件的中心机型旋转360或720就行。主要是平移动画,这里使用相对自身和相对parent 都不好处理,最好的方式就是使用 Animation.ABSOLUTE 绝对值的方式,不过要注意的是这里的绝对值是指的动画作用于的当前控件的坐标系为基准。最后,每个控件在平移到指定的位置后都会有个超出一段距离再归位的效果,这也是用的动画自带的插值器OvershootInterpolator()实现。
具体代码如下:
@Override
public void onClick(View v) {
//1.旋转自身
rotateChild0(v);
//2.执行其他子视图的动画
animateOtherChild();
}
private void animateOtherChild() {
int count = getChildCount();
for (int i = 0; i < count - 1; i++) {
AnimationSet as = new AnimationSet(true);
final View child = getChildAt(i + 1);
child.setVisibility(View.VISIBLE);
int left = child.getLeft();
TranslateAnimation ta;
if(status==CurrentStatus.CLOSE) {
ta = new TranslateAnimation(
Animation.ABSOLUTE, -left,
Animation.ABSOLUTE, 0,
Animation.ABSOLUTE, child0.getBottom() - child.getBottom(), Animation.ABSOLUTE, 0);
}else{
ta = new TranslateAnimation(
Animation.ABSOLUTE, 0,
Animation.ABSOLUTE, -left,
Animation.ABSOLUTE, 0, Animation.ABSOLUTE, child0.getBottom() - child.getBottom());
}
ta.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
Log.i(TAG, "onAnimationEnd");
if(status==CurrentStatus.CLOSE){
child.clearAnimation();
child.setVisibility(View.INVISIBLE);
}
}
@Override
public void onAnimationRepeat(Animation animation) {
}
});
ta.setStartOffset(200*i);
ta.setDuration(2000);
ta.setInterpolator(new OvershootInterpolator());
RotateAnimation ra = new RotateAnimation(0, 720, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
ra.setDuration(2000);
as.addAnimation(ra);
as.addAnimation(ta);
as.setFillAfter(true);
child.startAnimation(as);
}
changeCurrentStatus();
}
private void rotateChild0(View v) {
RotateAnimation ra = new RotateAnimation(0, 360, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
ra.setDuration(1000);
v.startAnimation(ra);
}
private CurrentStatus status = CurrentStatus.CLOSE;
//定义卫星菜单打开和关闭的状态
public enum CurrentStatus {
OPEN, CLOSE;
}
private void changeCurrentStatus() {
status = status == CurrentStatus.CLOSE ? CurrentStatus.OPEN : CurrentStatus.CLOSE;
}
以上就是该案例的整个思路分析和具体的代码展示,如果想要参考完整的代码请到GitHub下载,欢迎star和fork
https://github.com/zphuanlove/AnimationProject
2.属性动画demo
以下的几个动画效果也都是通过自定义控件绘制加属性动画的方式去实现的。
圆形缩放效果:
在ondraw方法中绘制圆,通过ValueAnimator实现圆的放大和透明渐变
线条的缩放效果:
在ondraw()方法中绘制线条,通过ValueAnimator动画来平移和缩放画布的方式控制线条
以上的效果就不在此处贴出代码了,可以到我的GitHub去下载观看更多的效果实现,也欢迎大家start和fork。
https://github.com/zphuanlove/AnimationProject
3.奔跑吧,汽车Demo
最后这个demo也是一个比较有意思的demo,一辆小汽车在马路上奔驰,来看下效果:
1)主界面的布局
代码如下:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:background="@drawable/backgroud"
android:layout_width="match_parent" android:layout_height="match_parent">
<FrameLayout
android:layout_width="277.33dp"
android:layout_height="334.66dp"
android:layout_gravity="center"
android:background="@drawable/water">
<FrameLayout
android:layout_marginTop="30dp"
android:layout_width="210dp"
android:layout_height="210dp"
android:background="@drawable/rain_bow"
android:layout_gravity="center_horizontal">
<com.zphuan.animationproject.view.Road
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<include layout="@layout/car"/>
</FrameLayout>
</FrameLayout>
</FrameLayout>
小汽车的组成也是由一个车壳加两个轮子和一起尾气组成,这里的两个轮子是由两张图片实现的一个帧动画。布局代码如下:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal|bottom"
android:layout_marginBottom="26dp">
<ImageView
android:id="@+id/car_body"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toRightOf="@+id/car_gas"
android:paddingBottom="7dp"
android:src="@drawable/car_body"/>
<ImageView
android:id="@+id/car_gas"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@id/car_body"
android:src="@drawable/car_gas"
android:layout_marginBottom="8dp"/>
<ImageView
android:id="@+id/car_front_tire"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/car_front_tire"
android:layout_alignBottom="@id/car_body"
android:layout_alignRight="@id/car_body"
android:layout_marginRight="7dp"
/>
<ImageView
android:id="@+id/car_back_tire"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/car_back_tire"
android:layout_alignBottom="@id/car_body"
android:layout_toLeftOf="@id/car_front_tire"
android:layout_marginRight="16dp"/>
</RelativeLayout>
2)路的处理
绘制路是这一块的难点,首先路的图片本身是方形的而且是一个长图,但是实际看到的效果是路在不停的移动而且感觉永远移不完,并且是裁剪过的圆形效果,所以这里路的处理最好单独抽出来做成一个自定义控件。
路不断移动的核心思路就是通过canvas的drawBitmap(@NonNull Bitmap bitmap, @Nullable Rect src, @NonNull Rect dst,@Nullable Paint paint)来实现,bitmap指的是路这张图,src指定的是从原图路上抠出来的矩形部分,dst指定的是实际绘制出来的矩形大小;所以想要让路不停的移动就是不断的控制从原图上抠出来的矩形区域,用一个偏移量offset来控制src的左和右,如果超出了整个路的位置,则绘制路尽头剩余的视图+路开始的部分视图。
具体代码如下:
/**
* Created by PeiHuan on 2017/6/28.
*/
public class Road extends View {
private static final String TAG = "huan";
private Paint paint;
private Bitmap bitmap;
private int roadWidth;
public Road(Context context) {
this(context, null);
}
public Road(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public Road(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private Rect src;
private Rect dst;
private Path path;
private Rect lastSrc;
private Rect lastDst;
private void init() {
paint = new Paint();
src = new Rect();
dst = new Rect();
lastSrc = new Rect();
lastDst = new Rect();
path = new Path();
bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.road);
roadWidth = bitmap.getWidth();
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
//Path.Direction.CW:顺时针方向
path.addCircle(w / 2, h / 2, w / 2, Path.Direction.CW);
}
private int offset;
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//画布的剪切:可以按照path去剪切出对应的图形
canvas.clipPath(path);
if (offset + getWidth() <= roadWidth) {
src.set(offset, 0, getWidth() + offset, getHeight());
dst.set(0, 0, getWidth(), getHeight());
canvas.drawBitmap(bitmap, src, dst, paint);
} else {
src.set(offset, 0, roadWidth, getHeight());
dst.set(0, 0, src.width(), getHeight());
canvas.drawBitmap(bitmap, src, dst, paint);
lastSrc.set(0, 0, getWidth() - src.width(), getHeight());
lastDst.set(dst.width(), 0, getWidth(), getHeight());
canvas.drawBitmap(bitmap, lastSrc, lastDst, paint);
}
offset += 3;
offset %= roadWidth;
Log.i(TAG, "offset: " + offset);
invalidate();
}
}
3)轮子和尾气的动画
轮子之前说过是用的帧动画,所以只要在activity中找到对应的控件开启动画即可,尾气一闪一现的效果可以通过handler实现一个死循环不停的隐藏和显示尾气控件即可。
/**
* Created by PeiHuan on 2017/6/28.
*/
public class CarActivity extends Activity {
private ImageView gas;
private Handler handler = new Handler();
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_car);
startTireAnimation();
gas = (ImageView) findViewById(R.id.car_gas);
handler.postDelayed(new MyRunnable(),300);
}
private void startTireAnimation() {
ImageView front= (ImageView) findViewById(R.id.car_front_tire);
AnimationDrawable frontAnimationDrawable= (AnimationDrawable) front.getDrawable();
frontAnimationDrawable.start();
ImageView back= (ImageView) findViewById(R.id.car_back_tire);
AnimationDrawable backAnimationDrawable= (AnimationDrawable) back.getDrawable();
backAnimationDrawable.start();
}
private boolean isGasVisible = false;
private class MyRunnable implements Runnable {
@Override
public void run() {
gas.setVisibility(isGasVisible?View.INVISIBLE:View.VISIBLE);
isGasVisible = !isGasVisible;
handler.postDelayed(this,300);
}
}
}
具体的详情代码请参考GitHub:
https://github.com/zphuanlove/AnimationProject
总结
那么通过以上几个案例效果给大家演示了自定义动画的第二种表现形式:即通过 自定义控件(View,ViewGroup)+原生动画(Animation动画,属性动画,帧动画) 的方式实现。
Thanks!