动画和图形——Property Animation


property animation系统是一套健壮的框架,允许我们动画化几乎任何东西。我们可以定义一个动画来改变对象的任何属性,而不用管它是否会绘制到屏幕上。属性动画随着时间改变对象属性值(对象的一个域)。动画化一个实体,我们指定这个实物需要动画化的属性,比如一个实体在屏幕上的位置,持续多长时间和其它在动画持续过程中的属性。

property animation系统允许需要我们定义以下特征:

  • Duration:指定动画持续时间。缺省时长为300ms。
  • Time interpolation(时间插值):指定当前时间下各属性值的函数计算方式。
  • Repeat count and behavior:当动画持续时间到时,指定是否需要重复和重复的次数。同时还可以指出是否反转动画。设置反转则会一直向前和向后播放,直到达到重复次数。
  • Animator sets:我们可以指定一组动画的播放规则。播放顺序和时间间隔等
  • Frame refresh delay:指定动画帧的刷新频率。缺省状态下是10ms刷新一次,但是应用的刷新速度则要看系统繁忙程度和系统使用底层定时器服务的速度。
How Property Animation Works
首先,我们看一个动画如何工作的简单例子。图一描绘了一个假定的在根据一个x属性移动的对象,这个x属性代表了它在屏幕水平线上的位置。动画的持续时间设置为40ms,动画间隔距离设置为40pixels。每隔10ms(缺省刷新时间),对象向右移动10个像素点。在40ms后,对象停在水平位置40像素点处。这个例子使用的是线性插值,这意味着对象匀速移动。
图1
我们也可以指定动画为非线性插值。图二描绘了假定实体对象一开始加速,最后减速的动画。该对象依然在40ms内移动40个像素点,但是是非线性的。开始,动画加速到一半距离的时候减速知道结束。如图二所示,中间部分对象移动的距离比开始和结尾都要长。
图2
我们来仔细分析下property animation的组成元素。图三描绘了它工作的类结构图
图3
ValueAnimator对象保存动画时间轨迹,比如动画已经播放的时间,当前动画的属性值。

ValueAnimator包括一个时间插值器(TimeInterpolator)定义了一个动画插值和一个动画属性值计算方式的类型器(TypeEvaluator)。比如,在图2中,时间插值器是加减速插值器(AccelerateDecelerateInterpolator),类型器是整型(IntEvaluator)。

启动一个动画,需要创建一个ValueAnimator并且赋开始属性值和结束属性值以及动画间隔长度(duration)。当你调用start()时,动画开始。在整个动画播放过程中,ValueAnimator在[0,1]区间计算过去的部分(elapsed fraction),它基于duration和已经过去的时间。已经过去的部分代表动画已完成时间百分比,0意味着完成1%,1意味着完成100%。比如,在图1中,已过去的部分在10ms处是0.25,因为整个duration为40ms。

当ValueAnimator计算完过去部分后,它调用TimeInterpolator来计算插入部分(interpolated fraction)。插入部分把过去部分映射到一个考虑了时间插值的新部分中。比如图2中,因为动画加速缓慢,插入部分,在10ms处大约为0.15,小于过去部分。在图1中,插入部分与过去部分总是相同。

插值部分计算完成后,ValueAnimator调用合适的TypeEvaluator,跟据插值部分来计算正在播放的动画属性值,开始值和结束值。比如在图2中,在10ms处,插值部分为0.15,所以这个时候的属性值是0.15*(40 - 0),即6。

API Demos 示例工程的com.example.android.apis.animation包里提供了很多如何使用property animation的例子。

How Property Animation Differs from View Animation

view animation只能用于绘制view对象的动画。所以你想实现非view对象动画,就必须自己用代码实现。view animation同时限制这view对象在动画方面的能力,比如放大,旋转等却不能实现背景颜色。

view animation的另外一个缺点是它之改变View绘制处的视图并不是改变真正的view。比如,如果你动画化一个按钮把它移动到屏幕别的地方,按钮绘制正确,但是你点击按钮的地方却不能改变,所以你必须自己在逻辑上实现这一点。

property animation没有这些限制,你可以动画化任何实体对象同时改变的也是它自己。同时它也是一个健壮的动画系统。你可以分配动画器(animator)给你想动画化的属性,比如颜色,位置,大小同时可以定义动画的影响,比如插值和多动画器的同步。

