Android 补间动画Animation的实用应用

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/sinat_30276961/article/details/49868409

转载请注明:
http://blog.csdn.net/sinat_30276961/article/details/49868409

上一篇,我总结了补间动画的各种特性,并写了个小的应用来使用这些特性。本篇,将在上篇的基础上,更进一步的使用Android的Animation,让你的应用不断炫起来~~

ok,闲话少说,开始正题~

先看一下效果:

第一个实例:
第一个实例

第二个实例:
第二个实例

先大致讲一下这两个实例用到了Animation的哪些特性。

第一个实例,我想大部分朋友都看到过了,最初是一个老外写的卫星菜单,然后有很多人尝试去实现,我也是其中一个。
这个实例中需要用到的特性是:
Animation中的透明度的渐变,平移和旋转。外加一个动画保持fillAfter。

第二个实例用到了Animation的渐变的最核心的特性,那就是值从0到1变化,并可以通过插值器进行过程控制。

接下来,我一个个来讲解下。

卫星菜单实现

要实现卫星菜单,有两种方案,一个是通过Android平台里的动画效果来实现,另一个是单纯的用画来实现。

这两个方案中,第二个方案是不可取的,虽然实现也不是很复杂,但是相比较第一种,还是挺复杂的。

第一个方案又能有两种实现方式:用补间动画和属性动画。
因为本篇是讲补间动画的应用,所以这里就用补间动画来实现。

这里,需要考虑的是补间动画虽然可以实现动画效果,但是view本身没有做相应的属性改变,所以,如果仅仅创建一组可动画的菜单view,然后动画到了最外围,去点击时是无效的。那怎么解决呢?

我们可以创建两组菜单view,一组负责动画,一组负责响应点击。这样一来,这两组view的位置就可以确定了:负责动画的view都放在最初始的地方;负责响应的view放在动画结束的地方。然后只要控制下view的可见不可见就可以了。

接着是动画效果的选择。
每个动画view在展开时,需要哪些动画呢?一个是自身旋转;一个是平移。然后插值器要选择OvershootInterpolator,达到一个稍微过头再回来的效果。动画view收回时,就是上述的反动画效果。注意,这里需要设置fillAfter=true,原因就不用我多说了吧。
然后是菜单点击选择时,动画view需要哪些动画呢?被点击的菜单view需要两个动画效果:一个是透明度渐变,因为最后要消失;一个是放大。没被选择的菜单view也需要两个:一个是透明度渐变,还有一个是缩小。注意,这里fillAfter=false,因为动画完之后,希望它们回到最初始的地方。
最后是控制打开关闭的菜单view需要的动画,很简单,旋转。

需要注意的就这些。

接下去开始实现:

先定义一些属性:

<resources>
    <attr name="num" format="integer"/>
    <attr name="radius" format="dimension"/>

    <declare-styleable name="UseTweenAnimationView">
        <attr name="num"/>
        <attr name="radius"/>
    </declare-styleable>
</resources>

这里,我定义了两个属性:菜单的数量;菜单的半径。当然你可以多定义一些,比方说动画时间等等。

然后是初始化:

public class UseTweenAnimationView extends ViewGroup {
    /**
     * mRadius = mMenuRadius *  RADIUS_TIME;
     */
    private static final int RADIUS_TIME = 8;
    /**
     * 每个菜单的默认半径(dp)
     */
    private static final int DEF_MENU_RADIUS = 25;
    /**
     * 动画时间
     */
    private static final int DURATION = 1000;

    private int mWidth;
    private int mHeight;

    /**
     * 每个菜单的半径
     */
    private int mMenuRadius;
    /**
     * 每个菜单相对于界面所在的半径
     */
    private int mRadius;
    /**
     * 菜单数量
     */
    private int mMenuNum;

    /**
     * 标记菜单打开关闭情况
     */
    private boolean mMenuClosed = true;
    /**
     * 标记是否在动画中
     */
    private boolean mIsAnimation = false;

    public UseTweenAnimationView(Context context) {
        this(context, null);
    }

