Claws Garden

Android基础03——用户界面进阶

动画 Animation

属性动画

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

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

旋转封面

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

在Activity.java中使用:

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

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

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

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

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

呼吸动画

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

在Activity中定义:

 1View imageView = findViewById(R.id.image_view);
 2 
 3ObjectAnimator scaleXAnimator = ObjectAnimator.ofFloat(imageView,"scaleX", 1.1f, 0.9f);
 4scaleXAnimator.setRepeatCount(ValueAnimator.INFINITE);
 5scaleXAnimator.setInterpolator(new LinearInterpolator());
 6scaleXAnimator.setDuration(1000);
 7scaleXAnimator.setRepeatMode(ValueAnimator.REVERSE);// 表示一轮动画结束后倒放回去
 8
 9ObjectAnimator scaleYAnimator = ObjectAnimator.ofFloat(imageView,"scaleY", 1.1f, 0.9f);
10scaleYAnimator.setRepeatCount(ValueAnimator.INFINITE);
11scaleYAnimator.setInterpolator(new LinearInterpolator());
12scaleYAnimator.setDuration(1000);
13scaleYAnimator.setRepeatMode(ValueAnimator.REVERSE);
14
15// 使用一个动画集包裹两个动画,然后同时播放两个动画
16AnimatorSet animatorSet = new AnimatorSet();
17animatorSet.playTogether(scaleXAnimator, scaleYAnimator);
18animatorSet.start();

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

 1<?xml version="1.0" encoding="utf-8"?>
 2<set xmlns:android="http://schemas.android.com/apk/res/android">
 3    <objectAnimator
 4        android:duration="1000"
 5        android:valueFrom="1.1"
 6        android:valueTo="0.9"
 7        android:propertyName="scaleX"
 8        android:interpolator="@android:anim/linear_interpolator"
 9        android:repeatMode="reverse"
10        android:repeatCount="infinite" />
11
12    <objectAnimator
13        android:duration="1000"
14        android:valueFrom="1.1"
15        android:valueTo="0.9"
16        android:propertyName="scaleY"
17        android:interpolator="@android:anim/linear_interpolator"
18        android:repeatMode="reverse"
19        android:repeatCount="infinite" />
20</set>

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

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

页面切换动画

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

下面是一个示例:

 1public class TransitionActivity extends AppCompatActivity {
 2
 3    private static final String EXTRA_EXIT_ANIM = "extra_exit_anim";
 4
 5    private int exitAnim;
 6
 7    @Override
 8    protected void onCreate(Bundle savedInstanceState) {
 9        super.onCreate(savedInstanceState);
10        setContentView(R.layout.activity_transition);
11
12        exitAnim = getIntent().getIntExtra(EXTRA_EXIT_ANIM, 0);
13        bindTransition(R.id.btn_slide_vertical, R.anim.slide_up, R.anim.slide_down);
14        bindTransition(R.id.btn_slide_horizontal, R.anim.slide_right, R.anim.slide_left);
15        bindTransition(R.id.btn_fade, R.anim.fade_in, R.anim.fade_out);
16    }
17
18    @Override
19    public void finish() {
20        super.finish();
21        if (exitAnim != 0) {
22            overridePendingTransition(0, exitAnim);
23        }
24    }
25
26    private void bindTransition(final int btnId, final int enterAnim, final int exitAnim) {
27        findViewById(btnId).setOnClickListener(new View.OnClickListener() {
28            @Override
29            public void onClick(View v) {
30                Intent intent = new Intent(TransitionActivity.this, TransitionActivity.class);
31                intent.putExtra(EXTRA_EXIT_ANIM, exitAnim); // 通过intent携带退出时应该播放的动画信息
32                startActivity(intent);
33                overridePendingTransition(enterAnim, 0); // 注意到放在startActivity之后也是可以的
34            }
35        });
36    }
37}

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

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

Drawable动画

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

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

1<com.airbnb.lottie.LottieAnimationView
2    android:id="@+id/animation_view"
3    android:layout_width="wrap_content"
4    android:layout_height="wrap_content"
5    android:layout_gravity="center"
6    app:lottie_autoPlay="true"
7    app:lottie_loop="true"
8    app:lottie_rawRes="@raw/material_wave_loading" />

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

