Android中的动画全解!

2020-07-23 16:11:57 浏览数 (1)

  • 一、View动画
    • 1.1 xml方式
    • 1.2 代码动态创建
    • 1.3 自定义View动画
    • 1.4 帧动画
    • 1.5 View动画的特殊使用场景
      • 1.5.1 给ViewGroup指定child的出场动画
      • 1.5.2 Activity的切换效果
  • 二、属性动画
    • 2.1 使用方法
    • 2.2对任意属性做动画
    • 2.3 属性动画的原理
  • 三、使用动画的注意事项

Android中动画分为:View动画、帧动画(也属于View动画)、属性动画。 View动画是对View做图形变换(平移、缩放、旋转、透明度)从而产生动画效果。 帧动画就是顺序播放一系列图片来产生动画效果。 属性动画可以动态改变对象的属性来达到动画效果。

一、View动画

View动画的平移、缩放、旋转、透明度 分别对应 Animation的的4个子类:TranslateAnimation、ScaleAnimation、RotateAnimation、AlphaAnimation。View可以用xml定义、也可以用代码创建。推荐使用xml,可读性好。

1.1 xml方式

如下所示,R.anim.animation_test 是xml定义的动画。 其中标签 translate、scale、alpha、rotate,就是对应四种动画。set标签是动画集合,对应AnimationSet类,有多个动画构成。

其中android:duration是指动画时间,fillAfter为true是动画结束后保持,false会回到初始状态。interpolator是指动画的执行速度,默认是先加速后减速。其他标签及属性较简单可自行研究验证。

代码语言:javascript复制
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="5000"
    android:fillAfter="true"
    android:interpolator="@android:anim/accelerate_decelerate_interpolator"> 
    <!--set里面的duration如果有值,会覆盖子标签的duration-->

    <translate
        android:duration="1000"
        android:fromXDelta="0"
        android:toXDelta="400" />
    <scale
        android:duration="2000"
        android:fromXScale="0.5"
        android:fromYScale="0.5"
        android:toXScale="1"
        android:toYScale="1" />
    <alpha
        android:duration="3000"
        android:fromAlpha="0.2"
        android:toAlpha="1" />

    <rotate
        android:fromDegrees="0"
        android:toDegrees="90" />
</set>

定义好动画后,使用也很简单,调用view的startAnimation方法即可。

代码语言:javascript复制
        //view动画使用,方式一:xml,建议使用。
        Animation animation = AnimationUtils.loadAnimation(this, R.anim.animation_test);
        textView1.startAnimation(animation);

1.2 代码动态创建

代码创建举例如下,也很简单。

代码语言:javascript复制
        //view动画使用,方式二:new 动画对象
        AnimationSet animationSet = new AnimationSet(false);
        animationSet.setDuration(3000);
        animationSet.addAnimation(new TranslateAnimation(0, 100, 0, 0));
        animationSet.addAnimation(new ScaleAnimation(0.1f, 1f, 0.1f, 1f));
        animationSet.setFillAfter(true);
        textView2.startAnimation(animationSet);

        //view动画使用,方式二:new 动画对象,使用setAnimation
        AnimationSet animationSet2 = new AnimationSet(false);
        animationSet2.setDuration(3000);
        animationSet2.addAnimation(new TranslateAnimation(0, 100, 0, 0));
        animationSet2.addAnimation(new ScaleAnimation(0.1f, 1f, 0.1f, 1f));
        animationSet2.setFillAfter(true);
        animationSet2.setAnimationListener(new Animation.AnimationListener() {
            @Override
            public void onAnimationStart(Animation animation) {

            }
            @Override
            public void onAnimationEnd(Animation animation) {
                MyToast.showMsg(AnimationTestActivity.this, "View动画:代码 set:View动画结束~");
            }
            @Override
            public void onAnimationRepeat(Animation animation) {

            }
        });
        textView3.setAnimation(animationSet2);

注意点:

  1. startAnimation方法是立刻播放动画;setAnimation是设置要播放的下一个动画。
  2. setAnimationListener可以监听动画的开始、结束、重复。

