在Android中动画能够分为3种:View动画、帧动画和属性动画。View动画从名字就能够大体知道,View动画是对View作图形变换(平移、缩放、旋转、透明度)从而产生动画效果,而且View动画支持自定义。帧动画是经过顺序播放一系列的图片从而产生的动画效果。属性动画是在API11(Android3.0)引进的动画效果,它是经过动态地改变对象的属性从而达到动画效果。php
View动画的做用对象是View,它支持4种动画效果,分别是平移、缩放、旋转、透明度动画。View动画的四种动画效果对应着Animation类的四个子类: TranslateAnimation、ScaleAnimation、RotateAnimation、AlphaAnimation。这4种动画便可以经过XML来定义,也能经过Java代码来动态建立。对于View动画来讲,推荐使用XML来定义,可读性高。java
名称 | 标签 | 子类 | 效果 |
---|---|---|---|
平移动画 | < translate > | TranslateAnimation | 移动View |
缩放动画 | < scale > | ScaleAnimation | 放大或缩小View |
旋转动画 | < rotate > | RotateAnimation | 旋转View |
透明度动画 | < alpha > | AlphaAnimation | 改变View的透明度 |
在实现动画效果以前,先讲一下Animation类,它是一个抽象类,能够应用于视图、表面或其余对象的动画。 Animation一些经常使用的方法:android
Animation类经常使用的方法 | 做用 |
---|---|
reset() | 重置Animation的初始化 |
cancel() | 取消Animation动画 |
start() | 开始Animation动画 |
setAnimationListener() | 给当前Animation设置动画监听 |
hasStarted() | 判断当前Animation是否开始 |
hasEnded() | 判断当前Animation是否结束 |
View类对Animation的经常使用方法:git
方法 | 做用 |
---|---|
startAnimation(Animation animation) | 对当前View开始设置的Animation动画 |
clearAnimation() | 取消当View在执行的Animation动画 |
Animation经常使用的属性:api
xml属性 | java方法 | 做用 |
---|---|---|
android:duration | setDuration(long) | 动画持续时间,毫秒为单位 |
android:fillAfter | setFillAfter(boolean) | 控件动画结束时是否保持动画最后的状态 |
android:fillBefore | setDuration(long) | 动画持续时间,毫秒为单位 |
android:duration | setFillBefore(boolean) | 控件动画结束时是否还原到开始动画前的状态 |
android:ShareInterpolator | setInterpolator(Interpolator) | 设定插值器(指定的动画效果,譬如回弹等) |
android:startOffset | setStartOffset(long) | 调用start函数以后等待开始运行的时间,单位为毫秒 |
上面的表格中涉及到 插值器 。那么什么是插值器呢?官方的注释解释道:插值器定义了动画的变化率。这容许基本的动画效果(透明,缩放,平移,旋转)被加速,减速,重复等。 app
xml | java | 做用 |
---|---|---|
AccelerateDecelerateInterpolator | @android:anim/accelerate_decelerate_interpolator | 其变化开始和结束速率较慢,中间加速 |
AccelerateInterpolator | @android:anim/accelerate_interpolator | 其变化开始速率较慢,后面加速 |
DecelerateInterpolator | @android:anim/decelerate_interpolator | 其变化开始速率较快,后面减速 |
LinearInterpolator | @android:anim/linear_interpolator | 其变化速率恒定 |
AnticipateInterpolator | @android:anim/anticipate_interpolator | 其变化开始向后甩,而后向前 |
AnticipateOvershootInterpolator | @android:anim/anticipate_overshoot_interpolator | 其变化开始向后甩,而后向前甩,过冲到目标值,最后又回到了终值 |
OvershootInterpolator | @android:anim/overshoot_interpolator | 其变化开始向前甩,过冲到目标值,最后又回到了终值 |
BounceInterpolator | @android:anim/bounce_interpolator | 其变化在结束时反弹 |
CycleInterpolator | @android:anim/cycle_interpolator | 循环播放,其速率为正弦曲线 |
TimeInterpolator | 一个接口,能够自定义插值器 |
平移的动画效果用到了 TranslateAnimation 类,TranslateAnimation属性以下:ide
xml属性 | java方法 | 做用 |
---|---|---|
android:fromXDelta | TranslateAnimation(float fromXDelta, float toXDelta, float fromYDelta, float toYDelta) | 起始点X轴坐标,数值,百分比,百分比p |
android:toXDelta | TranslateAnimation(float fromXDelta, float toXDelta, float fromYDelta, float toYDelta) | 结束点X轴坐标,数值,百分比,百分比p |
android:fromYDelta | TranslateAnimation(float fromXDelta, float toXDelta, float fromYDelta, float toYDelta) | 起始点Y轴坐标,这里能够是数值,百分比,百分比p |
android:toYDelta | TranslateAnimation(float fromXDelta, float toXDelta, float fromYDelta, float toYDelta) | 结束点Y轴坐标,这里能够是数值,百分比,百分比p |
数值、百分数、百分数p,譬如50表示以当前View左上角坐标加50px为初始点、50%表示以当前View的左上角加上当前View宽高的50%作为初始点、50%p表示以当前View的左上角加上父控件宽高的50%作为初始点。若是不太明白,能够本身动手试试,看看效果。函数
首先要建立XML文件,咱们须要在res下新建anim文件夹,接着在anim下建立animation resource file的xml文件,而后新建一个 translate_anim.xml 布局
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android" android:duration="2000">
<translate android:fromXDelta="0" android:toXDelta="0" android:fromYDelta="0" android:toYDelta="800"/>
</set>
复制代码
接着咱们打开 activity_main.xml 文件,添加 Button 和 TextView。 Button 用来开启动画效果, TextView 用来显示动画效果。动画
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center_horizontal" android:orientation="vertical" tools:context=".MainActivity">
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_margin="10dp" android:gravity="center" android:orientation="horizontal">
<Button android:id="@+id/btnTranslation" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="向下平移" />
</LinearLayout>
<TextView android:id="@+id/tvAnimation" android:layout_width="200dp" android:layout_height="50dp" android:background="#2196F3" android:gravity="center" android:text="动画效果" />
</LinearLayout>
复制代码
布局设置好以后咱们再到 MainActivity.java 文件中给 btnTranslation 添加点击事件,实现向下平移的效果。
这里我推荐你们用 ButterKnife 经过注解的方式绑定 View 就不须要 findByid() 了,配合插件(butterknife-zelezny)控件的绑定和点击事件代码一键生成,提升你的工做效率,美滋滋。
/** * @author Leung * @date 2020/4/16 16:52 */
public class MainActivity extends AppCompatActivity {
@BindView(R.id.tvAnimation)
TextView tvAnimation;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
}
@OnClick(R.id.btnTranslation)
public void onViewClicked(View view) {
// 方法一:经过xml方式实现动画效果
Animation animation = null;
switch (view.getId()) {
case R.id.btnTranslation:
animation = AnimationUtils.loadAnimation(this, R.anim.translate_anim);
break;
default:
}
tvAnimation.startAnimation(animation);
// // 方法二:经过代码动态的实现动画效果
// TranslateAnimation translateAnimation = new TranslateAnimation(0, 0, 0, 800);
// translateAnimation.setDuration(2000);
// tvAnimation.startAnimation(translateAnimation);
}
}
复制代码
动画效果:
缩放的动画效果用到了 ScaleAnimation 类,ScaleAnimation属性以下:
xml属性 | java方法 | 做用 |
---|---|---|
android:fromXScale | ScaleAnimation(float fromX, float toX, float fromY, float toY, float pivotX, float pivotY) | X轴开始的缩放比例,1.0表示无变化 |
android:toXScale | ScaleAnimation(float fromX, float toX, float fromY, float toY, float pivotX, float pivotY) | X轴结束的缩放比例 |
android:fromYScale | ScaleAnimation(float fromX, float toX, float fromY, float toY, float pivotX, float pivotY) | Y轴开始的缩放比例,1.0表示无变化 |
android:toYScale | ScaleAnimation(float fromX, float toX, float fromY, float toY, float pivotX, float pivotY) | Y轴结束的缩放比例 |
android:pivotX | ScaleAnimation(float fromX, float toX, float fromY, float toY, float pivotX, float pivotY) | X轴的缩放起始点(当对象改变大小时,这个点保持不变)==这里能够是数值,百分比,百分比p== |
android:pivotY | ScaleAnimation(float fromX, float toX, float fromY, float toY, float pivotX, float pivotY) | Y轴的缩放起始点(当对象改变大小时,这个点保持不变)==这里能够是数值,百分比,百分比p== |
数值、百分数、百分数p,譬如50表示以当前View左上角坐标加50px为初始点、50%表示以当前View的左上角加上当前View宽高的50%作为初始点、50%p表示以当前View的左上角加上父控件宽高的50%作为初始点。若是不太明白,能够本身动手试试,看看效果。
缩放动画效果的建立也是同样,这里我就不详细说明了,建立 scale_anim.xml 和 修改 activity_main.xml、MainActivity.java ,代码以下:
scale_anim.xml 文件
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android" android:duration="2000">
<scale android:fromXScale="1.0" android:toXScale="2.0" android:fromYScale="1.0" android:toYScale="2.0" android:pivotX="50%" android:pivotY="50%"/>
</set>
复制代码
activity_main.xml 文件
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center_horizontal" android:orientation="vertical" tools:context=".MainActivity">
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_margin="10dp" android:gravity="center" android:orientation="horizontal">
<Button android:id="@+id/btnTranslation" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="向下平移" />
<Button android:id="@+id/btnScale" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="缩放动画" />
</LinearLayout>
<TextView android:id="@+id/tvAnimation" android:layout_width="200dp" android:layout_height="50dp" android:background="#2196F3" android:gravity="center" android:text="动画效果" />
</LinearLayout>
复制代码
MainActivity.java 文件
/** * @author Leung * @date 2020/4/16 16:52 */
public class MainActivity extends AppCompatActivity {
@BindView(R.id.tvAnimation)
TextView tvAnimation;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
}
@OnClick({R.id.btnTranslation, R.id.btnScale})
public void onViewClicked(View view) {
// 方法一:经过xml方式实现动画效果
Animation animation = null;
switch (view.getId()) {
case R.id.btnTranslation:
animation = AnimationUtils.loadAnimation(this, R.anim.translate_anim);
break;
case R.id.btnScale:
animation = AnimationUtils.loadAnimation(this, R.anim.scale_anim);
default:
}
tvAnimation.startAnimation(animation);
// 方法二:经过代码动态的实现动画效果
// TranslateAnimation translateAnimation = new TranslateAnimation(0, 0, 0, 800);
// translateAnimation.setDuration(2000);
// tvAnimation.startAnimation(translateAnimation);
// ScaleAnimation scaleAnimation = new ScaleAnimation(1.0f, 2.0f, 1.0f, 2.0f, tvAnimation.getWidth() / 2, tvAnimation.getHeight() / 2);
// scaleAnimation.setDuration(2000);
// tvAnimation.startAnimation(scaleAnimation);
}
}
复制代码
动画效果:
旋转的动画效果用到了 RotateAnimation 类,RotateAnimation属性以下:
xml属性 | java方法 | 做用 |
---|---|---|
android:fromDegrees | RotateAnimation(float fromDegrees, float toDegrees, float pivotX, float pivotY) | 动画开始时的角度,正数表明顺时针,负数表明逆时针 |
android:toDegrees | RotateAnimation(float fromDegrees, float toDegrees, float pivotX, float pivotY) | 动画结束时的角度,正数表明顺时针,负数表明逆时针 |
android:pivotX | RotateAnimation(float fromDegrees, float toDegrees, float pivotX, float pivotY) | X轴的旋转起始点,==这里能够是数值,百分比,百分比p== |
android:pivotY | RotateAnimation(float fromDegrees, float toDegrees, float pivotX, float pivotY) | Y轴的旋转起始点,==这里能够是数值,百分比,百分比p== |
rotate_anim.xml 文件
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android" android:duration="2000">
<rotate android:fromDegrees="0" android:toDegrees="360" android:pivotX="50%" android:pivotY="50%"/>
</set>
复制代码
activity_main.xml 文件
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center_horizontal" android:orientation="vertical" tools:context=".MainActivity">
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_margin="10dp" android:gravity="center" android:orientation="horizontal">
<Button android:id="@+id/btnTranslation" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="向下平移" />
<Button android:id="@+id/btnScale" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="缩放动画" />
<Button android:id="@+id/btnRotate" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="旋转动画" />
</LinearLayout>
<TextView android:id="@+id/tvAnimation" android:layout_width="200dp" android:layout_height="50dp" android:background="#2196F3" android:gravity="center" android:text="动画效果" />
</LinearLayout>
复制代码
MainActivity.java 文件
@OnClick({R.id.btnTranslation, R.id.btnScale, R.id.btnRotate})
public void onViewClicked(View view) {
// 方法一:经过xml方式实现动画效果
Animation animation = null;
switch (view.getId()) {
case R.id.btnTranslation:
animation = AnimationUtils.loadAnimation(this, R.anim.translate_anim);
break;
case R.id.btnScale:
animation = AnimationUtils.loadAnimation(this, R.anim.scale_anim);
break;
case R.id.btnRotate:
animation = AnimationUtils.loadAnimation(this, R.anim.rotate_anim);
break;
default:
}
tvAnimation.startAnimation(animation);
// 方法二:经过代码动态的实现动画效果
// TranslateAnimation translateAnimation = new TranslateAnimation(0, 0, 0, 800);
// translateAnimation.setDuration(2000);
// tvAnimation.startAnimation(translateAnimation);
// ScaleAnimation scaleAnimation = new ScaleAnimation(1.0f, 2.0f, 1.0f, 2.0f, tvAnimation.getWidth() / 2, tvAnimation.getHeight() / 2);
// scaleAnimation.setDuration(2000);
// tvAnimation.startAnimation(scaleAnimation);
// RotateAnimation rotateAnimation = new RotateAnimation(0, 360, tvAnimation.getWidth() / 2, tvAnimation.getHeight() / 2);
// rotateAnimation.setDuration(2000);
// tvAnimation.startAnimation(rotateAnimation);
}
复制代码
动画效果:
透明的动画效果用到了 AlphaAnimation 类,AlphaAnimation属性以下:
xml属性 | java方法 | 做用 |
---|---|---|
android:fromAlpha | AlphaAnimation(float fromAlpha, float toAlpha) | 动画开始时的透明度(范围:0.0-1.0,1.0不透明,0.0全透明) |
android:toAlpha | AlphaAnimation(float fromAlpha, float toAlpha) | 动画结束时的透明度(范围:0.0-1.0,1.0不透明,0.0全透明) |
alpha_anim.xml 文件
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android" android:duration="2000">
<alpha android:fromAlpha="1" android:toAlpha="0"/>
</set>
复制代码
activity_main.xml 文件
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center_horizontal" android:orientation="vertical" tools:context=".MainActivity">
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_margin="10dp" android:gravity="center" android:orientation="horizontal">
<Button android:id="@+id/btnTranslation" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="向下平移" />
<Button android:id="@+id/btnScale" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="缩放动画" />
<Button android:id="@+id/btnRotate" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="旋转动画" />
<Button android:id="@+id/btnAlpha" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="透明动画" />
</LinearLayout>
<TextView android:id="@+id/tvAnimation" android:layout_width="200dp" android:layout_height="50dp" android:background="#2196F3" android:gravity="center" android:text="动画效果" />
</LinearLayout>
复制代码
MainActivity.java 文件
@OnClick({R.id.btnTranslation, R.id.btnScale, R.id.btnRotate, R.id.btnAlpha})
public void onViewClicked(View view) {
// 方法一:经过xml方式实现动画效果
Animation animation = null;
switch (view.getId()) {
case R.id.btnTranslation:
animation = AnimationUtils.loadAnimation(this, R.anim.translate_anim);
break;
case R.id.btnScale:
animation = AnimationUtils.loadAnimation(this, R.anim.scale_anim);
break;
case R.id.btnRotate:
animation = AnimationUtils.loadAnimation(this, R.anim.rotate_anim);
break;
case R.id.btnAlpha:
animation = AnimationUtils.loadAnimation(this, R.anim.alpha_anim);
break;
default:
}
tvAnimation.startAnimation(animation);
// 方法二:经过代码动态的实现动画效果
// TranslateAnimation translateAnimation = new TranslateAnimation(0, 0, 0, 800);
// translateAnimation.setDuration(2000);
// tvAnimation.startAnimation(translateAnimation);
// ScaleAnimation scaleAnimation = new ScaleAnimation(1.0f, 2.0f, 1.0f, 2.0f, tvAnimation.getWidth() / 2, tvAnimation.getHeight() / 2);
// scaleAnimation.setDuration(2000);
// tvAnimation.startAnimation(scaleAnimation);
// RotateAnimation rotateAnimation = new RotateAnimation(0, 360, tvAnimation.getWidth() / 2, tvAnimation.getHeight() / 2);
// rotateAnimation.setDuration(2000);
// tvAnimation.startAnimation(rotateAnimation);
// AlphaAnimation alphaAnimation = new AlphaAnimation(1.0f, 0.0f);
// alphaAnimation.setDuration(2000);
// tvAnimation.setAnimation(alphaAnimation);
}
复制代码
动画效果:
相信你们对4个动画效果的使用有初步的了解了,那么如今你可能会问:若是我想把4是个动画效果集成在一块儿呢?应该如何实现? 若是你有细心观察的话,会发现咱们在建立完一个 animation resource file的xml文件以后打开会发现有个 < set >标签,表示动画集合,对应 AnimationSet类,有多个动画构成。
AnimationSet类继承自Animation类,是上面四种的组合容器管理类,没有本身特有的属性,他的属性继承自Animation类。当咱们对set标签使用Animation类的属性时会对该标签下的全部子控件都产生影响。譬如咱们在set标签下加入duration=“1000”,子控件的duration属性会失效。
那么咱们如今把刚刚学到的4个动画效果集成在一块儿,看看会是什么样子的。
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android" android:duration="4000" android:shareInterpolator="@android:anim/decelerate_interpolator">
<!-- 向下平移 -->
<translate android:fromXDelta="0" android:toXDelta="0" android:fromYDelta="0" android:toYDelta="800"/>
<!-- 缩小控件 -->
<scale android:fromXScale="1.0" android:toXScale="0.0" android:fromYScale="1.0" android:toYScale="0.0" android:pivotX="50%" android:pivotY="50%"/>
<!-- 旋转360度 -->
<rotate android:fromDegrees="0" android:toDegrees="360" android:pivotX="50%" android:pivotY="50%"/>
<!-- 逐渐透明 -->
<alpha android:fromAlpha="1" android:toAlpha="0"/>
</set>
复制代码
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout...>
<LinearLayout...>
<Button android:id="@+id/btnSet" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="动画集合"/>
<TextView android:id="@+id/tvAnimation" android:layout_width="200dp" android:layout_height="50dp" android:background="#2196F3" android:gravity="center" android:text="动画效果" />
</LinearLayout>
复制代码
@OnClick({R.id.btnTranslation, R.id.btnScale, R.id.btnRotate, R.id.btnAlpha, R.id.btnSet})
public void onViewClicked(View view) {
// 方法一:经过xml方式实现动画效果
Animation animation = null;
switch (view.getId()) {
case R.id.btnTranslation:
animation = AnimationUtils.loadAnimation(this, R.anim.translate_anim);
break;
case R.id.btnScale:
animation = AnimationUtils.loadAnimation(this, R.anim.scale_anim);
break;
case R.id.btnRotate:
animation = AnimationUtils.loadAnimation(this, R.anim.rotate_anim);
break;
case R.id.btnAlpha:
animation = AnimationUtils.loadAnimation(this, R.anim.alpha_anim);
break;
case R.id.btnSet:
animation = AnimationUtils.loadAnimation(this, R.anim.set_anim);
default:
}
tvAnimation.startAnimation(animation);
// 方法二:经过代码动态的实现动画效果
// TranslateAnimation translateAnimation = new TranslateAnimation(0, 0, 0, 800);
// ScaleAnimation scaleAnimation = new ScaleAnimation(1.0f, 0.0f, 1.0f, 0.0f, tvAnimation.getWidth() / 2, tvAnimation.getHeight() / 2);
// RotateAnimation rotateAnimation = new RotateAnimation(0, 360, tvAnimation.getWidth() / 2, tvAnimation.getHeight() / 2);
// AlphaAnimation alphaAnimation = new AlphaAnimation(1.0f, 0.0f);
// AnimationSet animationSet = new AnimationSet(true);
// animationSet.setDuration(4000);
// animationSet.setInterpolator(new DecelerateInterpolator());
// animationSet.addAnimation(translateAnimation);
// animationSet.addAnimation(scaleAnimation);
// animationSet.addAnimation(rotateAnimation);
// animationSet.addAnimation(alphaAnimation);
// tvAnimation.startAnimation(animationSet);
}
复制代码
系统提供的4种动画效果已经能够知足绝大部分的动画效果了,但有特殊要求,咱们也是能够自定义View动画效果的。那么如何自定义View动画效果呢?咱们从前面的Animation类的介绍了解到,它是一个抽象类,咱们只须要继承它,并覆写它的initialize和applyTransformation方法。在initialize中作初始化工做,在applyTransformation中作相应的矩阵变换(须要用到Camera来简化矩阵的变化过程),须要用到数学知识。这里参考了 Android 的 apidemo 中自定义View动画 Rotate3dAnimation。Rotate3dAnimation 能够围绕Y轴旋转而且同时沿着Z轴平移从而实现一种相似3D的效果。具体的代码和使用能够参考这篇文章:翻牌(翻转)动画-Rotate3dAnimation的应用
在动画效果执行完成以后,并无改变View的位置。如,我左上角有一个Button,我把往下平移,并保持动画结束后的状态,此时你再点击左上角Button位置时,依旧能响应Button的点击事件。而你点击平移后的Button是没有任何反应的。
在进行动画的时候,尽可能使用dp,由于px会致使适配问题。
项目代码下载地址:码云