动画 Animation

属性动画

可以在Activity中进行定义,或者在单独的一个xml文件中声明,通常放在res/animator/目录下。

用两个例子说明如何使用属性动画:

旋转封面

类似听歌时看到的专辑封面不断旋转的动画。这个动画非常简单,仅仅由一个旋转的动画动作构成。

在Activity.java中使用:

// 新建动画对象,传入要参与动画的view、动画内容和一些必要参数。这里Float表示参数以float类型对待
ObjectAnimator animator = ObjectAnimator.ofFloat(findViewById(R.id.image_view),"rotation", 0, 360);
// 设置循环为无限
animator.setRepeatCount(ValueAnimator.INFINITE);
// 设置线性插值器,即匀速运动
animator.setInterpolator(new LinearInterpolator());
// 设置循环周期,单位为ms
animator.setDuration(8000);
// 设置一个周期结束后以重新开始的方式进入第二个周期
animator.setRepeatMode(ValueAnimator.RESTART);
// 开始播放
animator.start();

在xml中定义,下面为res/animator/rotate.xml的内容:

<?xml version="1.0" encoding="utf-8"?>
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="8000"
    android:propertyName="rotation"
    android:interpolator="@android:anim/linear_interpolator"
    android:repeatCount="infinite"
    android:repeatMode="restart"
    android:valueFrom="0"
    android:valueTo="360" />

然后在Activity中读取动画并播放:

// 从xml读取动画对象
Animator animator = AnimatorInflater.loadAnimator(this, R.animator.breath);
// 设置播放的目标
animator.setTarget(findViewById(R.id.image_view));
// 开始播放
animator.start();

呼吸动画

呼吸动画表现为图标不断变大变小,需要使用缩放来帮助完成。但是属性动画的缩放是x轴和y轴分开处理的,因此需要让x轴和y轴的两个缩放动画同步播放才能达到想要的效果。

在Activity中定义:

View imageView = findViewById(R.id.image_view);
 
ObjectAnimator scaleXAnimator = ObjectAnimator.ofFloat(imageView,"scaleX", 1.1f, 0.9f);
scaleXAnimator.setRepeatCount(ValueAnimator.INFINITE);
scaleXAnimator.setInterpolator(new LinearInterpolator());
scaleXAnimator.setDuration(1000);
scaleXAnimator.setRepeatMode(ValueAnimator.REVERSE);// 表示一轮动画结束后倒放回去

ObjectAnimator scaleYAnimator = ObjectAnimator.ofFloat(imageView,"scaleY", 1.1f, 0.9f);
scaleYAnimator.setRepeatCount(ValueAnimator.INFINITE);
scaleYAnimator.setInterpolator(new LinearInterpolator());
scaleYAnimator.setDuration(1000);
scaleYAnimator.setRepeatMode(ValueAnimator.REVERSE);

// 使用一个动画集包裹两个动画,然后同时播放两个动画
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.playTogether(scaleXAnimator, scaleYAnimator);
animatorSet.start();

在xml中定义就显得比较简单,复用也更为方便,下为breath.xml的内容:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <objectAnimator
        android:duration="1000"
        android:valueFrom="1.1"
        android:valueTo="0.9"
        android:propertyName="scaleX"
        android:interpolator="@android:anim/linear_interpolator"
        android:repeatMode="reverse"
        android:repeatCount="infinite" />

    <objectAnimator
        android:duration="1000"
        android:valueFrom="1.1"
        android:valueTo="0.9"
        android:propertyName="scaleY"
        android:interpolator="@android:anim/linear_interpolator"
        android:repeatMode="reverse"
        android:repeatCount="infinite" />
</set>

在Activity中可以视为一个动画来播放:

Animator animator = AnimatorInflater.loadAnimator(this, R.animator.breath);
animator.setTarget(findViewById(R.id.image_view));
animator.start();

页面切换动画

就像ppt一样,Activity切换时也可以设置动画效果。在这里通过overridePendingTransition(int enterAnim, int exitAnim);方法来完成Activity切换的动画效果。这个函数很有意思,看名称知道它可以覆盖下一次切换时的动画,但是却有两个参数。这两个参数分别是enterAnim和exitAnim,都应该是一个资源ID,enterAnim表示下一次进入任意Activity的动画,而exitAnim表示下一次退出任意一个Activity的动画。如果想从A切换到B,那么B是即将进入的,而A是即将退出的,可以理解为一个切换被分成退出A和进入B两个动作,都可以设置动画。如果传入0,表示这个动作位置不使用动画。