1.3 自定义View动画

通常我们不需要自定义View动画,上面4种基本够用。

自定义View动画,需要继承Animation,重写initialize和applyTransformation方法。在initialize中做初始化工作,在applyTransformation中做相应的矩阵变换(需要用到Camera),需要用到数学知识。这个给出一个例子Rotate3dAnimation,沿Y轴旋转并沿Z轴平移,到达3d效果。

代码语言:javascript复制
/**
 * 沿Y轴旋转并沿Z轴平移,到达3d效果
 * An animation that rotates the view on the Y axis between two specified angles.
 * This animation also adds a translation on the Z axis (depth) to improve the effect.
 */
public class Rotate3dAnimation extends Animation {
    private final float mFromDegrees;
    private final float mToDegrees;
    private final float mCenterX;
    private final float mCenterY;
    private final float mDepthZ;
    private final boolean mReverse;
    private Camera mCamera;

    /**
     * Creates a new 3D rotation on the Y axis. The rotation is defined by its
     * start angle and its end angle. Both angles are in degrees. The rotation
     * is performed around a center point on the 2D space, definied by a pair
     * of X and Y coordinates, called centerX and centerY. When the animation
     * starts, a translation on the Z axis (depth) is performed. The length
     * of the translation can be specified, as well as whether the translation
     * should be reversed in time.
     *
     * @param fromDegrees the start angle of the 3D rotation 沿y轴旋转开始角度
     * @param toDegrees   the end angle of the 3D rotation 沿y轴旋转结束角度
     * @param centerX     the X center of the 3D rotation 沿y轴旋转的轴点x(相对于自身)
     * @param centerY     the Y center of the 3D rotation 沿y轴旋转的轴点y(相对于自身)
     * @param depthZ      z轴的平移。如果>0,越大就越远离,视觉上变得越小。
     * @param reverse     true if the translation should be reversed, false otherwise
     */
    public Rotate3dAnimation(float fromDegrees, float toDegrees,
                             float centerX, float centerY, float depthZ, boolean reverse) {
        mFromDegrees = fromDegrees;
        mToDegrees = toDegrees;
        mCenterX = centerX;
        mCenterY = centerY;
        mDepthZ = depthZ;
        mReverse = reverse;
    }

    @Override
    public void initialize(int width, int height, int parentWidth, int parentHeight) {
        super.initialize(width, height, parentWidth, parentHeight);
        mCamera = new Camera();
    }

    @Override
    protected void applyTransformation(float interpolatedTime, Transformation t) {
        final float fromDegrees = mFromDegrees;
        float degrees = fromDegrees   ((mToDegrees - fromDegrees) * interpolatedTime);

        final float centerX = mCenterX;
        final float centerY = mCenterY;
        final Camera camera = mCamera;

        final Matrix matrix = t.getMatrix();

        camera.save();
        if (mReverse) {
            camera.translate(0.0f, 0.0f, mDepthZ * interpolatedTime);
        } else {
            camera.translate(0.0f, 0.0f, mDepthZ * (1.0f - interpolatedTime));
        }
        camera.rotateY(degrees);
        camera.getMatrix(matrix);
        camera.restore();

        matrix.preTranslate(-centerX, -centerY);
        matrix.postTranslate(centerX, centerY);
    }
}

1.4 帧动画

帧动画对应AnimationDrawable类,用来顺序播放多张图片。使用很简单,先xml定义一个AnimationDrawable,然后作为背景或资源设置给view并开始动画即可。 举例如下:

代码语言:javascript复制
R.drawable.frame_animation

<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
    android:oneshot="false">
    <item
        android:drawable="@drawable/home_icon_guide00"
        android:duration="50"/>
    <item
        android:drawable="@drawable/home_icon_guide01"
        android:duration="50"/>
    <item
        android:drawable="@drawable/home_icon_guide02"
        android:duration="50"/>
    <item
        android:drawable="@drawable/home_icon_guide03"
        android:duration="50"/>
    ......
