Android简单实现自定义的动画效果

905 阅读8分钟

我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第4篇文章,点击查看活动详情

我自己公司的业务需求上是基本不会涉及动画相关的,也可以说基本两年没怎么做过动画相关的开发,这次写这篇文章是因为读到一篇比较好的文章,想把它记录下来,以后有做动画效果的需求的话就能快速上手。 参考《Android 开发艺术探索》 7.3.4 对任意属性做动画

众所周知Android里面动画有3种,View动画、帧动画和属性动画。不同的场景使用不同的动画,对控件而言,你想实现一个自定义动画的效果,也就是随心,那就使用属性动画会比较好,因为View动画只提供了简单的平移、翻转、缩放、透明度。 当然这里也不会去介绍一些基础的内容。为什么单独说这一小节,因为我觉得这个地方是这章(第七章)最经典的地方。

一. 对任意属性做动画

按照书上的逻辑来讲。 一开始举了一个栗子,给Button加个动画,让这个Button的宽度增加500px。 直接写属性动画的代码:

    ObjectAnimator.ofInt(mButton, "width", 500).setDuration(5000).start();

这里是对这个Button的 width属性做动画,也就是对宽度这个属性做动画,但是并不能实现我们想要的宽度增加500px的效果。这时候就先引出了一个重要的结论: 属性动画要求动画作用的对象提供该属性的get和set方法 这个很重要,意思就是说,这个mButton对象,没有提供getWidth和setWidth方法。其实这说法有点那啥,因为Button是有getWidth和setWidth的方法,但是为什么没生效呢? 这是展示了setWidth的源码

    public void setWidth(int pixels) {
        mMaxWidth = mMinWidth = pixels;
        mMaxWidthMode = mMinWidthMode = PIXELS;

        requestLayout();
        invalidate();
    }

发现这里并不是设置Button的宽度,而是设置Button的最大宽度和最小宽度。 也就是说在上面的动画中:真的的是随着时间的改变去改变控件的最大最小宽度,所以视觉看不出效果,因为实际作用的属性不是你想要去让它作用的属性 所以想要实现效果,应该要提供该属性正确的get/set方法

书上又写出了这样的一个结论(原话): 针对上诉问题,官方文档上告诉我们有3种解决方法:

  • 给你的对象加上get和set方法,如果你有权限的话
  • 用一个类来包装原始对象,间接为其提供get和set方法
  • 采用ValueAnimator,监听动画过程,自己实现属性的变化

第一个方案肯定不可行,系统的View不是我们说改就改的,然后用两个Demo分别来描述方法2和方法3(不能复制要手写,好不想写)

方法2:

    private static class ViewWrapper{
        private View mTarget;
        
        public ViewWrapper(View target){
            mTarget = target;
        }
        
        public int getWidth(){
            return mTarget.getLayoutParams().width;
        }
        
        public void setWidth(int width){
            mTarget.getLayoutParams().width = width;
            mTarget.requestLayout();
        }

    }

调用的时候

ViewWrapper wrapper = new ViewWrapper(mButton);
ObjectAnimator.ofInt(wrapper , "width", 500).setDuration(5000).start();

这样就能正常展示我们想要的动画效果,对象是wrapper ,它确实为width属性提供了get/set方法。动画内部在执行过程中会拿到传给它的属性,然后用反射去调用它的get/sset方法。但是怎么说呢,具体的属性改变的算法还是要你去手动写。

方法3:

        ValueAnimator valueAnimator = ValueAnimator.ofInt(1, 100);
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {

            private IntEvaluator mEvaluator = new IntEvaluator();

            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                int currentValue = (Integer) animation.getAnimatedValue();
                float fraction = animator.getAnimatedFraction();
                view.getLayoutParams().width = mEvaluator.evaluate(fraction, start, end);
                view.requestLayout();
            }
        });

