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