</animation-list>
代码语言:javascript复制
        tvFrameAnimation.setBackgroundResource(R.drawable.frame_animation);
        AnimationDrawable frameAnimationBackground = (AnimationDrawable) tvFrameAnimation.getBackground();
        frameAnimationBackground.start();

1.5 View动画的特殊使用场景

1.5.1 给ViewGroup指定child的出场动画

1.先用xml定义标签LayoutAnimation:

  • android:animation设置child的出场动画
  • android:animationOrder设置child的出场顺序,normal就是顺序
  • delay是指:每个child延迟(在android:animation中指定的动画时间)0.8倍后播放动画。如果android:animation中的动画时间是100ms,那么每个child都会延迟800ms后播放动画。如果不设置delay,那么所有child同时执行动画。
代码语言:javascript复制
<?xml version="1.0" encoding="utf-8"?>
<layoutAnimation xmlns:android="http://schemas.android.com/apk/res/android"
    android:animation="@anim/enter_from_left_for_child_of_group"
    android:animationOrder="normal"
    android:delay="0.8">
</layoutAnimation>
代码语言:javascript复制
R.anim.enter_from_left_for_child_of_group

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate
        android:duration="1000"
        android:fromXDelta="-100%p"
        android:toXDelta="0"/>

</set>

2.把LayoutAnimation设置给ViewGroup

代码语言:javascript复制
    <LinearLayout
        android:id="@ id/ll_layout_animation"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:layoutAnimation="@anim/layout_animation">
        <TextView
            android:layout_width="50dp"
            android:layout_height="wrap_content"
            android:textColor="#ff0000"
            android:text="呵呵呵"/>
        <TextView
            android:layout_width="60dp"
            android:layout_height="wrap_content"
            android:textColor="#ff0000"
            android:text="qq"
            android:background="@color/colorPrimary"/>
        <TextView
            android:layout_width="30dp"
            android:layout_height="wrap_content"
            android:textColor="#ff0000"
            android:text="啊啊"/>
    </LinearLayout>

除了xml,当然也可以使用LayoutAnimationController 指定:

代码语言:javascript复制
        //代码设置LayoutAnimation,实现ViewGroup的child的出场动画
        Animation enterAnim = AnimationUtils.loadAnimation(this, R.anim.enter_from_left_for_child_of_group);
        LayoutAnimationController controller = new LayoutAnimationController(enterAnim);
        controller.setDelay(0.8f);
        controller.setOrder(LayoutAnimationController.ORDER_NORMAL);
        llLayoutAnimation.setLayoutAnimation(controller);

1.5.2 Activity的切换效果 Activity默认有切换动画效果,我们也可以自定义:overridePendingTransition(int enterAnim, int exitAnim) 可以指定activity开大或暂停时的动画效果。

  • enterAnim,指要打开的activity进入的动画
  • exitAnim,要暂停的activity退出的动画

注意 必须在startActivity或finish之后使用才能生效。如下所示:

代码语言:javascript复制
    public static void launch(Activity activity) {
        Intent intent = new Intent(activity, AnimationTestActivity.class);
        activity.startActivity(intent);
        //打开的activity,从右侧进入,暂停的activity退出到左侧。
        activity.overridePendingTransition(R.anim.enter_from_right, R.anim.exit_to_left);
    }
......

    @Override
    public void finish() {
        super.finish();
        //打开的activity,就是上一个activity从左侧进入,要finish的activity退出到右侧
        overridePendingTransition(R.anim.enter_from_left, R.anim.exit_to_right);
    }

R.anim.enter_from_right

代码语言:javascript复制
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate
        android:duration="500"
        android:fromXDelta="100%p"
        android:toXDelta="0"/>
</set>

R.anim.exit_to_left

代码语言:javascript复制
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate
        android:duration="500"
        android:fromXDelta="0"
        android:toXDelta="-100%p"/>
</set>

R.anim.enter_from_left

代码语言:javascript复制
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate
        android:duration="500"
        android:fromXDelta="-100%p"
        android:toXDelta="0"/>

</set>