view animation占用时间少,需要的编码也少,如果使用view animation即完成了你的功能需要,则没必要使用property animation。同时如果需要我们可以同时使用这两类动画。

API Overview

在android.animation中,我们可以找到property animation的绝大部分API接口。因为view animation已经定义了许多插值器,因此你也可以直接在property animation中使用它们,它们的API在android.view.animation中。下面的表格描述了property animation的主要元素类。

Animator类提供了创建动画的基础框架。通常我们不会直接使用它,因为它提供的功能比较有限,需要我们拓展。下列子类继承自Animator。
Animators
ValueAnimatorproperty animation的主要时间引擎,它同时计算了作为动画的属性的值。它有计算动画值的所有核心属性,包括每一个动画的时间细节,动画是否重复的信息,接受更新时间的监听器和设置计算类型的方法。动画的属性有两个片段组成:计算动画值和设置对象正在进行的动画的属性。ValurAnimator不实现第二个片段。所以你必须监听ValueAnimator计算出来的值。查看Animating with ValueAnimator获取更多信息。
ObjectAnimatorValueAnimator的子类,允许我们设置一个目标对象和动画化的对象属性。这个类根据它计算的动画的新值来更新属性。大部分时候我们使用这个类来创建动画,因为它对目标实体的动画之值的创建程序更简单。然而,有时候我们需要直接使用ValueAnimator。因为ObjectAnimator限制更少,比如需要确切的加速方法。
AnimatorSet提供了一组animation按一定关系播放的机制。这部分内容查看Choreographing multiple animations with Animator Sets 。

求值程序(Evaluators)为property animation计算给出的属性的值。它通过Animator类提供的时间数据,动画的初始和结束值计算动画过程中的值。property animation提供了以下求值程序:
IntEvaluator缺省的计算int类型属性值的求值程序
FloatEvaluator缺省的计算float类型属性值的求值程序
ArgbEvaluator缺省的计算颜色属性的求值属性,颜色属性以十六进制形式表示
TypeEvaluator一个允许我们创建自己的求值程序的接口。当对象的属性不是上述三种类型时需要我们创建自己的求值程序;当是上述三种类型,而我们想求值方式不与缺省相同时,也可以使用这个求值程序。更多信息查看Using a TypeEvaluator

时间插值器定义了一个指定值的时间函数。比如,我们可以指定一个动画在整个播放过程中是线性移动的,这意味着动画是匀速移动的。下表显示的插值器定义在android.view.animation中。如果这些插值器都不符合要求,我们可以继承TimeInterpolator接口创建自己的插值器。查看Using interpolators如何定义自己的插值器。
AccelerateDecelerateInterpolator开始和结尾变化速率较慢,中间较慢的插值器
AccelerateInterpolator加速插值器
AnticipateInterpolator开始向后然后向前的插值器
AnticipateOvershootInterpolator开始向后然后向前到超过目标值,最后返回到最终值的插值器
BounceInterpolator变化在结尾反弹的插值器
CycleInterpolator动画重复指定次数
DecelerateInterpolator减速插值器
OvershootInterpolator变化向前超过最后值然后回转
LinearInterpolator变化速率恒定不变
TimeInterpolator定义自己的插值器的接口

Animating with ValueAnimator
ValueAnimator类允许我们在把动画播放持续时间量化为我们指定的int,float或color值。调用工厂方法ofInt(),ofFloat()或ofObject()获取一个ValueAnimator。例如:
ValueAnimator animation = ValueAniator.ofFloat(0f, 1f);
animation.setDuration(1000);
animation.start();
在这段代码中,ValueAnimator在start()方法开始之后再持续时长1000ms中在[0,1]间计算动画值。

按下述方式可以指定自己的计算类型:
ValueAnimator animation = ValeAnimator.ofObject(new MyTypeEvaluator(), startPropertyValue, endPropertyValue);
animation.setDuration(1000);
animation.start();
在这段代码中,ValueAnimator在start()方法开始之后在持续时长1000ms中在[startPropertyValue, endPropertyValue]间使用MyTypeEvaluator提供的逻辑运算方式计算动画值。

