博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Android Property Animation
阅读量:5987 次
发布时间:2019-06-20

本文共 12851 字,大约阅读时间需要 42 分钟。

属性动画系统是一个健壮的框架,它使你几乎能够将任何东西做成动画。你可以定义一个动画来随着时间改变任何对象属性,而不管它是否会被绘制到屏幕上。一个属性动画在一个特定的时间长度中改变一个属性的值(一个对象中的一个成员)。当这个属性会影响到屏幕上绘制的组件时,也就产生了我们看得到的那类动画了。要动画化一些东西,你可以指定你想要动画化的对象属性,比如一个对象在屏幕上的位置,你想要动画化它的长度,及你想要动画化的插值。

属性动画系统允许你定义一个动画的如下属性:

  • 持续时间:你可以指定一个动画的持续时间。默认的长度为300 ms。

  • 时间插值:你可以把计算属性值的方式指定为一个以动画的当前时间为自变量的函数。

  • 重复次数和行为:你可以指定当持续时间结束时,是否重复执行一个动画,及重复执行的次数。你也可以指定是否要反向回放动画。设置它为reverse则将重复地正向->反向播放动画,直到达到重复次数。

  • Animator集合:你可以把动画分成一起播放、顺序播放或某个特定时间点之后播放的逻辑集合的组。

  • 帧刷新延迟:你可以指定刷新你的动画帧的频率。默认设置为每10ms刷新一次,但你的应用程序所能达到的刷新帧的速度最终还是依赖于系统总体上有多繁忙,及系统能够服务的底层定时器有多快。

属性动画如何工作


首先,让我们通过一个简单的例子看一下一个动画是如何工作的。如Figure1,描述了一个假设的对象,它的x属性将会被动画化,x属性表示它在屏幕上的水平位置。动画的持续时间被设置为40 ms,经过的距离被设置为40 pixels。每10ms,即默认的帧刷新频率,对象水平移动10 pixels。在40 ms的最后,动画停止,对象最终位于水平位置40处。这是一个线性插值的动画的例子,即对象以一个固定的速度移动。

31092435_mLna.png

Figure 1. 线性动画的一个例子

你也可以为动画指定一个非线性的插值法。Figure 2描绘了一个假设的对象,它在动画的开始处加速,而在动画的结束处减速。对象仍然在40 ms内移动40 pixels,但是是非线性的。在开始处,这个动画加速到中间点,然后从中间点减速直到动画结束。如Figure 2所示,开始处和结束处动画移动的距离要小于中间位置的。

31092437_fQM4.png

Figure 2. 一个非线性动画的例子

让我们详细地看一下属性动画系统的重要组件如何像上面描述的那样计算动画。Figure 3描述了各个主要的类是如何协同工作的。

31092437_p0yX.png

Figure 3. 动画如何计算

对象记录了你的动画的时序,比如动画已经运行了多长时间,及动画化的属性的当前值。

封装了一个,其定义了动画插值法,并封装了一个,其定义了如何计算被动画化的属性的值。比如,在Figure 2中,是,而将是。

要启动一个动画,则创建一个,并给它要动画化的属性的起始和结束值,同时还有动画的持续时间。当你调用时,动画开始执行。在整个动画期间, 计算一个0到1之间的运行部分,基于动画的持续时间及已经逝去的时间。运行部分表示动画已经完成的时间的百分比,0表示0%,1表示100%。比如在Figure 1中,由于总共的持续时间是t = 40 ms,则运行部分在 t = 10 ms时将是.25。

当计算一个运行部分完成之后,它会调用当前设置的,来计算一个插值部分。一个插值部分把运行部分映射到一个新的值,这其中会考量设置的时间插值。比如,在Figure 2中,由于动画缓慢加速,则插值部分,大概.15,小于运行部分,.25,在t = 10 ms时。在Figure 1中,插值部分总是与运行部分相同。