R.anim.exit_to_right

代码语言:javascript复制
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate
        android:duration="500"
        android:fromXDelta="-100%p"
        android:toXDelta="0"/>

</set>

二、属性动画

属性动画几乎无所不能,只要对象有这个属性,就可以对这个属性做动画。甚至还可以没有对象。可用通过ObjectAnimator、ValueAnimator、AnimatorSet实现丰富的动画。

2.1 使用方法

属性动画可对任意对象做动画,不仅仅是View。默认动画时间是300ms,10ms/帧。具体理解就是:可在给定的时间间隔内 实现 对象的某属性值 从 value1 到 value2的改变。

使用很简单,可以直接代码实现(推荐),也可xml实现,举例如下:

代码语言:javascript复制
        //属性动画使用,方式一:代码,建议使用。 横移
        ObjectAnimator translationX = ObjectAnimator
                .ofFloat(textView6, "translationX", 0, 200)
                .setDuration(1000);
        translationX.setInterpolator(new LinearInterpolator());
        setAnimatorListener(translationX);

        //属性动画使用,方式二:xml。   竖移
        Animator animatorUpAndDown = AnimatorInflater.loadAnimator(this, R.animator.animator_test);
        animatorUpAndDown.setTarget(textView6);

        //文字颜色变化
        ObjectAnimator textColor = ObjectAnimator
                .ofInt(textView6, "textColor", 0xffff0000, 0xff00ffff)
                .setDuration(1000);
        textColor.setRepeatCount(ValueAnimator.INFINITE);
        textColor.setRepeatMode(ValueAnimator.REVERSE);
        //注意,这里如果不设置 那么颜色就是跳跃的,设置ArgbEvaluator 就是连续过度的颜色变化
        textColor.setEvaluator(new ArgbEvaluator());

        //animatorSet
        mAnimatorSet = new AnimatorSet();
        mAnimatorSet
                .play(animatorUpAndDown)
                .with(textColor)
                .after(translationX);

        mAnimatorSet.start();


    /**
     * 设置属性动画的监听
     * @param translationX
     */
    private void setAnimatorListener(ObjectAnimator translationX) {
        translationX.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                //每播放一帧,都会调用
            }
        });
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            translationX.addPauseListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationResume(Animator animation) {
                    super.onAnimationResume(animation);
                }
            });
        }

        translationX.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
            }
        });
    }

R.animator.animator_test,是放在res/animator中。

代码语言:javascript复制
<?xml version="1.0" encoding="utf-8"?>
<!--属性动画test,一般建议采用代码实现,不用xml-->
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:ordering="sequentially">


    <!--repeatCount:默认是0,-1是无限循环-->
    <!--repeatMode:重复模式:restart-从头来一遍、reverse-反向来一遍-->
    <!--valueType:指定propertyName的类型可选intType、floatType-->

    <!--android:pathData=""
        android:propertyXName=""
        android:propertyYName=""-->
    <objectAnimator
        android:propertyName="translationY"
        android:duration="1000"
        android:valueFrom="0"
        android:valueTo="120"
        android:startOffset="0"
        android:repeatCount="0"
        android:repeatMode="reverse"
        android:valueType="floatType"
        android:interpolator="@android:interpolator/accelerate_decelerate" />

    <!--animator对用vueAnimator,比objectAnimator少了propertyName-->
    <!--<animator-->
        <!--android:duration="2000"-->
        <!--android:valueFrom=""-->
        <!--android:valueTo=""-->
        <!--android:startOffset=""-->
        <!--android:repeatCount=""-->
        <!--android:repeatMode=""-->
        <!--android:valueType=""-->
        <!--android:interpolator=""-->
        <!--android:pathData=""-->
        <!--android:propertyXName=""-->
        <!--android:propertyYName=""/>-->

</set>

translationX是实现横移,animatorUpAndDown是实现竖移、textColor是实现文字颜色变化。其中animatorUpAndDown是使用xml定义,标签含义也很好理解。最后使用AnimatorSet的play、with、after 实现 先横移,然后 竖移和颜色变化 同时的动画集合效果。