上述这些代码段,对一个对象没有实际效果,因为ValueAnimator不会直接操作对象或属性。最有可能的操作是我们用这些计算出来的值修改对象。通过在ValeAnimator的生命周期中定义监听器处理相应的事件,比如框架更新。当继承监听器时,我们可以通过调用getAnimatedValue()为指定的框架刷新过程获取计算值。更多关于监听器的信息,查看Animation Listeners部分。

Animating with ObjectAnimator

ObjectAnimator是ValueAnimator的子类,它联合时间引擎和ValueAnimator的值运算能力来计算一个目标对象的属性。这使得对象的动画变得简单,因为我们不需要继承ValueAnimator.AnimatorUpdateListener,动画属性会自动更新。

初始化ObjectAnimator和初始化ValueAnimator很相似,而且我们仍然指定对象和对象的名称(String类型)以及属性值如下进行动画播放:
ObjectAnimator anim = ObjectAnimator.ofFloat(foo, "alpha", 0f, 1f);
anim.setDuration(1000);
anim.start();

要想ObjectAnimator正确更新属性,我们需要做以下这些工作:
  • 对于将要播放的动画的属性,我们需要为其指定set函数,形式如set<propertyName>()。原因是ObjectAnimator能在动画播放期间自动更新属性,它必须用set函数获取属性值。例如,属性名为foo,我们需要写一个setFoo()函数,如果这个set函数不存在,那么我们有以下三个选择。
  1. 如果我们有权限添加一个set函数到类中,那么添加一个set函数。
  2. 使用一个wraaper类,前提是我们有权限修改wrapper类和使用一个空的set函数接收值和把它改为原先的对象。
  3. 使用ValueAnimator代替
  • 如果我们在一个ObjectAnimator工厂方法里只为属性值...参数指定了一个值,那么这个值被假定为动画结束值。同时,也需要有一个get函数来获取动画的初始值。get函数的形式为get<propertyName>()。例如,函数名为foo,我们需要一个getFoo()函数。
  • 属性的get和set函数必须是与我们指定给ObjectAnimator的开始和结束值相同的类型。例如,我们构建了一个下述的ObjectAnimator,那么必须有targetObject.setPropName(float)和targetObject.getPropName(float)函数:
  • ObjectAnimator.ofFloat(targetObject, "propName", 1f)
  • 根据我们播放的对象的属性值,我们需要在一个view视图上调用invalidate()方法强制屏幕在新更新的动画值下重绘。这个工作在onAnimationUpdate()回调中完成。例如,一个Drawable对象的颜色属性只在对象自己重绘的时候才会更新屏幕。所有的视图的属性的set函数,例如setAlpha()和setTranslationX()函数会重绘视图属性,所以当调用了这些方法获取新值时我们不需要重绘视图。更多信息,查看AnimationListeners部分。

Choreographing Multiple Animations with AnimatorSet

在许多实例中,一些动画的播放取决于另外的动画是否开始或结束。安卓系统允许我们把多个动画绑定到一个AnimatorSet中。所以我们只需要指定动画是否同时,先后或一定延迟之后开始。同时我们也可以嵌套AnimatorSet对象。

下面的这个实例代码取自Bouncing Balls实例,它播放Animator对象的规则如下:
  1. 播放bounceAnim
  2. 同时播放squashAnim1,squshAnim2,stretchAnim1和stretchAnim2
  3. 播放bounceBackAnim
  4. 播放fadeAnim

AnimatorSet bouncer = new AnimatorSet();
bouncer.play(bounceAnim).before(squashAnim1);
bouncer.play(squashAnim1).with(squashAnim2);
bouncer.play(squashAnim1).with(stretchAnim1);
bouncer.play(squashAnim1).with(stretchAnim2);
bouncer.play(bounceBackAnim).after(stretchAnim2);
ValueAnimator fadeAnim = ObjectAnimator.ofFloat(newBall, "alpha", 1f, 0f);
fadeAnim.setDuration(250);
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.play(bouncer).before(fadeAnim);
animatorSet.start();

完整的使用animator set实例,查看APIDemos中的Bouncing Balls样例。


Animation Listeners

