Android 动画详解

news2024/9/23 11:24:20

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 为了在当前页面关闭的时候及时的停止动画释放资源。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/377201.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Mysql——双机同步

遇到一个需求&#xff0c;需要两台服务器的上的mysql数据库数据实时同步&#xff0c;包括结构也同步。 利用mysql本身的binlog确实实现了这个效果&#xff0c;但是因为个实际业务场景不满足&#xff0c;所以pass掉了&#xff0c;但是记录一下。 目录一、环境二、配置2.1 创建同…

件测试之易用性测试

一、易用性测试概述我们所说的易用性测试是指软件界面的测试&#xff0c;而对于产品的易用性来说&#xff0c;不仅仅是软件界面&#xff0c;还包括硬件(即产品的外观)&#xff0c;如按钮图标是否易懂、菜单是否易找到等。易用性主要研究3个方向&#xff1a;用户研究、交互设计、…

【使用两个队列实现栈】

文章目录前言使用两个队列实现栈1.队列接口函数引入2.栈的初始化3.向栈中插入元素4.出栈操作5.取出栈顶元素6.判断栈是否为空7.释放内存空间总结前言 本文章主要介绍栈和队列的相互转换。 使用两个队列实现栈 我们知道&#xff0c;栈的特点是后进先出&#xff0c;而队列的特点…

[工具笔记]1.UnityEngine.Plane

public struct Plane : IFormattable{} Plane是存在于 3D 空间中&#xff0c;无限大的平坦表面&#xff0c;将空间划分为两半&#xff08;称为半空间&#xff09;。可方便地确定特定点处于两个半空间的哪一个中&#xff0c;以及确定该点与平面相距多远。 此对象在unity并不可见…

一分钟掌握技术术语:API(接口)

很多产品经理在项目开发过程中经常听到&#xff1a;你调我这个接口就好了&#xff1b;这个功能你写个接口给我&#xff1b;有什么不懂的就看下API接口文档。 开发经常说的接口是什么意思呢&#xff1f;术语解释&#xff1a;API&#xff08;Application Programming Interface&…

【C++】泛型编程——模板初阶

文章目录1. 泛型编程2. 函数模板2.1 函数模板的概念2.2 函数模板的使用2.3 函数模板的原理2.4 函数模板的实例化隐式实例化显式实例化2.5 模板参数的匹配原则3. 类模板1. 泛型编程 首先我们来思考一个问题&#xff1a;如何实现一个通用的交换函数呢&#xff1f; 即我们想交换两…

神经网络中的激活函数

文章目录为什么要使用激活函数常用的激活函数如何选择激活函数ReLU激活函数的优点及局限性为什么Sigmoid和Tanh会导致梯度消失的问题为什么Tanh收敛速度⽐Sigmoid快&#xff1f;为什么要使用激活函数 在真实情况中&#xff0c;我们往往会遇到线性不可分问题&#xff0c;需要非…

汇编指令学习(MOV,MOVSX,MOVZX,LEA,XCHG)

一、MOV指令1、将十六进制0x1234数值&#xff0c;赋值给eax寄存器mov eax,0x12342、将十六进制0x123数值&#xff0c;赋值给内存地址为ebxmov dword [ebx],0x1233、将edx的高八位赋值给eax的低八位ax&#xff0c;eax的低16位&#xff0c;al&#xff0c;eax的低8位&#xff0c;a…

RedisTemplate 的基本使用手把手教

下载实例源码 使用步骤 1、引入 spring-boot-starter-data-redis 依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId> </dependency>2、在 application.yml 配置 R…

一文分析Linux虚拟化KVM-Qemu之virtqueue

说明&#xff1a; KVM版本&#xff1a;5.9.1QEMU版本&#xff1a;5.0.0工具&#xff1a;Source Insight 3.5&#xff0c; Visio 1. 概述 前边系列将Virtio Device和Virtio Driver都已经讲完&#xff0c;本文将分析virtqueue&#xff1b;virtqueue用于前后端之间的数据交换&…

[4.10]-AutoSAR零基础学习-Secure Debug(SHE+)(一)