    public UseTweenAnimationView(Context context, AttributeSet attrs) {
        super(context, attrs);

        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.UseTweenAnimationView);
        mMenuNum = ta.getInteger(R.styleable.UseTweenAnimationView_num, 5);
        mMenuRadius = ta.getDimensionPixelSize(R.styleable.UseTweenAnimationView_radius,
                (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 25, getResources().getDisplayMetrics()));
        mRadius = mMenuRadius * RADIUS_TIME;
        ta.recycle();

        // 控件的宽高留个菜单直径的余量
        mWidth = mRadius + mMenuRadius * 4;
        mHeight = mWidth;

        // 根据数量,创建动画菜单,这里偷懒直接用textview
        for (int i = 0; i < mMenuNum; i++) {
            TextView menuView = new TextView(context);
            menuView.setBackgroundResource(R.drawable.round_bg);
            menuView.setText(String.valueOf(i + 1));
            menuView.setTextColor(Color.WHITE);
            menuView.setGravity(Gravity.CENTER);

            LayoutParams menuViewLP = new LayoutParams(
                    mMenuRadius * 2,
                    mMenuRadius * 2
            );
            menuView.setLayoutParams(menuViewLP);
            addView(menuView);
        }
        // 根据数量,创建用于点击响应菜
        for (int i = 0; i < mMenuNum; i++) {
            TextView menuView = new TextView(context);
            menuView.setBackgroundResource(R.drawable.round_bg);
            menuView.setText(String.valueOf(i + 1));
            menuView.setTextColor(Color.WHITE);
            menuView.setGravity(Gravity.CENTER);

            LayoutParams menuViewLP = new LayoutParams(
                    mMenuRadius * 2,
                    mMenuRadius * 2
            );
            menuView.setLayoutParams(menuViewLP);
            menuView.setOnClickListener(new OnClickListener() {
                @Override
                public void onClick(View v) {
                    clickMenuItem(v);
                }
            });
            addView(menuView);
        }

        // 创建开关菜单
        ImageView controlView = new ImageView(context);
        controlView.setImageResource(R.drawable.add);
        LayoutParams lp = new LayoutParams(
                mMenuRadius * 2,
                mMenuRadius * 2
        );
        controlView.setLayoutParams(lp);
        controlView.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                if (!mIsAnimation) {
                    mIsAnimation = true;
                    animControlView(v);
                    toggleMenu();
                }
            }
        });
        addView(controlView);
    }

代码可能有点长,不过不复杂。继承自ViewGroup这无可厚非,因为要包含很多view。然后,定义了一些变量值,这里的mIsAnimation这个是为了防止用户在动画还没结束时就点击。
控件的宽和高是一样长的,都为mRadius加上两个菜单view的直径。多了一个菜单view直径的预留是为了给overshoot插值器和放大动画效果预留空间。

然后是先添加动画菜单,再添加响应菜单,最后添加控制开关菜单。这里可以看到都设置了LayoutParams。早加晚加都要加,干脆就放这里了。

菜单view点击响应的clickMenuItem和控制菜单的点击响应animControlView和toggleMenu后面会再贴出来。

ok,接下去是测量尺寸和放置控件

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            final LayoutParams lp = child.getLayoutParams();
            final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, lp.width);
            final int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, lp.height);
            child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        }
        setMeasuredDimension(mWidth, mHeight);
    }

    /**
     * 这里偷懒了,明确大小
     *
     * @param spec
     * @param childDimension
     * @return
     */
    private int getChildMeasureSpec(int spec, int childDimension) {
        return MeasureSpec.makeMeasureSpec(childDimension, MeasureSpec.EXACTLY);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (changed) {
            // 先放动画菜单
            for (int i = 0; i < mMenuNum; i++) {
                View child = getChildAt(i);
                child.layout(0, 0, child.getMeasuredWidth(), child.getMeasuredHeight());
                child.setVisibility(View.INVISIBLE);
            }
            // 再放用于响应的菜单
            // 计算一个角度
            final double angle = Math.PI / 2 / (mMenuNum - 1);
            for (int i = 0; i < mMenuNum; i++) {
                View child = getChildAt(i + mMenuNum);
                int cl = 0;
                int ct = 0;
                if (i == 0) {
                    cl = 0;
                    ct = mRadius;
                } else if (i == mMenuNum - 1) {
                    cl = mRadius;
                    ct = 0;
                } else {
                    cl = (int) (mRadius * Math.sin(angle * i));
                    ct = (int) (mRadius * Math.cos(angle * i));
                }
                child.layout(cl, ct, cl + mMenuRadius * 2, ct + mMenuRadius * 2);
                child.setVisibility(View.INVISIBLE);
            }

            // 最后放开关菜单
            View controlView = getChildAt(mMenuNum * 2);
            controlView.layout(0, 0, controlView.getMeasuredWidth(), controlView.getMeasuredHeight());
        }
    }