在动画播放期间,可以监听以下事件:

  • Animator.AnimatorListener
  1. onAnimationStart()——动画开始时调用
  2. onAnimationEnd()——动画结束时调用
  3. onAnimationRepeat()——重复动画播放时调用
  4. onAnimationCancel()——取消动画播放时调用,同时会调用onAnimationEnd()。
  • ValueAnimator.AnimatorUpdateListener
  1. onAnimationUpdate()——动画的每一桢都调用。在动画播放期间监听这个事件以使用ValueAnimator计算产生的值。要使用这个值,查询传递给监听事件的ValueAnimator对象并使用getAnimatedValue()方法获取值。
  2. 根据播放的对象和属性,我们可能需要在视图对象上调用invalidate()方法强制屏幕上的某个区域使用新值重绘。例如,使用一个Drawable对象的颜色属性只有在该对象自己重绘时才会重绘屏幕。所有视图的set属性,比如setAlpha()和setTranslationX()会刷新视图。所以调用了这些方法时不需要重新刷新视图。

如果我们不想实现Animator.AnimatorListener接口的所有方法,可以继承AnimatorListenerAdapter类代替。AnimatorListenerAdapter类提供了空继承方法以便我们选择重写。

例如在Bouncing Balls样例中,在onAnimationEnd()回调中创建了AnimatorListenerAdapter对象。