下面是一个示例:

public class TransitionActivity extends AppCompatActivity {

    private static final String EXTRA_EXIT_ANIM = "extra_exit_anim";

    private int exitAnim;

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

        exitAnim = getIntent().getIntExtra(EXTRA_EXIT_ANIM, 0);
        bindTransition(R.id.btn_slide_vertical, R.anim.slide_up, R.anim.slide_down);
        bindTransition(R.id.btn_slide_horizontal, R.anim.slide_right, R.anim.slide_left);
        bindTransition(R.id.btn_fade, R.anim.fade_in, R.anim.fade_out);
    }

    @Override
    public void finish() {
        super.finish();
        if (exitAnim != 0) {
            overridePendingTransition(0, exitAnim);
        }
    }

    private void bindTransition(final int btnId, final int enterAnim, final int exitAnim) {
        findViewById(btnId).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(TransitionActivity.this, TransitionActivity.class);
                intent.putExtra(EXTRA_EXIT_ANIM, exitAnim); // 通过intent携带退出时应该播放的动画信息
                startActivity(intent);
                overridePendingTransition(enterAnim, 0); // 注意到放在startActivity之后也是可以的
            }
        });
    }
}

另外,切换动画必须用xml事先定义好,比如一个从左边滑入的动画(anim/slide_left.xml):

<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"
    android:fromXDelta="0%p" 
    android:toXDelta="100%p"
    android:duration="300"/>

Drawable动画

Drawable Animation 可以实现依次加载一组 Drawable 资源, 它和早期的电影一样 , 加载一组不同的图片,然后像胶卷一样播放就形成了动画。所以Drawable动画是最为灵活的动画,但是制作过程也很复杂,几乎和一帧一帧画动画差不多。

但是好在一些艺术家已经帮我们做好了很多好看的动画,并且以json的格式储存了下来——Lottie动画就是这样的。我们可以直接引入Lottie的依赖,然后下载需要的动画json,在xml中简单定义一下就可以使用了。例如:

<com.airbnb.lottie.LottieAnimationView
    android:id="@+id/animation_view"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center"
    app:lottie_autoPlay="true"
    app:lottie_loop="true"
    app:lottie_rawRes="@raw/material_wave_loading" />

这样的动画甚至不需要在Activity中做任何声明,直接以类似ImageView的形式放在布局文件中即可播放。

Fragment

相当于一个小组件碎片,可以拼合在Activity中构成多样化的视图,而且方便复用。

创建

一个Fragment的创建也分为两部分:xml布局文件和fragment类文件。

xml布局文件的写法和Activity是一样的,可以直接当做一个页面布局文件来写。

fragment类文件需要继承Fragment父类,并至少实现onCreateView方法,例如:

@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
    return inflater.inflate(R.layout.fragment_hello, container, false);
}

声明周期

fragment的声明周期中和Activity的重合度高,但是还多了几个状态,参考下图:

fragment_lifecycle

静态使用

可以直接把定义好的fragment写入其他地方的布局文件中,当做一个View来使用,语法例如:

<fragment
    android:id="@+id/hello_fragment"
    android:name="com.example.chapter3.demo.fragment.HelloFragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

android:name的位置填入类的包路径(Path from source root)即可。

动态创建

也可以不事先把fragment写入布局文件中(有时候不能确定使用具体哪一个fragment),在需要的时候动态填入一个fragment。这样做要求事先在布局文件中放一个用来站位的ViewGroup当做容器,然后把需要的fragment注入到这个容器中达到效果。语法参考下面的例子:

<!--布局文件中,在合适的位置放入一个container-->
<FrameLayout
    android:id="@+id/fragment_container"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />
// 注入具体fragment的方法
getSupportFragmentManager()
        .beginTransaction()
        .add(R.id.fragment_container, new HelloFragment())
        .commit();

ViewPager + Fragment

在一个Activity中通过ViewPager分页,每一页填入一个Fragment,可以滑动切换,非常实用的搭配。

使用时需要一个Adapter来帮助ViewPager填入Fragment内容。这个Adapter不需要自己定义了,抽象类已经定义好,重写一下必要方法就可以。

首先需要在Activity的布局文件中加入一个ViewPager组件:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <android.support.v4.view.ViewPager
        android:id="@+id/view_pager"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</FrameLayout>