目录 1 内部调试保护概述 2 UCB confirmation AURIXTM 设备提供多个安全保护层&#xff0c;以限制调试器访问整个微控制器。 保护层的配置基于用户配置块 UCB&#xff0c;存在于DFlash上&#xff08;DF_UCB&#xff09;。UCB 包含保护设置参数和其他可由用户配置的参数。 DF_…

使用Python对excel中的数据进行处理

一、读取excel中的数据首先引入pandas库&#xff0c;没有的话使用控制台安装 —— pip install pandas 。import pandas as pd #引入pandas库&#xff0c;别名为pd#read_excel用于读取excel中的数据&#xff0c;这里只列举常用的两个参数&#xff08;文件所在路径&#xff…

华为OD机试模拟题 用 C++ 实现 - 连续子串(2023.Q1)

最近更新的博客 【华为OD机试模拟题】用 C++ 实现 - 最多获得的短信条数(2023.Q1)) 文章目录 最近更新的博客使用说明连续子串题目输入输出示例一输入输出说明Code使用说明 参加华为od机试,一定要注意不要完全背诵代码,需要理解之后模仿写出,通过率才会高。 华为 OD …

SQL Server开启CDC的完整操作过程

这里写自定义目录标题写在前面SQL Server开启CDC1. 将指定库的实例先开启CDC2. 开启需要开启CDC的表3. 关闭CDC功能更详细信息参照官网写在前面 鉴于老旧数据的结构和项目都在sqlserver上存储&#xff0c;且迁移成本巨大&#xff0c;当下要为sqlserver的存储过程减负。要将一部…

深入理解Spring MVC下

上一篇博客从理论概念上来梳理Spring MVC相关知识&#xff0c;此篇博客将通过spring官网提供showcase代码为例子&#xff0c;详细介绍showcase代码中包含的各个例子是如何实现的。官网的showcase代码包含的主要例子包括&#xff0c;Demo地址&#xff1a;Mapping Requests&#…

2023王道考研数据结构笔记第一章绪论

第一章 绪论 1.1 数据结构的基本概念 1.数据&#xff1a;数据是信息的载体&#xff0c;是描述客观事物属性的数、字符以及所有能输入到计算机中并被程序识别和处理的符号的集合。 2.数据元素&#xff1a;数据元素是数据的基本单位&#xff0c;通常作为一个整体进行考虑和处理…

【LeetCode】2363. 合并相似的物品

2363. 合并相似的物品 题目描述 给你两个二维整数数组 items1 和 items2 &#xff0c;表示两个物品集合。每个数组 items 有以下特质&#xff1a; items[i] [valuei, weighti] 其中 valuei 表示第 i 件物品的 价值 &#xff0c;weighti 表示第 i 件物品的 重量 。items 中每…

cpp之STL

STL原理 STL ⼀共提供六⼤组件&#xff0c;包括容器&#xff0c;算法&#xff0c;迭代器&#xff0c;仿函数&#xff0c;适配器和空间配置器&#xff0c;彼此可以组合套⽤。容器通过配置器取得数据存储空间&#xff0c;算法通过迭代器存取容器内容&#xff0c;仿函数可以协助算…

电子科技大学软件工程期末复习笔记(九):项目管理

目录 前言 重点一览 软件项目管理的四大要素 团队组织方式 虚拟团队 软件范围 项目计划 P-CMM的5个级别 本章小结 前言 本复习笔记基于王玉林老师的课堂PPT与复习大纲&#xff0c;供自己期末复习与学弟学妹参考用。 重点一览 本节是软件工程复习笔记的最后一篇&…

Hive---Zeppelin安装教程

Zeppelin安装教程 安装zeppelin必须基于Hadoop和Hive上 文章目录Zeppelin安装教程简介安装步骤1.上传zeppelin压缩包2.解压并更名3.修改配置文件编辑zeppelin-site.xml---将配置文件的ip地址和端口号进行修改编辑 zeppelin-env.sh---添加JDK和Hadoop环境拷贝hive文件切换目录拷…