这个方法就是对动画进行监听,每监听到一帧的时候再做具体的操作。可以看到这里也没写get/set方法,大概就那么一个意思就得了,也不是非得说一定要这样写,或者不用这两种方法也还有其它方法能实现。 总之,属性的具体的变化,是需要我们自己去写逻辑(如果要实现自定义的效果),而ObjectAnimator、ValueAnimator这些属性广告的类,能告诉我们这个动画执行的整个过程,简单的说就是我们知道播放到哪一帧,就能做到在这一帧做什么效果。 这两个Demo也是我觉得这章最有意思的地方。

二. 自己实现一个自定义动画

光看肯定是3天忘,自己动手写个简单的自定义动画效果。 我弄一个圆形View,可以拖动它,点击下去的时候圆会放大并变成正方形,然后移动,松开时正方形会缩小并变回圆,而且我们要让圆变成正方形不是秒变,要有个渐变的效果(只实现了简单的渐变效果,因为懒得去计算)。 最终的效果也没有做成gif,想看效果的直接复制代码去试就行,不多。

####1. 绘制view的初始位置 我这Demo再补充一些细节的东西,还是《Android 开发艺术探索》3.1.2 View的位置参数 不是什么难点,就是有些细节要注意,这个view的位置参数列举了3套 (1) Left, Right, Top, Bottom , 当成一个矩形看(圆也是矩形),就是4条边距离父容器x\y轴的距离。 (2) x, y 左上点的坐标 (3)translationX,translationY 距离父View坐标的偏移量

我就直接贴代码讲吧。 先看Activity布局

<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/fl_content"
    android:padding="100px"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
</FrameLayout>

这里padding一个100px,是为了方便介绍这些方位,单位相同容易看,实际开发中肯定最好用dp做单位。 然后看Activity代码

public class RsActivity extends Activity {

    private FrameLayout flContent;
    private RoundSquareView roundSquareView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_rs);
        initView();
    }

    private void initView(){
        flContent = findViewById(R.id.fl_content);
        roundSquareView = new RoundSquareView(this);
        FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(150, 150);
        roundSquareView.setLayoutParams(lp);
        flContent.addView(roundSquareView);
        roundSquareView.setX(100);
        roundSquareView.setY(100);
    }

    @Override
    protected void onResume() {
        super.onResume();
        Handler handler = new Handler();
        handler.postDelayed(new Runnable() {
            @Override
            public void run() {
                Log.v("mmp", "getLeft "+roundSquareView.getLeft());
                Log.v("mmp", "getTranslationX "+roundSquareView.getTranslationX());
                Log.v("mmp", "getX "+roundSquareView.getX());
                roundSquareView.setX(0);
            }
        }, 1000);
    }
}

RoundSquareView是一个自定义View, 先不用管它。 第一个问题来了,为什么要写个延迟,重点啊,因为getLeft ,就是获取第一套的那4个属性,要在view绘制完成之后才能获取到,不然获取到的会是0,加个延迟是为了保证绘制完成(这只是Demo,真实开发中判断绘制完成肯定不能用延时这样玩) 看看显示结果

amin1.png 再看看打印结果

amin2.png 距离顶部和左边都是200px,但是left打印是100,如果把父布局中的padding去掉的话,left会打印0,这就是第二个地方,使用Left的时候要注意padding,并且它不算translationX偏移的部分。 然后translationX也打印的是100,他也不算是padding的部分。 最后getX是200,他是正常的相对于父布局的部分距离,不受任何影响。 也证实了x = left + translationX 这里只是为了介绍一些细节上的东西而已,onResume里面的代码对我们要实现的功能没有任何意义,所以使用时请屏蔽掉。

2. 自定义View

看看代码

public class RoundSquareView extends View {

    private float oldRawX = 0;
    private float oldRawY = 0;

    private int currentValue = 0;
    private boolean isExpand;

    public RoundSquareView(Context context) {
        super(context);
    }

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