这里,测量尺寸时,我偷懒了,每个view长宽直接用固定值,也就是最初读入进来的半径*2。然后整个控件的宽高在上面有讲过,就是mRadius+view的直径*2,预留一个view的直径。

然后是放置view。负责动画的view的位置很简单,就是0,0点;负责响应的view的位置稍微麻烦点。需要通过sin,cos去计算一下它们的x和y点。
为了方便讲解,我贴张图:
这里写图片描述

view1和view5的坐标很好计算,它们的x点分别是0和mRadius,y点分别是mRadius和0。然后是view2,view3,view4,它们的x和y需要通过弧度去计算,先算出90度被分为4个一样大小的小角度a,然后view2的x就是sina*mRadius,view3的x是sin(2a)*mRadius,以此类推,应该不难理解。

ok,尺寸量好,位置放好,接着就是设置响应了。
在上面已经有贴出调用点,就是初始化那里。这里就贴出详细定义的方法:

    private void animControlView(View v) {
        RotateAnimation rotateAnimation = new RotateAnimation(0, 270,
                Animation.RELATIVE_TO_SELF, 0.5f,
                Animation.RELATIVE_TO_SELF, 0.5f);
        rotateAnimation.setDuration(DURATION);
        rotateAnimation.setFillAfter(true);
        v.startAnimation(rotateAnimation);
    }

上面就是控制菜单点击的动画效果。
接着是toggleMenu

    private void toggleMenu() {
        final double angle = Math.PI / 2 / (mMenuNum - 1);
        for (int i = 0; i < mMenuNum; i++) {
            final View animChild = getChildAt(i);
            final View showChild = getChildAt(mMenuNum+i);
            int cl = 0;
            int ct = 0;
            if (i == 0) {
                cl = 0;
                ct = mRadius;
            } else if (i == mMenuNum - 1) {
                cl = mRadius;
                ct = 0;
            } else {
                cl = (int) (mRadius * Math.sin(angle * i));
                ct = (int) (mRadius * Math.cos(angle * i));
            }
            animChild.setVisibility(View.VISIBLE);
            if (!mMenuClosed) {
                showChild.setVisibility(View.INVISIBLE);
            }
            AnimationSet animationSet = new AnimationSet(true);
            animationSet.setInterpolator(new OvershootInterpolator(2f));

            TranslateAnimation translateAnimation = null;
            RotateAnimation rotateAnimation = null;

            if (mMenuClosed) {
                translateAnimation = new TranslateAnimation(0, cl, 0, ct);
                rotateAnimation = new RotateAnimation(0, 1440,
                        Animation.RELATIVE_TO_SELF, 0.5f,
                        Animation.RELATIVE_TO_SELF, 0.5f);
            } else {
                translateAnimation = new TranslateAnimation(cl, 0, ct, 0);
                rotateAnimation = new RotateAnimation(1440, 0,
                        Animation.RELATIVE_TO_SELF, 0.5f,
                        Animation.RELATIVE_TO_SELF, 0.5f);
            }
            translateAnimation.setFillAfter(true);
            rotateAnimation.setFillAfter(true);
            // 设置一些延迟时间
            translateAnimation.setStartOffset((i * 100) / mMenuNum);
            rotateAnimation.setStartOffset((i * 100) / mMenuNum);

            animationSet.addAnimation(rotateAnimation);
            animationSet.addAnimation(translateAnimation);
            animationSet.setDuration(DURATION);

            final int position = i;
            animationSet.setAnimationListener(new Animation.AnimationListener() {
                @Override
                public void onAnimationStart(Animation animation) {

                }
                @Override
                public void onAnimationEnd(Animation animation) {
                    if (mMenuClosed) {
                        animChild.setVisibility(View.INVISIBLE);
                        showChild.setVisibility(View.VISIBLE);
                        if (position == mMenuNum - 1) {
                            mIsAnimation = false;
                            mMenuClosed = false;
                        }
                    } else {
                        animChild.setVisibility(View.INVISIBLE);
                        showChild.setVisibility(View.INVISIBLE);
                        if (position == mMenuNum - 1) {
                            mIsAnimation = false;
                            mMenuClosed = true;
                        }
                    }
                }
                @Override
                public void onAnimationRepeat(Animation animation) {

                }
            });
            animChild.startAnimation(animationSet);
        }
    }