当计算插值部分时,调用适当的 ,来计算你在动画化的属性的值,基于插值部分,及动画的起始值和结束值。比如在Figure 2中,在t = 10 ms时,插值部分是.15,因此在那个时间点属性的值将是将是.15 X (40 - 0),或6。

在示例工程的com.example.android.apis.animation包中提供了许多关于如何使用属性动画系统的例子。

属性动画与View动画有何不同


View动画系统提供的功能只能动画化对象,因而如果你想要动画化一个非-对象,则你将不得不实现自己的code来做到这一点。事实上view动画系统也有一些限制,它只暴露了动画化一个对象的某些方面的接口,比如放缩或者旋转一个View,但不能是,比如背景颜色。

View动画系统的另外一个劣势是,它只修改绘制View的位置,但不改变实际的View本身。比如如果你动画化一个button来跨屏幕移动,button会被正确的绘制,但是你能够click的button的实际位置不会改变,因而你将不得不实现你自己的逻辑来处理这些。

借助于属性动画系统,这些限制完全被移除,你能够动画化任何对象的任何属性(Views和非-Views),并且实际上对象本身会被修改。就实现动画的方式来说,属性动画系统也更健壮。在高层级来看,你为要动画化的属性分配animators,比如颜色,位置,或大小,并且可以定义动画的某些方面,比如插值和多个动画的同步。

然而view动画系统,写起来需要更少的code,设置起来需要更少的时间。如果view动画完成了你想要做的所有事情,或者你的已有的code已经以你想要的方式运行了,则没有必要使用属性动画系统了。如果有需要,对不同的情形同时使用两种动画系统也可能是有意义的。

API概览


你可以在中找到大部分属性动画系统的APIs。由于view动画系统已经在中定义了许多插值法,因而你也可以在属性动画系统中使用那些插值法。下面的表描述了属性动画系统的主要组件。

类为创建动画提供了基本的结构。通常你不需要直接使用这个类,由于它只提供了最小的功能,因而需要扩展它来完整地支持动画的值。下面的子类扩展了:

Table 1. Animators

描述

属性动画主要的计时引擎,它同样会为被动画化的属性计算其值。它具有计算动画值的所有核心功能,并且包含每个动画的计时细节,一个动画关于重复的信息,接收更新事件的listeners,并能够设置定制的类型来计算。要动画化属性有两个事情要做:计算动画化的值,把那些值设置给对象中被动画化的那个属性。不计算第二片,因此你必须监听计算出的值以更新,并修改你想要以你自己的逻辑动画化的对象。更多信息,请参考关于 的一节。

 的一个子类,它使你可以设置一个目标对象和要动画化的对象属性。这个类会在它为动画计算出了一个新值时依据计算出的值对属性进行更新。大多数时间你都会想要使用,因为它使得在目标对象上动画化值的过程变得简单得多。然而,有时你会想要直接使用 由于有着更多的限制,比如需要目标对象中具有特定的acessor方法

提供了一种机制来为动画分组,以使他们相对地运行。你可以设置动画一起播放,顺序播放,或在一个特定延迟之后播放。参考关于的一节来获取更多信息

Evaluators告诉属性动画系统如何为一个给定的属性计算其值。它们接收一个类提供的时序数据,动画的起始和结束值,然后基于这些数据计算动画化的属性值。属性动画系统提供了如下的evaluators:

Table 2. Evaluators

Class/Interface Description
计算int属性值的默认的evaluator。
计算float属性值的默认的evaluator。
计算由16进制值表示的色彩属性值的默认evaluator。

这是一个接口,允许你创建你自己的evaluator。如果你在动画化一个对象属性,它不是一个int,float或色彩,你必须实现接口来描述如何计算对象被动画化的属性值。你也可以为int,float和色彩值指定一个定制的,如果你想要处理这些类型而不是用默认的行为。请参考关于的一节来获取关于如何编写一个定制的evaluator的更多信息。