然后在Activity的类文件中,使用如下的方式对ViewPager填充内容:

ViewPager pager = findViewById(R.id.view_pager);
pager.setAdapter(new FragmentPagerAdapter(getSupportFragmentManager()) {
    // 核心方法,返回每页对应的Fragment
    @Override
    public Fragment getItem(int i) {
        return new HelloFragment();
    }

    // 返回总页数
    @Override
    public int getCount() {
        return 3;
    }
});

ViewPager + TabLayout + Fragment

在上面的基础上有加了一个导航栏TabLayout,可以显示当前页的标题,更加实用。

先在布局文件中加入TabLayout组件:

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <android.support.design.widget.TabLayout
        android:id="@+id/tab_layout"
        android:layout_width="match_parent"
        android:layout_height="40dp" />

    <android.support.v4.view.ViewPager
        android:id="@+id/view_pager"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</LinearLayout>

然后在Adapter中再重写一个getPageTitle方法,返回每一页的标题。最后将ViewPager和TabLayout绑定一下即可。例如:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_view_pager_with_tab);
    ViewPager pager = findViewById(R.id.view_pager);
    TabLayout tabLayout = findViewById(R.id.tab_layout);
    pager.setAdapter(new FragmentPagerAdapter(getSupportFragmentManager()) {
        @Override
        public Fragment getItem(int i) {
            return new HelloFragment();
        }

        @Override
        public int getCount() {
            return PAGE_COUNT;
        }

        // 返回当前页的标题,给TabLayout使用
        @Override
        public CharSequence getPageTitle(int position) {
            return "Hello " + position;
        }
    });
    // 一句话绑定(合体!)
    tabLayout.setupWithViewPager(pager);
}

Fragment与Activity之间的通信

如果是Activity传数据给Fragment,需要在构造Fragment的时候传入参数。方法是:首先定义Fragment的静态getNewInstance方法传入参数数据(不要使用构造函数传参),然后通过setArguments将打包的数据设置进去。修改onCreateView方法,通过getArguments方法获得参数数据。例如:

public final class ColorFragment extends Fragment {
    private static final String KEY_EXTRA_COLOR = "extra_color";
    public static ColorFragment newInstance(int color) {
        ColorFragment cf = new ColorFragment();
        Bundle args = new Bundle();
        args.putInt(KEY_EXTRA_COLOR, color);
        cf.setArguments(args);
        return cf;
    }
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater,
                             @Nullable ViewGroup container,
                             @Nullable Bundle savedInstanceState) {
        int color = Color.BLUE;
        Bundle args = getArguments();
        if (args != null) {
            color = args.getInt(KEY_EXTRA_COLOR, Color.BLUE);
        }
        View view = inflater.inflate(R.layout.fragment_color, container, false);
        view.setBackgroundColor(color);
        return view;
    }
}

然后在需要用到该Fragment的Activity中动态创建Fragment,并调用newInstance方法传入参数。

如果是Fragment传数据给Activity,最好的办法是接口回调,即把外层Activity设置为Fragment的观察者。设置监听的时候要放在Fragment的onAttach时。Fragment需要传数据的时候通过调用接口中提供的方法操纵Activity。例如:

public final class ColorPlusFragment extends Fragment {
    public interface Listener {
        void onCollectColor(int color);
    }
    private Listener mListener;
    
    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        if (context instanceof Listener) {
            mListener = (Listener) context;
        }
    }
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater,
                             @Nullable ViewGroup container,
                             @Nullable Bundle savedInstanceState) {
…
// fire event when needed
        if (mListener != null) {
            mListener.onCollectColor(color);
        }
        return view;
    }
}

然后Activity需要实现ColorPlusFragment.Listener,并且完成对传来信息的处理。

适应屏幕横竖屏

如果屏幕是横屏的,那么系统会自动从/res/layout-land/下找同名的xml布局文件,没找到才会使用原来的xml。因此可以利用这一点很方便地实现在竖屏时只显示列表,而横屏时在列表右侧通过Fragment显示内容的效果。

在Activity中有一点小技巧判断当前用的是哪一个布局文件:通过id找一个只出现在横屏的View,如果发现为null,则说明这个View现在不存在,那么布局就不是横屏的而是竖屏的。

具体的实现这里不再赘述了,可以参考下面的本节实例代码工程。

本节示例工程

https://github.com/jingjiecb/Chapter-3