这里,要先计算出动画到哪个位置。当然,你也可以在onLayout那里把坐标保存下来,然后这里直接使用。确定好动画的路径,接着就是创建组合动画效果:旋转加平移。这里添加的顺序很重要:先添加旋转,再添加平移,至于为什么,你可以试试。

然后可以设置一些延迟,让菜单响应有个先后的感觉。然后监听一下动画,获取到动画结束回调,然后做处理。这里可能有朋友有疑问,为啥每个动画都做监听,给最后一个设置监听不就行了,因为最后一个结束了,也就意味着全部结束了。没错,这样思路是对的,不过,这样会有一个不好的效果,为啥呢?因为我在前面加了延时动画,如果给最后一个设置监听,然后在动画结束得到回调时再去遍历每个view,去隐藏,就会看起来不自然,所以只能给每个做动画监听,每次结束就马上把当前的动画view隐藏起来。至于为啥要隐藏,应该不用我说吧。

ok,最后就是点击菜单时的动画效果了:

    private void clickMenuItem(final View v) {
        AnimationSet animationSet = new AnimationSet(true);
        animationSet.setInterpolator(new AccelerateInterpolator(1));
        animationSet.setDuration(DURATION/2);

        ScaleAnimation scaleAnimation = new ScaleAnimation(1, 2, 1, 2,
                Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
        AlphaAnimation alphaAnimation = new AlphaAnimation(1, 0);
        animationSet.addAnimation(scaleAnimation);
        animationSet.addAnimation(alphaAnimation);
        animationSet.setAnimationListener(new Animation.AnimationListener() {
            @Override
            public void onAnimationStart(Animation animation) {
            }
            @Override
            public void onAnimationEnd(Animation animation) {
                v.setVisibility(View.INVISIBLE);
            }
            @Override
            public void onAnimationRepeat(Animation animation) {
            }
        });
        v.startAnimation(animationSet);

        for (int i = 0; i < mMenuNum; i++) {
            final View showChild = getChildAt(i+mMenuNum);
            if (showChild != v) {
                AnimationSet as = new AnimationSet(true);
                as.setInterpolator(new AccelerateInterpolator(3));
                as.setDuration(DURATION/2);

                ScaleAnimation sa = new ScaleAnimation(1, 0, 1, 0,
                        Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
                AlphaAnimation aa = new AlphaAnimation(1, 0);
                as.addAnimation(sa);
                as.addAnimation(aa);
                as.setAnimationListener(new Animation.AnimationListener() {
                    @Override
                    public void onAnimationStart(Animation animation) {
                    }
                    @Override
                    public void onAnimationEnd(Animation animation) {
                        showChild.setVisibility(View.INVISIBLE);
                        mMenuClosed = true;
                    }
                    @Override
                    public void onAnimationRepeat(Animation animation) {
                    }
                });
                showChild.startAnimation(as);
            }
        }
    }

照例,使用组合动画,点击的view添加放大和透明的动画;其余view添加缩小和透明的动画。

至此,一个鲜活的卫星菜单就完成了,这样一路下来不难吧。

闪亮的文字

接着,我来讲讲闪亮的文字怎么实现。
这个的实现需要用到的核心技术是LinearGradient。什么是LinearGradient,就是线性渐变,通俗点讲,就是可以设置颜色的线性变化。比方说,我起点设置白色,终点设置黑色,然后设置到一个空图上,就会出现从白色渐变到黑色的颜色。

那为啥这个会和animation扯到一起呢?其实,这只是我的心血来潮。当然,你也可以不用animation。

既然扯到animation,那就讲一下怎么把Animation使用进去。大家都知道,Animation它的值变化过程是从0到1或者从1到0,然后插值器控制从0到1的变化过程。既然如此,那么我们就可以利用这0到1的自动产生值的特性,用比例去放大它,就能控制很多方式了。

不懂?我举个例子,我现在要实现月亮绕地球旋转的效果。我有很多种方案去实现月亮绕行的速度,有一个办法就是用animation。首先,我要知道月亮绕地球的轨迹的长度,然后通过animation从0到1变化的特性,把这个值再乘以月亮绕行轨迹长度,不就可以不用计算速度了吗。因为我给animation安个线性插值器,它的比例自然是线性增长,也就是匀速了。最妙的是,我觉得匀速缺乏美感,那安个OverShoot..啥的,你想咋地就咋地,不用你自己去设定这个变化过程,animation本身会提供给你值,多妙哉!

ok,闲话就说到这,我们来实现。

public class ShiningTextView extends TextView{
    /**  
     * 可以加入插值器,简单实现更多特效
     */
    private Animation mAnimation;
    /**  
     * 线性颜色变化控制类
     */
    private LinearGradient mLinearGradient;
    private Matrix mGradientMatrix;

    private int mWidth = 0;
    private int mHeight = 0;

    private float mOffset = 0;

    public ShiningTextView(Context context) {
        this(context, null);
    }

    public ShiningTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        if (mWidth == 0) {
            mWidth = getMeasuredWidth();
            if (mWidth > 0) {
                final Paint paint = getPaint();
                mGradientMatrix = new Matrix();
                mLinearGradient = new LinearGradient(-mWidth, 0, 0, 0, 
                        new int[] {0x33ffffff, 0xffffffff, 0x33ffffff}, new float[] {0, 0.5f, 1}, Shader.TileMode.CLAMP);
                paint.setShader(mLinearGradient);
            }
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mGradientMatrix.setTranslate(mOffset, 0);
        mLinearGradient.setLocalMatrix(mGradientMatrix);
    }

    public void startShining(Interpolator interpolator, int duration, int repeatCount) {
        mAnimation = new Animation() {

            @Override
            protected void applyTransformation(float interpolatedTime,
                    Transformation t) {
                super.applyTransformation(interpolatedTime, t);
                mOffset = interpolatedTime*mWidth+mWidth;
                invalidate();
            }
        };
        mAnimation.setAnimationListener(new AnimationListener() {

            @Override
            public void onAnimationStart(Animation animation) {
            }
            @Override
            public void onAnimationRepeat(Animation animation) {
            }
            @Override
            public void onAnimationEnd(Animation animation) {
                getPaint().setShader(null);
            }
        });
        mAnimation.setDuration(duration);
        mAnimation.setInterpolator(interpolator);
        mAnimation.setRepeatCount(repeatCount);
        mAnimation.setRepeatMode(Animation.RESTART);
        startAnimation(mAnimation);
    }
}

代码不多,我就直接贴出来了。
重点就是applyTransformation的应用。它传进来的interpolatedTime是从0到1变化的,这个变化在你设置的duration时间里,变化过程由interpolator插值器决定,很简单明了吧。

至此,这两个实例就讲到这。
感兴趣的朋友可以下载源码(用Android studio创建的):代码入口

猜你喜欢

转载自blog.csdn.net/sinat_30276961/article/details/49868409
今日推荐