一个时间插值器将一个动画中的特定值定义为一个关于时间的函数。比如,你可以指定动画线性地穿过整个动画,即在整个时间段内动画平均地移动,或者你可以指定动画使用非线性的时间,比如,在开始处加速,在结束处减速。Table 3描述了在中包含的插值器。如果这些插值器都无法满足你的需要,你可以实现接口,并创建你自己的插值器。请参考来获取更多关于如何编写一个定制的插值器的信息。

Table 3. Interpolators

Class/Interface Description
一个interpolator,其变化频率在开始和结束处比较慢,在中间比较快。
一个interpolator,其变化频率开始时慢,然后加速。
一个interpolator,其变化频率,开始时后退然后前进。
An interpolator whose change starts backward, flings forward and overshoots the target value, then finally goes back to the final value.
An interpolator whose change bounces at the end.
An interpolator whose animation repeats for a specified number of cycles.
An interpolator whose rate of change starts out quickly and and then decelerates.
An interpolator whose rate of change is constant.
An interpolator whose change flings forward and overshoots the last value then comes back.
An interface that allows you to implement your own interpolator.

使用ValueAnimator来创建动画


通过指定一个动画通过的int,float或colors值的集合,让你能够为动画过程动画化一些类型的值。你可以通过调用它的一个工厂方法来获取一个:,,或。比如:

ValueAnimator animation = ValueAnimator.ofFloat(0f, 1f);animation.setDuration(1000);animation.start();

这段代码中,在start()方法执行时开始计算动画的值,0和1之间,持续时长为1000ms。

你也可以通过下面的做法指定要动画化的定制的类型:

ValueAnimator animation = ValueAnimator.ofObject(new MyTypeEvaluator(), startPropertyValue, endPropertyValue);animation.setDuration(1000);animation.start();

在这段代码中,当执行方法时,开始计算动画的值,使用由MyTypeEvaluator提供的逻辑,计算出的值在startPropertyValue和endPropertyValue之间,持续时长为1000ms。

然而,前面的代码片段,不会真的作用于一个对象上,因为不直接在对象或属性上操作。最可能的情况是你想要用这些计算出的值修改你要动画化的对象。你可以通过在中定义listeners来适当地处理动画的生命周期中的重要事件,比如帧更新,来做到这些。当实现listeners时,你可以通过调用来为特定的帧刷新获取计算出的值。更多关于listeners的信息,请参考关于的小节。

使用ObjectAnimator来创建动画


是(在上一节中讨论)的一个子类,并结合了的时间引擎和值计算,而能够动画化目标对象的一个命名属性。这使得动画化任何对象更简单,你不再需要实现,因为动画化的属性会自动地更新。

实例化一个与实例化一个类似,但在指定values to animate between的同时你也要指定对象和对象属性的名字(一个String):

ObjectAnimator anim = ObjectAnimator.ofFloat(foo, "alpha", 0f, 1f);anim.setDuration(1000);anim.start();

要使得正确地更新属性,你必须完成如下的这些:

  • 你要动画化的对象属性必须具有一个以set<propertyName>()的形式命名的setter函数(in camel case)。由于在动画期间自动地更新属性值,它必须能够通过这个setter方法访问属性。比如,如果属性的名字为foo,你需要有一个setFoo()方法。如果没有这个setter方法,你有三个选择:
    • 为类添加setter方法,如果你有权这么做的话。
    • 使用一个你有权修改wrapper类,并使得wrapper有一个有效的setter方法,接收值,并把值forward给原始的对象。
    • 使用
  • 如果在的工厂方法中的一个中,只为values...参数指定了一个值,那么假定这个值为动画的结束值。然后,你在动画化的对象属性必须有一个getter函数,以用于获取动画的起始值。这个getter方法必须以get<propertyName>()的形式命名。比如,如果属性名字为foo,你需要有一个getFoo()方法。
  • 你在动画化的属性的getter(如果需要)和setter方法必须作用于与你为指定的起始和结束值相同的类型上。比如,如果你构造了下面的,你就必须有targetObject.setPropName(float)和targetObject.getPropName(float):
    ObjectAnimator.ofFloat(targetObject, "propName", 1f)
  • 依赖于你在动画化的属性或对象,你可能需要调用一个View的方法来强制屏幕以更新后的动画值重绘。你可以在回调中做这些。比如,动画化一个Drawable对象的color属性,只有当那个对象重绘时才会导致更新到屏幕。View的所有的属性setters,比如 会适当地invalidate View,因而当你以新值调用这些方法时不需要invalidate View。更多关于listeners的信息,请参考关于的一节。

