要实现什么效果?

android 展开 android展开折叠动画_android 展开


我们就是要实现如图所示的动画效果,在开始之前我们先了解一下实现这个动画的相关知识。

属性动画相关知识

动画执行的逻辑

逻辑大概流程如下:

  1. 为 ValueAnimator 设置动画的时长,以及对应属性的始 & 末值
  2. 设置属性在 始 & 末值 间的变化逻辑
  • TimeInterpolator实现类:插值器-描述动画的变化速率
  • TypeEvaluator实现类:估值器-描述 属性值 变化的具体数值
  1. 根据2中的逻辑更新当前值
  2. 获取3中更新的值 ,修改目标属性值
  3. 刷新视图
  4. 重复4-5,直到 属性值 == 末值

动画工作的关键类

Java类

说明

ValueAnimator

动画执行类,核心。负责动画的整体协调

ObjectAnimator

动画执行类

TimeInterpolator

时间插值(插值器接口),控制动画变化率

TypeEvaluator

类型估值(估值器接口),设置属性值计算方式,根据属性的始末值和插值计算出当前时间的属性值

AnimatorSet

动画集

AnimatorInflater

加载属性动画的XML文件

思路分析与实现代码

android 展开 android展开折叠动画_属性动画_02


首先我们实现这样一个TextView用于展示,实现代码如下:

<TextView
        android:id="@+id/tv_left"
        android:layout_width="wrap_content"
        android:layout_height="60dp"
        android:layout_gravity="end"
        android:layout_marginTop="30dp"
        android:background="@drawable/circleside"
        android:gravity="center_vertical"
        android:maxLines="1"
        android:paddingLeft="16dp"
        android:paddingRight="16dp"
        android:text="青柠天气提醒您,天冷注意添加衣"
        android:textSize="17sp" />

其中的@drawable/circleside 代码如下:

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle" >  
        <!-- 填充颜色 -->  
        <solid android:color="#40FF552E"></solid>  
        <!-- 矩形的圆角半径 --> 
    <corners android:bottomLeftRadius="999dp" android:topLeftRadius="999dp"></corners>
</shape>

其实思路很简单,就是先将View的宽度设置为0,随着时间的增多逐渐把View的宽度设置为它应该有的宽度即可。

private ValueAnimator createDropAnimator(final View v, int start, int end) {
        ValueAnimator animator = ValueAnimator.ofInt(start, end);
        animator.addUpdateListener(arg0 -> {
            int value = (int) arg0.getAnimatedValue();
            ViewGroup.LayoutParams layoutParams = v.getLayoutParams();
            layoutParams.width = value;
            v.setLayoutParams(layoutParams);

        });
        return animator;
    }

首先我们构建一个ValueAnimator,添加了一个UpdateLisenter的方法,这个方法是在动画开始后返回值来让我们进行处理的,这个方法我们传入了三个参数分别是View,start,end,分别代表了我们创建的那个TextView,动画开始的宽度,最终的宽度。我们在updateListener里对view的宽度进行的修改。

private void show(View view, int tvWidth, long delay) {
        view.setVisibility(View.VISIBLE);
        ValueAnimator valueAnimator = createDropAnimator(view, 0, tvWidth);
        valueAnimator.setDuration(500);
        valueAnimator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationStart(Animator animation) {
                super.onAnimationStart(animation);
            }

            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
                Timer timer = new Timer();
                TimerTask timerTask = new TimerTask() {
                    @Override
                    public void run() {
                        runOnUiThread(() -> disMiss(view, tvWidth));
                    }
                };
                timer.schedule(timerTask, delay);

            }
        });
        valueAnimator.start();
    }

这个是触发动画的方法,第三行调用了上述的ValueAnimator的构建方法,从0到width的一个变化过程。在打开动画结束后我们又延时delay ms调用了dismiss方法来把view收起来。

private void disMiss(View view, int tvWidth) {
        ValueAnimator valueAnimator = createDropAnimator(view, tvWidth, 0);
        valueAnimator.setDuration(500);
        valueAnimator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationStart(Animator animation) {
                super.onAnimationStart(animation);
            }

            @Override
            public void onAnimationEnd(Animator animation) {
                view.setVisibility(View.GONE);
            }

        });
        valueAnimator.start();
    }

dismiss方法的思路和show的差不多,只不过是从width到0的一个过程。

进过上述的3个方法我们的动画基本上已经完成了

要踩的坑

1. show()方法何时调用(动画)何时开始?

如果你在onCreate里调用show方法,动画播放的过程是这样的。等你看见View的时候动画已经播放到一半了,为什么会这样?我们要了解Activity的生命周期。
onCreate和onStart调用之后我们还不能看见Activity的视图,所以调用之后动画已经绘制了一半了我们还没有看见。那我们放在onResume里再调用不就可以了吗?这样做可以,但是还是有问题,因为我们进入Activity时是有动画的,这个动画是和我们的动画一起执行的,所以还是会有可能看不全的(概率较小)。这里我们就要引入一个监听了,代码如下:

ViewTreeObserver viewTreeObserver = getWindow().getDecorView().getViewTreeObserver();
        viewTreeObserver.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
            @Override
            public boolean onPreDraw() {
                show(mTextView,tvWidth,1000);
                getWindow().getDecorView().getViewTreeObserver().removeOnPreDrawListener(this::onPreDraw);
                return true;
            }
        });

这个方法监听了onDraw方法,在onDraw执行之前就会调用这个show方法保证我们的动画播放完全。

2.为什么横幅(View)有时候会闪一下?

有时候View会闪,就是因为View本来就是VISIBLE的,动画开始的时候回突然把view的宽度置为0,所以会闪一下。如何解决这个问题呢?答案就一句话:

在XML里把View的可见性设置为INVISIBLE或者GONE

3.这个TVWidth(View的宽度)如何获得呢?

众所周知,在OnCreate里是无法获取View的真实宽高的,那我们要如何解决这个问题呢?有人要说了,我百度了,用第一个问题里的监听方法就可以获取宽高。可是这个动画里你能获取到吗?答案是不能,第二个问题里我们已经把View的可见性设置为INVISIBLE或者GONE了,这两个方法View是不会绘制的,所以你根本无法获得View的宽高,那怎么解决呢?答案就是两个字:

计算

tvWidth = ScreenUtils.dip2px(this, (float) (paddingLeft + paddingRight)) +
                ScreenUtils.sp2px(this, (float) ( 17))*s.length();

这里我提供了一个简单的思路,View的宽度就是paddingLeft + paddingRight + 字的宽度;看这个计算方法应该非常简单了。下面是ScreenUtils的三个方法:

public static int dp2px(Context context, float dpValue) {
        float scale = context.getResources().getDisplayMetrics().density;
        return (int) (dpValue * scale + 0.5F);
    }

    public static int px2dp(Context context, float pxValue) {
        float scale = context.getResources().getDisplayMetrics().density;
        return (int) (pxValue / scale + 0.5F);
    }

    public static int sp2px(Context context, float spValue) {
        final float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
        return (int) (spValue * fontScale + 0.5f);
    }

总结

至此,我们已经实现了这个简单的展开收回动画,简单易懂,如果可以的话可以帮我

点个赞吗?

如果有不懂的也可以在评论里问我,下次见拜拜!