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写入其他地方的布局文件中,当做一个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现在不存在,那么布局就不是横屏的而是竖屏的。
具体的实现这里不再赘述了,可以参考下面的本节实例代码工程。