Choreographing Multiple Animations with AnimatorSet


在许多情况下,你想要播放一个依赖于另一个动画何时开始或结束的动画。Android系统使你可以把多个动画绑定在一起形成一个,以便于你可以指定是否并行地,顺序地,或一个动画完成之后有一个特定的延迟地启动动画。你也可以让对象之间相互嵌套。

下面的代码取自示例(做了一点点修改以简化问题),以下面的方式播放下面的对象:

  1. 播放 bounceAnim.
  2. 同时播放squashAnim1squashAnim2stretchAnim1stretchAnim2
  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();

关于如何使用动画集合的更复杂的例子,请参考API Demos中例子。

Animation Listeners


你可以通过下述的listeners监听动画执行期间的重要事件。

    •  - 当动画启动时调用。
    •  - 当动画结束时调用。
    •  - 当动画重复时调用。
    •  - 当动画被取消时调用。一个取消的动画也会调用,而无论它是如何被结束的。
    •  - 动画的每一帧上会被调到。在动画期间监听这个事件以使用由产生的计算值。要使用该值,则通过方法查询传入的对象来获取当前的动画值。如果你使用了,则需要实现这个listener。

      依赖于你在动画化的属性或对象,你可能需要调用一个View的方法来强制屏幕以更新后的动画值重绘。你可以在回调中做这些。比如,动画化一个Drawable对象的color属性,只有当那个对象重绘时才会导致更新到屏幕。View的所有的属性setters,比如 会适当地invalidate View,因而当你以新值调用这些方法时不需要invalidate View。

你可以扩展类,而不是实现接口,如果你不想实现所有的接口方法的话。类提供了这些方法的空实现,你可以覆写它们。

比如,API demos中的示例创建了一个,只重新实现了回调:

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

动画化ViewGroups的布局变化


属性动画系统提供了一些功能来将ViewGroup对象的变化做成动画,和动画化View对象一样简单。

你可以通过类来将ViewGroup内的变化做成动画。一个ViewGroup中的Views可以执行一个出现或消失动画,当你将它们加入或将它们移除出ViewGroup或当你以, android.view.View#INVISIBLE},或调用一个View的时。当你添加或移除Views时,ViewGroup中其余的Views也可以执行一个动画到它们的新位置。你可以通过调用并传入 对象及如下的常量中的一个来在一个里定义下面的动画:

  • APPEARING - 一个标记,表示动画在items出现在容器中时执行。
  • CHANGE_APPEARING - 一个标记,表示动画在items由于一个新的items出现在容器中而发生改变时执行。
  • DISAPPEARING - 一个标记,表示动画在items由容器中消失时执行。
  • CHANGE_DISAPPEARING - 一个标记,表示动画在items由于一个item从容器中消失而发生改变时执行。

你可以为这四种类型的事件定义你自己的定制动画来定制你的layout transitions的外观,或者只是告诉动画系统使用默认的动画。

API Demos中的示例向你展示了如何为layout transitions定义动画,并给你想要动画化的View对象设置动画。