    public RoundSquareView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public void draw(Canvas canvas) {
        super.draw(canvas);
        Paint paint = new Paint();
        paint.setColor(Color.RED);

        paint.setStyle(Paint.Style.FILL);
        paint.setStrokeWidth(1);

        int w = getWidth() /2;
        int h = getHeight() /2;

        if (!isExpand) {
            // 非扩大状态下的绘制
            canvas.drawCircle(w, h, w + currentValue , paint);
        }else {
            // 扩大状态下的绘制
            canvas.drawCircle(w, h, w + (100 - currentValue) , paint);
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                isExpand = false;
                expand();
                oldRawX = event.getRawX();
                oldRawY = event.getRawY();
                return true;
            case MotionEvent.ACTION_MOVE:
                moveView(event.getRawX(), event.getRawY());
                return true;
            case MotionEvent.ACTION_UP:
                oldRawX = 0;
                oldRawX = 0;
                isExpand = true;
                narrow();
                return true;
        }
        return super.onTouchEvent(event);
    }

    @Override
    public boolean performClick() {
        return super.performClick();
    }

    private void moveView(float rawX, float rawY){
        // 计算偏移量
        float offsetX = rawX - oldRawX;
        float offsetY = rawY - oldRawY;
        // 更改位置
        setX(getX()+offsetX);
        setY(getY()+offsetY);
        // 更新位置
        oldRawX = rawX;
        oldRawY = rawY;
    }

    /**
     *  扩大
     */
    private void expand(){
        int w = getWidth();
        int h = getHeight();
        ViewGroup.LayoutParams lp = this.getLayoutParams();
        ValueAnimator valueAnimator = ValueAnimator.ofInt(1, 100);
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                currentValue = (int) animation.getAnimatedValue();
                Log.v("mmp", "动画进度 "+currentValue);
                lp.width = w + (w/100 * currentValue);
                lp.height = h + (h/100 * currentValue);
                RoundSquareView.this.setLayoutParams(lp);
            }
        });
        valueAnimator.setDuration(300).start();
    }

    /**
     *  缩小
     */
    private void narrow(){
        int w = getWidth();
        int h = getHeight();
        ViewGroup.LayoutParams lp = this.getLayoutParams();
        ValueAnimator valueAnimator = ValueAnimator.ofInt(1, 100);
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                currentValue = (int) animation.getAnimatedValue();
                Log.v("mmp", "动画进度 "+currentValue);
                lp.width = w - (w/200 * currentValue);
                lp.height = h - (h/200 * currentValue);
                RoundSquareView.this.setLayoutParams(lp);
            }
        });
        valueAnimator.setDuration(300).start();
    }


}

自定义View,在draw中用Paint画个圆,这个,这个没什么好说的,都能看懂,最后

        if (!isExpand) {
            // 非扩大状态下的绘制
            canvas.drawCircle(w, h, w + currentValue , paint);
        }else {
            // 扩大状态下的绘制
            canvas.drawCircle(w, h, w + (100 - currentValue) , paint);
        }

先不用管,先看成

canvas.drawCircle(w, h, w, paint);

这样就画成一个圆了。 然后写onTouchEvent让view随手指移动而移动。记录改变前的手指的点击位置oldRawX,oldRawY,去获取当前的位置减去旧的位置就能得到一个偏移量,然后再用这个偏移量去改变x和y属性,这是最简单的view随手指移动的方式。 然后在按下时调用expand()做放大的属性动画,再抬起时调用narrow()做缩小的属性动画。

这里是用了上面的方法3去实现属性动画,这里离就不多说了,定义了一个变量isExpand来记录当前是否处于放大的状态。 最后,这个Demo只是简单的实现动画的效果,会存在一些问题,比如说在放大动画的执行过程中抬起,这些都没做处理,时间问题,就不打算花太多时间在这个Demo上。

文章出自www.jianshu.com/p/ad80cea0a… 当然也是自己写的,是同一个号,只是最近打算转平台

这是很早之前写的文章,当时还是有点年轻,技术差点火候。这里更正一下,onResume那不用延时,用post就行