注意点

  1. 关于View动画和属性动画的平移属性动画改变属性值setTranslationX 的视图效果像view动画的平移一样,都是view实际的layout位置没变,只改变了视图位置;不同点是属性动画 给触摸点生效区域增加了位移(而view动画仅改变了视图位置)。
  2. 插值器:Interpolator,根据 时间流逝的百分比,计算当前属性值改变的百分比。 例如duration是1000,start后过了200,那么时间百分比是0.2,那么如果差值器是LinearInterpolator线性差值器,那么属性值改变的百分比也是0.2
  3. 估值器:Evaluator,就是根据 差值器获取的 属性值百分比,计算改变后的属性值。ofInt、onFloat内部会自动设置IntEvaluator、FloatEvaluator。如果使用ofInt且是颜色相关的属性,就要设置ArgbEvaluator。上面例子中 文字颜色变化动画 设置了ArgbEvaluator:textColor.setEvaluator(new ArgbEvaluator())。
  4. 动画监听:主要是两个监听接口,AnimatorUpdateListener、AnimatorListenerAdapter。AnimatorUpdateListener的回调方法在每帧更新时都会调用一次;AnimatorListenerAdapter可以监听开始、结束、暂停、继续、重复、取消,重写你要关注的方法即可。

2.2对任意属性做动画

一个问题,针对下面的Button,如何实现 的宽度逐渐拉长的动画,即文字不变,仅拉长背景宽度?

代码语言:javascript复制
    <Button
        android:id="@ id/button_animator_test"
        android:layout_width="180dp"
        android:layout_height="wrap_content"
        android:text="任意属性动画-宽度拉长"/>

首先,View动画的ScaleAnimation是无法实现的,因为view的scale是把view的视图放大,这样文字也会拉长变形。那么属性动画呢?试试~

代码语言:javascript复制
        ObjectAnimator width1 = ObjectAnimator.ofInt(button, "width", 1000);
        width1.setDuration(2000);
        width1.start();

但是发现,没有效果!这是为啥呢?解释如下.

对object 的任意属性做动画 要求两个条件:

  1. object有 对应属性 的set方法,动画中没设置初始值 还要有get方法,系统要去取初始值(不满足则会crash)。
  2. set方法要对object有所改变,如UI的变化。不满足则会没有动画效果

上面Button没有动画效果,就是没有满足第二条。看下Button的setWidth方法:

代码语言:javascript复制
    public void setWidth(int pixels) {
        mMaxWidth = mMinWidth = pixels;
        mMaxWidthMode = mMinWidthMode = PIXELS;
        requestLayout();
        invalidate();
    }

实际就是TextView的setWidth方法,看到设置进去的值仅影响了宽度最大值和最小值。按照官方注释和实测,发现只有当Button/TextView在xml中设置android:layout_width为"wrap_content"时,才会setWidth改变宽度;而当Button/TextView在xml中设置android:layout_width为固定dp值时,setWidth无效。而我们上面给出的Button xml中确实是固定值180dp,所以是属性"width"的setWidth是无效的,即不满足第二条要求,就没有动画效果了。(当修改Button xml中设置android:layout_width为"wrap_content"时,上面执行的属性动画是生效的。)

那么,当不满足条件时,如何解决此问题呢?有如下处理方法:

  1. 给object添加set、get方法,如果有权限。(一般不行,如TextView是SDK里面的不能直接改)
  2. 给Object包装一层,在包装类中提供set、get方法。
  3. 使用ValueAnimator,监听Value变化过程,自己实现属性的改变。