Fragment

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

创建

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

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

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

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

声明周期

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

fragment_lifecycle

静态使用

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

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

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

动态创建

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

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

ViewPager + Fragment

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

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

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

 1<?xml version="1.0" encoding="utf-8"?>
 2<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
 3    android:layout_width="match_parent"
 4    android:layout_height="match_parent">
 5
 6    <android.support.v4.view.ViewPager
 7        android:id="@+id/view_pager"
 8        android:layout_width="match_parent"
 9        android:layout_height="match_parent" />
10
11</FrameLayout>

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

 1ViewPager pager = findViewById(R.id.view_pager);
 2pager.setAdapter(new FragmentPagerAdapter(getSupportFragmentManager()) {
 3    // 核心方法,返回每页对应的Fragment
 4    @Override
 5    public Fragment getItem(int i) {
 6        return new HelloFragment();
 7    }
 8
 9    // 返回总页数
10    @Override
11    public int getCount() {
12        return 3;
13    }
14});

ViewPager + TabLayout + Fragment

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

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

 1<?xml version="1.0" encoding="utf-8"?>
 2
 3<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
 4    android:layout_width="match_parent"
 5    android:layout_height="match_parent"
 6    android:orientation="vertical">
 7
 8    <android.support.design.widget.TabLayout
 9        android:id="@+id/tab_layout"
10        android:layout_width="match_parent"
11        android:layout_height="40dp" />
12
13    <android.support.v4.view.ViewPager
14        android:id="@+id/view_pager"
15        android:layout_width="match_parent"
16        android:layout_height="match_parent" />
17
18</LinearLayout>

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

 1@Override
 2protected void onCreate(Bundle savedInstanceState) {
 3    super.onCreate(savedInstanceState);
 4    setContentView(R.layout.activity_view_pager_with_tab);
 5    ViewPager pager = findViewById(R.id.view_pager);
 6    TabLayout tabLayout = findViewById(R.id.tab_layout);
 7    pager.setAdapter(new FragmentPagerAdapter(getSupportFragmentManager()) {
 8        @Override
 9        public Fragment getItem(int i) {
10            return new HelloFragment();
11        }
12
13        @Override
14        public int getCount() {
15            return PAGE_COUNT;
16        }
17
18        // 返回当前页的标题,给TabLayout使用
19        @Override
20        public CharSequence getPageTitle(int position) {
21            return "Hello " + position;
22        }
23    });
24    // 一句话绑定(合体!)
25    tabLayout.setupWithViewPager(pager);
26}

Fragment与Activity之间的通信

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

 1public final class ColorFragment extends Fragment {
 2    private static final String KEY_EXTRA_COLOR = "extra_color";
 3    public static ColorFragment newInstance(int color) {
 4        ColorFragment cf = new ColorFragment();
 5        Bundle args = new Bundle();
 6        args.putInt(KEY_EXTRA_COLOR, color);
 7        cf.setArguments(args);
 8        return cf;
 9    }
10    @Override
11    public View onCreateView(@NonNull LayoutInflater inflater,
12                             @Nullable ViewGroup container,
13                             @Nullable Bundle savedInstanceState) {
14        int color = Color.BLUE;
15        Bundle args = getArguments();
16        if (args != null) {
17            color = args.getInt(KEY_EXTRA_COLOR, Color.BLUE);
18        }
19        View view = inflater.inflate(R.layout.fragment_color, container, false);
20        view.setBackgroundColor(color);
21        return view;
22    }
23}

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

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

 1public final class ColorPlusFragment extends Fragment {
 2    public interface Listener {
 3        void onCollectColor(int color);
 4    }
 5    private Listener mListener;
 6    
 7    @Override
 8    public void onAttach(Context context) {
 9        super.onAttach(context);
10        if (context instanceof Listener) {
11            mListener = (Listener) context;
12        }
13    }
14    @Override
15    public View onCreateView(@NonNull LayoutInflater inflater,
16                             @Nullable ViewGroup container,
17                             @Nullable Bundle savedInstanceState) {
18
19// fire event when needed
20        if (mListener != null) {
21            mListener.onCollectColor(color);
22        }
23        return view;
24    }
25}

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

适应屏幕横竖屏

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

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

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

本节示例工程

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

#Android