及它的对应的 layout资源文件向你展示了如何为XML中的ViewGroups启用默认的layout transitions。你所需要做的仅有的事情就是将ViewGroup的android:animateLayoutchanges属性设置为true。比如:

设置这个属性为true会自动地将所有添加或移除出ViewGroup的Views及ViewGroup中其余的Views动画化。

使用一个TypeEvaluator


如果你想要动画化一个Android系统未知的类型,你可以通过实现接口创建你自己的evaluator。Android系统直接支持的类型是int,float,或一个color,它们通过,和类型evaluators来支持。

接口中只需要实现一个方法,即方法。这使得你可以在动画的当前点为你所动画化的属性返回一个你所使用动画的适当值。类演示了如何做到这一点:

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);    }}
注意:当(或)执行时,他计算一个动画当前的运行部分(一个0和1之间的值),然后计算一个依赖于你所使用的插值器的插值后的版本。插值部分即是接收到的fraction参数,因而当你计算动画的值时不需要考虑插值器。

使用插值器


一个插值器定义了一个动画中的特定的值是如何由一个关于时间的函数计算出来的。比如,你可以指定动画线性地发生在整个动画期间,意味着动画在整个时间段内均匀地移动,或者你可以指定动画使用非线性的时间,比如,在动画的开始或结束处使用加速或减速。

动画系统中的插值器由Animators接收一个代表着动画已进行时间的小数。插值器修改这个小数以与它所运用的动画的类型一致。在中Android系统提供了一系列通用的插值器。如果这些组件都不能满足你的需求,你可以实现接口来实现你自己的。

作为一个示例,下面对比了默认的插值器和是如何计算插值的小数的。不改变逝去时间小数。则在进入动画时加速,退出动画时减速。下面的方法为这些插值器定义了逻辑:

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的近似的值:
ms elapsed Elapsed fraction/Interpolated fraction (Linear) Interpolated fraction (Accelerate/Decelerate)
0 0 0
200 .2 .1
400 .4 .345
600 .6 .8
800 .8 .9
1000 1 1
如表所示,以相同的速度改变值,每过200ms改变.2。而,在200ms到600ms之间,改变值的速度比快,在600ms到1000ms之间,则更慢一些。

指定关键帧


一个对象由一个时间/值对组成,以使你能够定义一个动画在一个特定时间点的一个特定状态。每一个keyframe也可以有它自己的插值器来控制前面的keyframe的时间到这个keyframe的时间之间动画的行为。

要实例化一个对象,你必须是有你哦个工厂方法中的一个, ,,或来获取适当类型的。然后你调用工厂方法来获取一个对象。一旦你获取了这个对象,你可以通过把对象和要动画化的对象传入来获取一个动画。下面的代码片段演示了如何做到这一点:

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);

.关于如何使用keyframe的一个更复杂的例子,请参考APIDemos中的示例。

Views动画


属性动画系统可以简化View对象的动画,相对于view动画系统还提供了一些优势。View动画系统通过改变view绘制的方式来改变View对象。这由每个View的容器来处理,由于View本身没有属性来控制。这导致了,虽然View被动画化了,但View本身的属性不发生改变。这导致了一些行为,比如一个对象仍然出现在它的原始位置,即使它被画在了屏幕上的一个不同的位置。在Android 3.0中,添加了新的属性和对应的getter和setter方法来消除这种缺点。

属性动画系统可以通过改变View对象中的实际的属性来动画化屏幕上的Views。此外,Views也会自动地调用方法来刷新屏幕,而无论何时它的属性被改变。View类中使得属性动画更容易的新属性有:

  • translationX和translationY:这些属性以相对于它的layout容器为其设置的坐标系的左上角的距离而定义的位置来控制View的位置。
  • rotation,rotationX和rotationY:这些属性控制2D(rotation 属性)和3D中关于轴心点的rotation。
  • scaleX和scaleY:T这些属性控制一个View关于它的轴心点的2D缩放比例。
  • pivotX和pivotY:这些属性控制轴心点的位置,rotation和放缩比例都据此来定义。默认情况下,轴心点位于对象的中心点。
  • x和y:这些实用属性用于描述View在它的容器中的最终位置,是left和top与translationX和translationY值的和。
  • alpha:表示View中的alpha透明度。默认情况下这个值为1(不透明),值为0表示完全透明(不可见)。

