Android 动画详解
Android动画的分类与使用
学习Android必不可少的就是动画的使用了,在Android版本迭代的过程中,出现了很多动画框架,这里做一个总结。
Android动画类型分类
逐帧动画【Frame Animation】,即顺序播放事先准备的图片。
补间动画【Tween Animation】,View的动画效果可以实现简单的平移、缩放、旋转。
属性动画【Property Animation】,补间动画增强版,支持对对象执行动画。
过渡动画【Transition Animation】,实现Activity或View过渡动画效果。包括5.0之后的MD过渡动画等。
动画的分类与版本
Android动画实现方式分类都可以分为xml定义和java定义。
Android 3.0之前版本,逐帧动画,补间动画 Android 3.0之后版本,属性动画 Android 4.4中,过渡动画 Android 5.0以上 MD的动画效果。
下面一起看看简单的实现吧。
逐帧动画
推荐使用一些小图片,它的性能不是很好,如果使用大图的帧动画,会出现性能问题导致卡顿。
比较常用的方式,在res/drawable目录下新建动画XML文件:
设置或清除动画代码:
//开始动画
mIvRefreshIcon.setImageResource(R.drawable.anim_loading);
mAnimationDrawable = (AnimationDrawable) mIvRefreshIcon.getDrawable();
mAnimationDrawable.start();
//停止动画
mIvRefreshIcon.clearAnimation();
if (mAnimationDrawable != null){
mAnimationDrawable.stop();
}
设置Background和设置ImageResource是一样的效果:
ImageView voiceIcon = new ImageView(CommUtils.getContext());
voiceIcon.setBackgroundResource(message.isSelf() ? R.drawable.right_voice : R.drawable.left_voice);
final AnimationDrawable frameAnim = (AnimationDrawable) voiceIcon.getBackground();
frameAnimatio.start();
MediaUtil.getInstance().setEventListener(new MediaUtil.EventListener() {
@Override
public void onStop() {
frameAnimatio.stop();
frameAnimatio.selectDrawable(0);
}
});
补间动画
一句话说明补间动画:只能给View加,不能给对象加,并且不会改变对象的真实属性。
无需关注每一帧,只需要定义动画开始与结束两个关键帧,并指定动画变化的时间与方式等 。主要有四种基本的效果。
透明度变化
大小缩放变化
位移变化
旋转变化
可以在xml中定义,也可以在代码中定义!
透明度的定义:
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android" >
<alpha
android:duration="1000"
android:fromAlpha="0.0"
android:toAlpha="1.0" />
</set>
缩放的定义:
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android" >
<scale
android:duration="1000"
android:fillAfter="false"
android:fromXScale="0.0"
android:fromYScale="0.0"
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
android:pivotX="50%"
android:pivotY="50%"
android:toXScale="1.4"
android:toYScale="1.4" />
</set>
平移的定义:
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android" >
<translate
android:duration="2000"
android:fromXDelta="30"
android:fromYDelta="30"
android:toXDelta="-80"
android:toYDelta="300" />
</set>
旋转的定义:
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android" >
<rotate
android:duration="3000"
android:fromDegrees="0"
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
android:pivotX="50%"
android:pivotY="50%"
android:toDegrees="+350" />
</set>
Java代码中使用补间动画(推荐):
透明度定义:
AlphaAnimation alpha = new AlphaAnimation(0, 1);
alpha.setDuration(500); //设置持续时间
alpha.setFillAfter(true); //动画结束后保留结束状态
alpha.setInterpolator(new AccelerateInterpolator()); //添加差值器
ivImage.setAnimation(alpha);
缩放定义:
ScaleAnimation scale = new ScaleAnimation(1.0f, scaleXY, 1.0f, scaleXY, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
scale.setDuration(durationMillis);
scale.setFillAfter(true);
ivImage.setAnimation(scale);
平移定义:
TranslateAnimation translate = new TranslateAnimation(fromXDelta, toXDelta, fromYDelta, toYDelta);
translate.setDuration(durationMillis);
translate.setFillAfter(true);
ivImage.setAnimation(translate);
RotateAnimation rotate = new RotateAnimation(fromDegrees, toDegrees, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
rotate.setDuration(durationMillis);
rotate.setFillAfter(true);
ivImage.setAnimation(rotate);
组合Set的定义:
RelativeLayout rlRoot = (RelativeLayout) findViewById(R.id.rl_root);
// 旋转动画
RotateAnimation animRotate = new RotateAnimation(0, 360,
Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF,
0.5f);
animRotate.setDuration(1000);// 动画时间
animRotate.setFillAfter(true);// 保持动画结束状态
// 缩放动画
ScaleAnimation animScale = new ScaleAnimation(0, 1, 0, 1,
Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF,0.5f);
animScale.setDuration(1000);
animScale.setFillAfter(true);// 保持动画结束状态
// 渐变动画
AlphaAnimation animAlpha = new AlphaAnimation(0, 1);
animAlpha.setDuration(2000);// 动画时间
animAlpha.setFillAfter(true);// 保持动画结束状态
// 动画集合
AnimationSet set = new AnimationSet(true);
set.addAnimation(animRotate);
set.addAnimation(animScale);
set.addAnimation(animAlpha);
// 启动动画
rlRoot.startAnimation(set);
set.setAnimationListener(new AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationRepeat(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
// 动画结束,跳转页面
// 如果是第一次进入, 跳新手引导
// 否则跳主页面
boolean isFirstEnter = PrefUtils.getBoolean(
SplashActivity.this, "is_first_enter", true);
Intent intent;
if (isFirstEnter) {
// 新手引导
intent = new Intent(getApplicationContext(),
GuideActivity.class);
} else {
// 主页面
intent = new Intent(getApplicationContext(),MainActivity.class);
}
startActivity(intent);
finish();
}
});
属性动画
补间动画增强版本。补充补间动画的一些缺点。
作用对象:任意 Java 对象,不再局限于 视图View对象。
实现的动画效果:可自定义各种动画效果,不再局限于4种基本变换:平移、旋转、缩放 & 透明度。
分为ObjectAnimator和ValueAnimator。
3.1 一个简单的属性动画
先用xml的方式实现:
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<animator
android:valueFrom="0"
android:valueTo="100"
android:valueType="intType"
android:duration="3000"
android:startOffset ="1000"
android:fillBefore = "true"
android:fillAfter = "false"
android:fillEnabled= "true"
android:repeatMode= "restart"
android:repeatCount = "0"
android:interpolator="@android:anim/accelerate_interpolator"/>
</set>
使用:
Button b3 = (Button) findViewById(R.id.b3);
Animator mAnim = AnimatorInflater.loadAnimator(this, R.animator.animator_1_0);
mAnim.setTarget(b3);
mAnim.start();
当然我们可以直接使用Java代码实现:
public static ObjectAnimator setObjectAnimator(View view , String type , int start , int end , long time){
ObjectAnimator mAnimator = ObjectAnimator.ofFloat(view, type, start, end);
// 设置动画重复播放次数 = 重放次数+1
// 动画播放次数 = infinite时,动画无限重复
mAnimator.setRepeatCount(ValueAnimator.INFINITE);
// 设置动画运行的时长
mAnimator.setDuration(time);
// 设置动画延迟播放时间
mAnimator.setStartDelay(0);
// 设置重复播放动画模式
mAnimator.setRepeatMode(ValueAnimator.RESTART);
// ValueAnimator.RESTART(默认):正序重放
// ValueAnimator.REVERSE:倒序回放
//设置差值器
mAnimator.setInterpolator(new LinearInterpolator());
return mAnimator;
}
3.2 ValueAnimator与ObjectAnimator区别:
• ValueAnimator 类是先改变值,然后手动赋值 给对象的属性从而实现动画;是间接对对象属性进行操作;
• ObjectAnimator 类是先改变值,然后自动赋值 给对象的属性从而实现动画;是直接对对象属性进行操作;
//不同的定义方式
ValueAnimator animator = null;
if (isOpen) {
//要关闭
if (longHeight > shortHeight) {
isOpen = false;
animator = ValueAnimator.ofInt(longHeight, shortHeight);
}
} else {
//要打开
if (longHeight > shortHeight) {
isOpen = true;
animator = ValueAnimator.ofInt(shortHeight, longHeight);
}
}
animator.start();
//不同的定义方式
ObjectAnimator animatorX = ObjectAnimator.ofFloat(mSplashImage, "scaleX", 1f, 2f);
animatorX.start();
3.3 监听动画的方式:
mAnim2.addListener(new AnimatorListenerAdapter() {
// 向addListener()方法中传入适配器对象AnimatorListenerAdapter()
// 由于AnimatorListenerAdapter中已经实现好每个接口
// 所以这里不实现全部方法也不会报错
@Override
public void onAnimationCancel(Animator animation) {
super.onAnimationCancel(animation);
ToastUtils.showShort("动画结束了");
}
});
3.4 组合动画AnimatorSet:
xml的组合
<?xml version="1.0" encoding="utf-8"?> <set xmlns:android="http://schemas.android.com/apk/res/android"
android:ordering="sequentially" >
<!--表示Set集合内的动画按顺序进行-->
<!--ordering的属性值:sequentially & together-->
<!--sequentially:表示set中的动画,按照先后顺序逐步进行(a 完成之后进行 b )-->
<!--together:表示set中的动画,在同一时间同时进行,为默认值-->
<set android:ordering="together" >
<!--下面的动画同时进行-->
<objectAnimator
android:duration="2000"
android:propertyName="translationX"
android:valueFrom="0"
android:valueTo="300"
android:valueType="floatType" >
</objectAnimator>
<objectAnimator
android:duration="3000"
android:propertyName="rotation"
android:valueFrom="0"
android:valueTo="360"
android:valueType="floatType" >
</objectAnimator>
</set>
<set android:ordering="sequentially" >
<!--下面的动画按序进行-->
<objectAnimator
android:duration="1500"
android:propertyName="alpha"
android:valueFrom="1"
android:valueTo="0"
android:valueType="floatType" >
</objectAnimator>
<objectAnimator
android:duration="1500"
android:propertyName="alpha"
android:valueFrom="0"
android:valueTo="1"
android:valueType="floatType" >
</objectAnimator>
</set>
</set>
Java方式的组合
ObjectAnimator translation = ObjectAnimator.ofFloat(mButton, "translationX", curTranslationX, 300,curTranslationX); // 平移动画
ObjectAnimator rotate = ObjectAnimator.ofFloat(mButton, "rotation", 0f, 360f); // 旋转动画
ObjectAnimator alpha = ObjectAnimator.ofFloat(mButton, "alpha", 1f, 0f, 1f); // 透明度动画 // 创建组合动画的对象
AnimatorSet animSet = new AnimatorSet(); // 根据需求组合动画
animSet.play(translation).with(rotate).before(alpha);
animSet.setDuration(5000); //启动动画
animSet.start();
常用的组合方法
• AnimatorSet.play(Animator anim) :播放当前动画。
• AnimatorSet.after(long delay) :将现有动画延迟x毫秒后执行。
• AnimatorSet.with(Animator anim) :将现有动画和传入的动画同时执行。
• AnimatorSet.after(Animator anim) :将现有动画插入到传入的动画之后执行。
• AnimatorSet.before(Animator anim) :将现有动画插入到传入的动画之前执行。
3.5 Evaluator估值器
表示计算某个时间点,动画需要更新 view 的值。
Evaluator.evaluate(float fraction, T startValue, T endValue) 是核心方法。其中,fraction 表示一个百分比。startValue 和 endValue 表示动画的起始值和结束值。通过 fraction、startValue、endValue 计算 view 对应的属性位置。
常用的就那么几个:
ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(animationView, "X", 0, 500);
objectAnimator.setInterpolator(new LinearInterpolator());
objectAnimator.setEvaluator(new FloatEvaluator());
objectAnimator.setDuration(5 * 1000);
objectAnimator.start();
3.6 简单Demo
实现开始隐藏在屏幕顶部,已动画的形式慢慢返回:
text.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
@Override
public void onGlobalLayout() {
text.getViewTreeObserver().removeOnGlobalLayoutListener(this);
textHeight = text.getHeight();
Log.e("tag", "textHeight: "+textHeight);
//一开始需要先让text往上移动它自身的高度
ViewHelper.setTranslationY(text, -textHeight);
Log.e("tag", "top:"+text.getTop());
//再以动画的形式慢慢滚动下拉
text.animate(text).translationYBy(textHeight)
.setDuration(500)
.setStartDelay(1000)
.start();
属性动画设置控件的高度,实现动画关闭和打开的效果:
private boolean isOpen = false;
/**
* 状态的开关。上下关闭的属性动画
*/
private void toggle() {
ValueAnimator animator = null;
if (isOpen) {
isOpen = false;
//开启属性动画
animator = ValueAnimator.ofInt(mDesHeight, 0);
} else {
isOpen = true;
animator = ValueAnimator.ofInt(0, mDesHeight);
}
//动画的过程监听
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
Integer height = (Integer) valueAnimator.getAnimatedValue();
mParams.height = height;
llDesRoot.setLayoutParams(mParams);
}
});
//设置动画的状态监听。给小箭头设置状态
animator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animator) {
}
@Override
public void onAnimationEnd(Animator animator) {
//结束的时候,更换小箭头的图片
if (isOpen){
ivArrow.setImageResource(R.drawable.arrow_up);
}else {
ivArrow.setImageResource(R.drawable.arrow_down);
}
}
@Override
public void onAnimationCancel(Animator animator) {
}
@Override
public void onAnimationRepeat(Animator animator) {
}
});
animator.setDuration(200); //动画时间
animator.start(); //启动
}
属性动画讲的好乱,太多了,比较复杂。后面会有更详细的代码!
过渡动画
4.1 Android5.0以前的过渡动画
同样可以在xml中定义 ,也可以使用java代码控制。
我们在style文件夹中定义。
<!--左右进出场的activity动画-->
<style name="My_AnimationActivity" mce_bogus="1" parent="@android:style/Animation.Activity">
<item name="android:activityOpenEnterAnimation">@anim/open_enter</item>
<item name="android:activityCloseExitAnimation">@anim/close_exit</item>
</style>
<!--上下进出场的activity动画-->
<style name="up_down_activity_anim" mce_bogus="1" parent="@android:style/Animation.Activity">
<item name="android:activityOpenEnterAnimation">@anim/open_up</item>
<item name="android:activityCloseExitAnimation">@anim/close_down</item>
</style>
定义的文件如下,补间动画的方式:
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="270"
android:fromXDelta="100%p"
android:toXDelta="0%p" />
</set>
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="270"
android:fromXDelta="0%p"
android:toXDelta="-100%p" />
</set>
对应的Activity实现指定的样式即可实现。
在Java文件中同样可以通过 overridePendingTransition 来实现。
大致实现如下:
startActivity(intent);
overridePendingTransition(R.anim.bottom_top_anim, R.anim.alpha_hide);
finish();
overridePendingTransition(R.anim.alpha_show, R.anim.top_bottom_anim);
4.2 Android5.0以后的过渡动画
5.0之后,Android就自带几种动画特效。3种转场动画 ,1种共享元素。
三种转场动画如下:
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public void explode(View view) {
intent = new Intent(this, TransitionActivity.class);
intent.putExtra("flag", 0);
startActivity(intent, ActivityOptions.makeSceneTransitionAnimation(this).toBundle());
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public void slide(View view) {
intent = new Intent(this, TransitionActivity.class);
intent.putExtra("flag", 1);
startActivity(intent, ActivityOptions.makeSceneTransitionAnimation(this).toBundle());
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public void fade(View view) {
intent = new Intent(this, TransitionActivity.class);
intent.putExtra("flag", 2);
startActivity(intent, ActivityOptions.makeSceneTransitionAnimation(this).toBundle());
}
通过对面的页面来指定实现的方式:
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getWindow().requestFeature(Window.FEATURE_CONTENT_TRANSITIONS);
int flag = getIntent().getExtras().getInt("flag");
switch (flag) {
case 0:
//分解效果 上面的上面消失 下面的下面消失 分解掉了
getWindow().setEnterTransition(new Explode());
break;
case 1:
//滑动效果 默认上下滑动
getWindow().setEnterTransition(new Slide());
break;
case 2:
//淡出效果 透明度
getWindow().setEnterTransition(new Fade());
getWindow().setExitTransition(new Fade());
break;
case 3:
break;
}
setContentView(R.layout.activity_transition);
}
5.0的Share共享动画:
跳转的方法:
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public void share(View view) {
View fab = findViewById(R.id.fab_button);
intent = new Intent(this, TransitionActivity.class);
intent.putExtra("flag", 3);
//创建单个共享
// startActivity(intent, ActivityOptions.makeSceneTransitionAnimation(this, view, "share")
// .toBundle());
//创建多个共享
startActivity(intent, ActivityOptions.makeSceneTransitionAnimation(this, Pair.create
(view, "share"),
Pair.create(fab,"fab"))
.toBundle());
}
share的方式,不需要对方页面接收设置过渡动画,而是需要在xml中配置transitionName属性:
<View
android:background="?android:colorPrimary"
android:id="@+id/holder_view"
android:transitionName="share"
android:layout_width="match_parent"
android:layout_height="300dp"/>
那边是一个button 共享名字叫“share” 那边是拿到的view 不是button 转过来定义的是view。
那边共享的是button 共享名字叫tab 共享过来也定义的button。
如果Share动画 想Share一个ViewGroup怎么办?比如一个Item跳转到Detail页面 可以直接使用这种过渡效果。
private void toActivity(View sharedElement) {
Intent intent = new Intent(getContext(), TimeTableAcivity.class);
ActivityOptions options =
ActivityOptions.makeSceneTransitionAnimation(getActivity(), sharedElement, "shared_element_end_root");
startActivity(intent, options.toBundle());
}
@Override
protected void onCreate(Bundle savedInstanceState) {
getWindow().requestFeature(Window.FEATURE_ACTIVITY_TRANSITIONS);
findViewById(android.R.id.content).setTransitionName("shared_element_end_root");
setEnterSharedElementCallback(new MaterialContainerTransformSharedElementCallback());
getWindow().setSharedElementEnterTransition(buildContainerTransform(true));
getWindow().setSharedElementReturnTransition(buildContainerTransform(false));
super.onCreate(savedInstanceState);
}
private MaterialContainerTransform buildContainerTransform(boolean entering) {
MaterialContainerTransform transform = new MaterialContainerTransform(this, entering);
transform.setAllContainerColors(
MaterialColors.getColor(findViewById(android.R.id.content), R.attr.colorSurface));
transform.addTarget(android.R.id.content);
//设置动画持续时间(毫秒)
transform.setDuration(666);
return transform;
}
5.0之后在MD中还有其他的动画,比如揭露动画,不知道算不算转场动画的一种。因为一般也是用于转场的时候使用,但是这个动画我们使用的很少很少。
简单的使用如下:
View myView = findView(R.id.awesome_card);
int cx = (myView.getLeft() + myView.getRight()) / 2;
int cy = (myView.getTop() + myView.getBottom()) / 2;
int dx = Math.max(cx, myView.getWidth() - cx);
int dy = Math.max(cy, myView.getHeight() - cy);
float finalRadius = (float) Math.hypot(dx, dy);
Animator animator =
ViewAnimationUtils.createCircularReveal(myView, cx, cy, 0, finalRadius);
animator.setInterpolator(new AccelerateDecelerateInterpolator());
animator.setDuration(1500);
animator.start();
这些动画虽然牛皮,但是记得5.0以上才生效的哦,同时我们也不能看着什么动画炫酷都想上,转场动画也是在主线程执行的,如果定义不当也会造成卡顿的。
异步动画
在子线程中执行动画?我懂了,看我操作!
Thread {
val animatorscaleX = ObjectAnimator.ofFloat(mBinding.ivAnim, "scaleX", 2f)
val animatorscaleY = ObjectAnimator.ofFloat(mBinding.ivAnim, "scaleY", 2f)
val animatortranslationX = ObjectAnimator.ofFloat(mBinding.ivAnim, "translationX", 200f)
val animatortranslationY = ObjectAnimator.ofFloat(mBinding.ivAnim, "translationY", 200f)
val set = AnimatorSet()
set.setDuration(1000).play(animatorscaleX).with(animatorscaleY).with(animatortranslationX).with(animatortranslationY)
set.start()
}.start()
开个线程,执行属性动画。so easy! 等等,怎么写个属性动画这么多代码,修改一下,优雅一点,同样的效果一行代码解决。
Thread {
mBinding.ivAnim.animate().scaleX(2f).scaleY(2f).translationX(200f).translationY(200f).setDuration(1000).start()
}.start()
运行居然报错?不能运行在没有looper的子线程?哦...我懂了,子线程不能更新UI来着。
到此就引出一个经典面试题,子线程真的不能更新UI吗?当然可以更新UI了。看我操作!
public class MyLooperThread extends Thread {
// 子线程的looper
private Looper myLooper;
// 子线程的handler
private Handler mHandler;
// 用于测试的textview
private TextView testView;
private Activity activity;
public Looper getLooper() {
return myLooper;
}
public Handler getHandler() {
return mHandler;
}
public MyLooperThread(Context context, TextView view) {
this.activity = (Activity) context;
testView = view;
}
@Override
public void run() {
super.run();
// 调用了此方法后,当前线程拥有了一个looper对象
Looper.prepare();
YYLogUtils.w("消息循环开始");
if (myLooper == null) {
while (myLooper == null) {
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 调用此方法获取当前线程的looper对象
myLooper = Looper.myLooper();
}
}
// 当前handler与当前线程的looper关联
mHandler = new Handler(myLooper) {
@Override
public void handleMessage(Message msg) {
YYLogUtils.w("处理消息:" + msg.obj);
//此线程,此Looper创建的ui可以随便修改
addTextViewInChildThread().setText(String.valueOf(msg.obj));
//发现跟ui创建的位置有关。如果ui是在main线程创建的,则在子线程中不可以更改此ui;
// 如果在含有looper的子线程中创建的ui,则可以任意修改
// 这里传进来的是主线程的ui,不能修改!低版本可能可以修改
//CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
// try {
// if (testView != null) {
// testView.setText(String.valueOf(msg.obj));
// }
// } catch (Exception e) {
// e.printStackTrace();
//
// }
}
};
Looper.loop();
YYLogUtils.w("looper消息循环结束,线程终止");
}
/**
* 创建TextView
*/
private TextView addTextViewInChildThread() {
TextView textView = new TextView(activity);
textView.setBackgroundColor(Color.GRAY); //背景灰色
textView.setGravity(Gravity.CENTER); //居中展示
textView.setTextSize(20);
WindowManager windowManager = activity.getWindowManager();
WindowManager.LayoutParams params = new WindowManager.LayoutParams(
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.WRAP_CONTENT,
0, 0,
WindowManager.LayoutParams.FIRST_SUB_WINDOW,
WindowManager.LayoutParams.TYPE_TOAST,
PixelFormat.TRANSPARENT);
windowManager.addView(textView, params);
return textView;
}
}
我们需要定义线程,然后准备Looper,并创建内部的Handler处理数据。我们内部线程创建TextView,我们发送handle消息创建textview并赋值。
val looperThread = MyLooperThread(this, mBinding.tvRMsg)
looperThread.start()
mBinding.ivAnim.click {
looperThread.handler.obtainMessage(200, "test set tv'msg").sendToTarget()
}
正常显示子线程创建的textview,但是我们传入线程对象的tvRMsg是不能在子线程赋值的,会报错:
CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
结论:如果ui是在main线程创建的,则在子线程中不可以更改此ui;如果在含有looper的子线程中创建的ui,则可以任意修改。
既然子线程都可以更新UI了,那么子线程执行动画行不行?当然行!
我们直接修改代码:
val looperThread = MyLooperThread(this, mBinding.tvRMsg)
looperThread.start()
mBinding.ivAnim.click {
//试试子线程执行动画看看
looperThread.handler.post {
mBinding.ivAnim.animate().scaleX(2f).scaleY(2f).translationX(200f).translationY(200f).setDuration(1000).start()
}
}
完美运行!
其实官方早有说明,RenderThread 中运行动画。其实我们上面的Thread类就是仿 HandlerThread 来写的。我们可以使用 HandlerThread 很方便的实现子线程动画。具体的使用方式和我们自定义的 Thread 类似。
我们可以基于系统类 HandlerThread 封装一个异步动画工具类:
class AsynAnimUtil private constructor() : LifecycleObserver {
private var mHandlerThread: HandlerThread? = HandlerThread("anim_run_in_thread")
private var mHandler: Handler? = mHandlerThread?.run {
start()
Handler(this.looper)
}
private var mOwner: LifecycleOwner? = null
private var mAnim: ViewPropertyAnimator? = null
companion object {
val instance: AsynAnimUtil by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
AsynAnimUtil()
}
}
//启动动画
fun startAnim(owner: LifecycleOwner?, animator: ViewPropertyAnimator) {
try {
if (mOwner != owner) {
mOwner = owner
addLoopLifecycleObserver()
}
if (mHandlerThread?.isAlive != true) {
YYLogUtils.w("handlerThread restart")
mHandlerThread = HandlerThread("anim_run_in_thread")
mHandler = mHandlerThread?.run {
start()
Handler(this.looper)
}
}
mHandler?.post {
mAnim = animator.setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator?) {
super.onAnimationEnd(animation)
destory()
}
override fun onAnimationCancel(animation: Animator?) {
super.onAnimationCancel(animation)
destory()
}
override fun onAnimationEnd(animation: Animator?, isReverse: Boolean) {
super.onAnimationEnd(animation, isReverse)
destory()
}
})
mAnim?.start()
}
} catch (e: Exception) {
e.printStackTrace()
}
}
// 绑定当前页面生命周期
private fun addLoopLifecycleObserver() {
mOwner?.lifecycle?.addObserver(this)
}
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun onDestroy() {
YYLogUtils.i("AsynAnimUtil Lifecycle -> onDestroy")
mAnim?.cancel()
destory()
}
private fun destory() {
YYLogUtils.w("handlerThread quit")
try {
mHandlerThread?.quitSafely()
mAnim = null
mOwner = null
mHandler = null
mHandlerThread = null
} catch (e: Exception) {
e.printStackTrace()
}
}
}
使用的时候就可以直接拿工具类来进行异步动画。
mBinding.ivAnim.click {
//试试HandlerThread执行动画
val anim = mBinding.ivAnim.animate()
.scaleX(2f)
.scaleY(2f)
.translationXBy(200f)
.translationYBy(200f)
.setDuration(2000)
AsynAnimUtil.instance.startAnim(this, anim)
}
Ok,完美运行。这里注意需要传入LifecycleOwner 为了在当前页面关闭的时候及时的停止动画释放资源。