Android自定义动画专题二

android自定义动画专题二

在上篇文章中给大家介绍了android自定义动画的第一种表现形式:view的绘制;不过这只是一种单纯利用自定义控件绘制的方式去实现;这篇文章会给大家演示如何通过自定义控件(测量,排版,绘制)+android原生动画的方式一起实现一些比较酷炫复杂的效果。

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!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,980评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,178评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,868评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,498评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,492评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,521评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,910评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,569评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,793评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,559评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,639评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,342评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,931评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,904评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,144评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,833评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,350评论 2 342

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,380评论 25 707
  • 每二百六十一步会有一盏路灯 走到第五十七盏时 夜空飘起了雪 第一百三十四盏 与一名衣领遮脸年轻女子擦肩而过 第一百...
    封城雪阅读 416评论 4 8
  • 《羞羞的铁拳》真的搞笑吗,那为什么我愣是没笑出来。 影片刚开始就是男女主掉进泳池亲上被雷劈了,瞬间我就知道套路是...
    小清新和女神阅读 298评论 0 2
  • 对于一个从小对任何事情都秉持着三分热度的人怎么能养成一个好的习惯呢? 还记得初中的时候,爸觉得我花钱大手大脚跟我说...
    蝶筱璇阅读 81评论 2 2