要动画化一个View对象的一个属性,比如它的color或rotation值,你所需做的全部就是创建一个属性动画,并指定你想要动画化的View属性。比如:

ObjectAnimator.ofFloat(myView, "rotation", 0f, 360f);

更多关于创建动画的信息,请参考animating with  and 的部分。

使用ViewPropertyAnimator来创建动画

提供了一种简单的方式来并行地动画化一个的一些属性,使用一个underlying 对象。它的行为非常像一个,因为它改变view属性的实际值,但当一次动画化许多属性时它更高效。此外,使用的代码要简洁和易读得多。下面的代码片段显示了使用多个对象,一个单独的,和在并发地动画化一个view的x和y属性时的差异。

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);PropertyValuesHolder pvhY = PropertyValuesHolder.ofFloat("y", 100f);ObjectAnimator.ofPropertyValuesHolder(myView, pvhX, pvyY).start();
ViewPropertyAnimator
myView.animate().x(50f).y(100f);

关于的更详细的信息,请参考对应的Android Developers 。

在XML中声明动画


属性动画系统使你可以用XML来声明属性动画,而不是编程来做。通过在XML中定义你的动画你可以简单地在多个activities中重用你的动画,并更简单地编辑动画序列。

为了区别使用了新的属性动画APIs的动画文件和使用了传统的框剪的动画文件,自Android 3.1起,你应当将属性动画的XML文件保存在res/animator/目录下(而不是res/anim/下)。使用animator目录名是可选的,但如果你想在Eclipse ADT plugin (ADT 11.0.0+)中使用layout编辑器工具则是必须的,因为ADT只为属性动画resource搜索res/animator/目录。

下面的属性动画类通过如下的XML标签来提供XML声明支持:

  •  - <animator>
  •  - <objectAnimator>
  •  - <set>

下面的例子顺序地播放两个对象动画的集合,其中嵌套的第一个集合同时播放两个对象动画:

要运行这个动画,你必须在你的代码中将XML resource inflate为一个对象,然后在启动动画集合前为所有的动画设置目标对象。调用为所有的children设置一个单独的目标对象。下面的代码显示了如何完成这一点:

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

关于定义属性动画的XML的语法的更多信息,请参考。

原文链接:http://developer.android.com/guide/topics/graphics/prop-animation.html。

Done.

转载于:https://my.oschina.net/wolfcs/blog/296512

你可能感兴趣的文章
MySQLdump常用命令
查看>>
如何才能正确的关闭Socket连接
查看>>
MongoDB基本操作
查看>>
[转]微擎(微赞)学习之 -- 模块开发:目录结构
查看>>
css 手机适配
查看>>
5个界面效果很炫的JavaScript UI框架
查看>>
根据标准word模板生成word文档类库(开源)
查看>>
Html网页表格结构化标记的应用
查看>>
数据结构和算法 (二)数据结构基础、线性表、栈和队列、数组和字符串
查看>>
二叉树的层序遍历算法实现
查看>>
Measuring Power Values
查看>>
wince6下载地址
查看>>
UIView+LHQExtension(分类)
查看>>
KiB、MiB与KB、MB的区别
查看>>
Java开发环境配置
查看>>
ASP.NET MVC实现多个按钮提交事件
查看>>
移动端与PHP服务端接口通信流程设计(增强版)
查看>>
Linux 下模拟Http 的get or post请求(curl和wget两种方法)
查看>>
Windows去除快捷箭头
查看>>
关于分页的解决方案收集
查看>>