ValueAnimator fadeAnim = ObjectAnimator.ofFloat(newBall, "alpha", 1f, 0f);
fadeAnim.setDuration(250);
fadeAnim.addListener(new AnimatorListenerAdapter(){
       public void onAnimationEnd(Animator animation){
              balls.remove(((ObjectAnimator)animation).getTarget());
       }
}

Animating Layout Changes to ViewGroups

property animation提供了动画改变ViewGroup对象和简单动画化View对象的功能。

我们可以使用LayoutTransition类动画改变一个ViewGroup.动画化一个layout的意思是ViewGroup里的Views添加或移除(setVisibility()方法)时,可以用动画形式表现出来。同时此时ViewGroup里剩余的Views布置到新位置的过程同样可以用动画形式表现。在LayoutTransition对象中可以定义以下animations,我们需要在一个Animator对象中调用setAnimator传递LayoutTransition常数。
  • APPEARING——说明为项目出现在容器中的的动画的标志
  • CHANGE_APPEARING——添加新项目后的动画的标志
  • DISAPPEARING——项目移除的动画标志
  • CHANGE_DISAPPEARING——移除新项目后的动画的标志

除这四个事件之外我们还可以定义自己的动画方式。API Demos里的LayoutAnimations样例展示了如何定义layout transition动画,再给Views对象添加动画。


LayoutAnimationsByDefault类和相应的layout_animations_by_default.xml资源展示了如何在XML文件中使用layout transitions。我们唯一需要做的是设置ViewGroup的android:animateLayoutchanges属性值为true。例如:

<LinearLayout
         android:orientation="vertical"
         android:layout_width="wrap_content"
         android:layout_height="match_parent"
         android:id="@+id/verticalContainer"
         android:animateLayoutChanges="true" />

设置这个属性为true,则添加或移除的views以及其他重新布置的views都会自动动画化。


Using a TypeEvaluator


通过实现TypeEvaluator接口,我们可以创建自己的evaluator。安卓系统提供了IntEvaluator,FloatEvaluator和ArgbEvaluator类型的类型估算器对应于int,float和color类型。

TypeEvaluator只需要实现一个evaluate()方法。这就使得我们可以为当前的动画属性值返回一个合适的值。FloatEvaluator类告诉我们如何实现这一点:
public class FloatEvaluator implements TypeEvaluator{
         public Object evaluate(float fraction, Object startValue, Object endValue){
                      float startFloat = ((Number)startValue).floatValue();
                      return startFloat + fraction & (((Number)endValue).floatValue() - startFloat);
         }
}

Note:当ValueAnimator(或ObjectAnimator)启动后,它计算动画当前过去的部分,值在[0,1],接着根据使用的插值器计算插值部分。插值部分是我们的TypeEvaluator在fraction参数中接收到的部分,所以我们计算动画值时不需要考虑插值器。

Using Interpolators

插值器定义了一个指定值在动画播放期间的时间函数。例如,我们可以指定某个动画在整个播放期间线性移动。这意味着动画在整个期间匀速移动。

在动画系统中插值器接收Animator的一个fraction代表动画过去的时间。插值器修改这个fraction以使目标提供的动画类型一致。安卓系统提供了一些插值器,除此之外我们也可以继承TimeInterpolator定义自己的插值器。

例如,缺省的AccelerateDecelerateInterpolator和LinearInterpolator是如何计算插值部分如下相较。LinearInterpolator在过去的部分没有效果,AccelerateDecelerateInterpolator加速进去,减速出来。下面的方法定义了这些插值器的运算逻辑:
AccelerateDecelerateInterpolator 
public float getInterpolation(float input){
          return (float)(Math.cos((input + 1) * Math.PI) / 2.0f) + 0.5f;
}

LinearInterpolator
public float getInterpolation(float input){
         return input;
}

下面的这个表格表示了一个动画持续1000ms下这些插值器计算出来的估算值。

正如表格所示,LinearInterpolator匀速改变值,.2表示200ms过去,AccelerateDecelerateInterpolator比LinearInterpolator在200ms到600ms改变值快而在600ms到1000ms慢。

Specifying Keyframes

一个Keyframe对象由一个time/value键值对组成,它定义了一个动画在特定时间下的特定状态。keyframe可以有自己的插值器控制先前的keyframe时间到当下keyframe时间区间内的动画操作。

初始化Keyframe对象,必须使用以下工厂方法,ofInt(),ofFloat(),ofObject来获取Keyframe的合适类型。然后调用ofKeyframe()工厂方法获取PropertyValuesHolder对象。一旦有了这个对象,我们可以传递PropertyValueHolder对象获取animator进行动画播放。以下代码段描述了如何操作这些:
Keyframe kf0 = Keyframe.ofFloat(0f, 0f);
Keyframe kf1 = Keyframe.ofFloat(.5f, 360f);
Keyframe kf2 = Keyframe.ofFloat(1f, 0f);
PropertyValuesHolder pvhRotation = PropertyValuesHolder.ofKeyframe("rotation", kf0, kf1, kf2);
ObjectAnimator rotationAnim = ObjectAnimator.ofPropertyValuesHolder(target, pvhRotation);
rotationAnim.setDuration(5000ms);

更多使用keyframes的实例,查看APIDemos中的MultiPropertyAnimation实例

Animating Views

Property animation系统允许给View对象的流线动画,并比view animation系统多提供一些优势。view animation系统通过改变它们绘制的方式移动View对象。这中操作方式的句柄在装载它们的容器里,因为view自己并没有任何可供操作的属性。这样的结果是view视图有了动画,但View对象自身并没有任何改变。这导致的结果是尽管视图绘制到了不同的地方,但对象依然存在于它的原始位置,Android3.0中,添加了新的属性和相应的get和set方法以消除这个缺点。

property animation系统可以在屏幕上真正的改变View的属性而动画化View。同时Views会自动调用invalidate()方法来更新即时刷新屏幕。View类的以下属性会引发property animation:
  • translationX和translationY——这两个属性有layout容器控制,它们从代表以屏幕左上角为顶点的坐标轴的坐标。
  • rotation,rotationX和rotationY——控制二维旋转(rotation)和三维绕轴旋转的属性。
  • scaleX和scaleY——以中心点为基准的缩放属性。
  • pivotX和pivotY——控制中心点。缺省状态下中心点是对象的中心。
  • x和y——容器根据left,top和translationX,translationY计算出来的表示View最终位置的属性。
  • alpha——视图透明度,缺省状态下这个值为1,0表示完全透明。

动画化View对象的一个属性,比如颜色或rotation值,需要创建一个property animator指定需要动画化的View属性。例如:
ObjectAnimator.ofFloat(myView, "rotation", 0f, 360f);

更多创建animator的信息,查看ValueAnimator和ObjectAnimator类相关内容。

Animating with ViewPropertyAnimator
ViewPropertyAnimator类通过使用单一的Animator对象提供了一个简单的平行动画化View属性的简单方法。它更像一个ObjectAnimator,它改变View属性的真实值,而且同时处理多个属性时更高效。同时使用这个类的代码段更加精确和简便易读。下面的代码段展示了使用多个ObjectAnimator对象,一个单一的ObjectAnimator对象和同时在x和y属性动画化一个view使用ViewPropertyAnimator的区别:
Multiple ObjectAnimator objects
ObjectAnimator animX = ObjectAnimator.ofFloat(myView, "x", 50f);
ObjectAnimator animY = ObjectAnimator.ofFloat(myView, "y", 100f);
AnimatorSet animSetXY = new AnimatorSet();
animSetXY.playTogether(animX, animY);
animSetXY.start();

One ObjectAnimator
PropertyValuesHolder pvhX = PropertyValuesHolder.ofFloat("x", 50f);
PropertyValuesHolde pvhY = PropertyValuesHolder.ofFloat("y", 100f);
ObjectAnimator.ofPropertyValuesHolder(myView, pvhX, pvhY).start();

ViewPropertyAnimator
myView.animate().x(50f).y(100f);

Declaring Animations in XML
property animation系统允许在xml文件中定义动画。在xml文件中定义动画使我们可以在多个activity中重复使用动画,同时编辑animation也更加简便。

从Android3.1开始,分开了使用新property animation API和使用view animation的文件。property animations的xml文件放在res/animator/文件夹中,而不再是res/anim/文件夹。animator文件夹的名称可以随便命名,但是为了使用Eclipse ADT中的layout编辑器,最好还是使用animator,因为ADT仅仅会在res/animator/文件夹中搜索property animation资源。

以下几个property animation类在XML文件中申明了对应的标签:
  • ValueAnimator——<animator>
  • ObjectAnimator——<objectAnimator>
  • AnimatorSet——<set>

下面的实例按先后顺序播放两组动画,第一组嵌套动画同时播放两个object animation:

<set android:ordering="sequentially">
    <set>
        <objectAnimator
            android:propertyName="x"
            android:duration="500"
            android:valueTo="400"
            android:valueType="intType"/>
        <objectAnimator
            android:propertyName="y"
            android:duration="500"
            android:valueTo="300"
            android:valueType="intType"/>
    </set>
    <objectAnimator
        android:propertyName="alpha"
        android:duration="500"
        android:valueTo="1f"/>
</set>

播放这些动画,必须在代码中把XML资源填充到一个AnimatorSet对象中,并且在动画播放前为所有动画指定目标对象。调用setTarget()为AnimatorSet的所有子类设置一个目标对象。如下代码所示:

AnimatorSet set = (AnimatorSet)AnimatorInflater.loadAnimator(myContext, R.anim.property_animator);
set.setTarget(myObject);
set.start();

更多关于property animations的XML语法,查看Animation Resources。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
资源包主要包含以下内容: ASP项目源码:每个资源包中都包含完整的ASP项目源码,这些源码采用了经典的ASP技术开发,结构清晰、注释详细,帮助用户轻松理解整个项目的逻辑和实现方式。通过这些源码,用户可以学习到ASP的基本语法、服务器端脚本编写方法、数据库操作、用户权限管理等关键技术。 数据库设计文件:为了方便用户更好地理解系统的后台逻辑,每个项目中都附带了完整的数据库设计文件。这些文件通常包括数据库结构图、数据表设计文档,以及示例数据SQL脚本。用户可以通过这些文件快速搭建项目所需的数据库环境,并了解各个数据表之间的关系和作用。 详细的开发文档:每个资源包都附有详细的开发文档,文档内容包括项目背景介绍、功能模块说明、系统流程图、用户界面设计以及关键代码解析等。这些文档为用户提供了深入的学习材料,使得即便是从零开始的开发者也能逐步掌握项目开发的全过程。 项目演示与使用指南:为帮助用户更好地理解和使用这些ASP项目,每个资源包中都包含项目的演示文件和使用指南。演示文件通常以视频或图文形式展示项目的主要功能和操作流程,使用指南则详细说明了如何配置开发环境、部署项目以及常见问题的解决方法。 毕业设计参考:对于正在准备毕业设计的学生来说,这些资源包是绝佳的参考材料。每个项目不仅功能完善、结构清晰,还符合常见的毕业设计要求和标准。通过这些项目,学生可以学习到如何从零开始构建一个完整的Web系统,并积累丰富的项目经验。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值