代码语言:javascript复制
    private void testAnimatorAboutButtonWidth() {
        //Button width 属性动画:如果xml中宽度是wrap_content,那么动画有效。
        // 如果设置button确切的dp值,那么无效,因为对应属性"width"的setWidth()方法就是 在wrap_content是才有效。
        ObjectAnimator width1 = ObjectAnimator.ofInt(button, "width", 1000);
        width1.setDuration(2000);
//        width1.start();

        //那么,想要在button原本有确切dp值时,要能对width动画,怎么做呢?
        //方法一,包一层,然后用layoutParams
        ViewWrapper wrapper = new ViewWrapper(button);
        ObjectAnimator width2 = ObjectAnimator.ofInt(wrapper, "width", 1000);
        width2.setDuration(2000);
//        width2.start();

        //方法二,使用ValueAnimator,每一帧自己显示宽度的变化
        ValueAnimator valueAnimator = ValueAnimator.ofInt(button.getLayoutParams().width, 1000);
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                int animatedValue = (Integer) animation.getAnimatedValue();
                Log.i("hfy", "onAnimationUpdate: animatedValue="   animatedValue);

//                IntEvaluator intEvaluator = new IntEvaluator();
////                获取属性值改变比例、计算属性值
//                float animatedFraction = animation.getAnimatedFraction();
//                Integer evaluate = intEvaluator.evaluate(animatedFraction, 300, 600);
//                Log.i("hfy", "onAnimationUpdate: evaluate=" evaluate);


                if (button != null) {
                    button.getLayoutParams().width = animatedValue;
                    button.requestLayout();
                }
            }
        });

        valueAnimator.setDuration(4000).start();

    }

    /**
     * 包一层,提供对应属性的set、get方法
     */
    private class ViewWrapper {

        private final View mView;

        public ViewWrapper(View view) {
            mView = view;
        }

        public int getWidth() {
            return mView.getLayoutParams().width;
        }

        public void setWidth(int width) {
            ViewGroup.LayoutParams layoutParams = mView.getLayoutParams();
            layoutParams.width = width;
            mView.setLayoutParams(layoutParams);
            mView.requestLayout();
        }
    }

2.3 属性动画的原理

属性动画,要求对象有这个属性的set方法,执行时会根据传入的 属性初始值、最终值,在每帧更新时调用set方法设置当前时刻的 属性值。随着时间推移,set的属性值会接近最终值,从而达到动画效果。如果没传入初始值,那么对象还要有get方法,用于获取初始值。

在获取初始值、set属性值时,都是使用 反射 的方式,进行 get、set方法的调用。 见PropertyValuesHolder的setupValue、setAnimatedValue方法:

代码语言:javascript复制
    private void setupValue(Object target, Keyframe kf) {
        if (mProperty != null) {
            Object value = convertBack(mProperty.get(target));
            kf.setValue(value);
        } else {
            try {
                if (mGetter == null) {
                    Class targetClass = target.getClass();
                    setupGetter(targetClass);
                    if (mGetter == null) {
                        // Already logged the error - just return to avoid NPE
                        return;
                    }
                }
                Object value = convertBack(mGetter.invoke(target));
                kf.setValue(value);
            } catch (InvocationTargetException e) {
                Log.e("PropertyValuesHolder", e.toString());
            } catch (IllegalAccessException e) {
                Log.e("PropertyValuesHolder", e.toString());
            }
        }
    }
代码语言:javascript复制
    void setAnimatedValue(Object target) {
        if (mProperty != null) {
            mProperty.set(target, getAnimatedValue());
        }
        if (mSetter != null) {
            try {
                mTmpValueArray[0] = getAnimatedValue();
                mSetter.invoke(target, mTmpValueArray);
            } catch (InvocationTargetException e) {
                Log.e("PropertyValuesHolder", e.toString());
            } catch (IllegalAccessException e) {
                Log.e("PropertyValuesHolder", e.toString());
            }
        }
    }

以上效果图:

动画效果

三、使用动画的注意事项

  1. 使用帧动画,避免OOM。因为图片多。
  2. 属性动画 如果有循环动画,在页面退出时要及时停止,避免内存泄漏。
  3. 使用View动画后,调用setVisibility(View.GONE)失效时,使用view.clearAnimation()可解决。

附上之前记录的一些动画效果 自定义view:TextSwitcher使用 自定义view:信息飘窗/弹幕——AutoSwitchTextView 自定义view:ProgressBar 前景色、背景色、平滑显示进度

0 人点赞