「译」Android Animation in Honeycomb by Chet Haase(3.0中的动画机制)_html/css_WEB-ITnose
原文 2011-02-24 Animation in Honeycomb 发表于 2011.02(和3.0发布的时间同步),作者是 Chet Haase,一个致力于图形和动画研究的 Android 开发者,可以从他的 个人博客graphics-geek.blogspot.com 阅读更多相关主题的博文。
译者说:
这篇文章讨论了一个问题,已经有了能实现 move, scale, rotate, and fade 这些视图动画的 android.view.animation,为什么还要在 3.0 引入新 APIs android.animation? 新 APIs 带来了哪些新特性? 然后进一步展示了这些新特性的强大便利之处。虽然是11年2月发布的,而现在已经是2016年的1月底了,虽然步子慢了五年,但3.x的动画依然没有过时,这篇文章风采依旧,非常值得初学者(我)学习。
本文非全文翻译,亦非逐字逐句翻译,作为我自己的学习笔记,可谓是「总结式翻译」,梳理、备忘。
强烈建议阅读原文,文档和代码要结合阅读,以相互佐证和理解,同时运行 ApiDemos 中相关的示例,观察动画效果,修改参数再观察动画效果。系列文章:
如何学习 Android Animation?
从 Android Sample ApiDemos 中学习 android.animation API 的用法
Android Animation Interpolator - Android 动画插值器分析
weiyi.li li2.me 2016-01-28 ~ 2016-02-01
Honeycomb 引入的新特性之一是全新的动画系统 android.animation,它比以前更容易实现对象动画(objects animation)和属性动画(properties animation)。
Honeycomb之前的动画
Honeycomb 之前,动画由 android.view.animation 实现,比如
视图的移动 move、缩放 scale、旋转 rotate、渐变 fade;
通过 AnimationSet 组合安排多个动画;
把动画指定给 LayoutAnimationController,当容器排列子视图时,会自动错开所有子视图动画的开始时间(文末有注释);
使用 Interpolator(插值器,用来定义动画改变的速率,文末有注释),比如 AccelerateInterpolator 和 BounceInterpolator,使动画不再匀速改变,从而显得更自然。
如上述,honeycomb 之前可以实现视图动画,但也仅止于此,因为 honeycomb 之前动画只能操作视图对象(View Objects),缺乏一些关键功能的支持,对诸如 Drawable 的位置、背景色等视图属性无能为力。
之前的动画仅仅改变目标视图的视觉效果(看起来的样子),而不会改变视图对象的属性。比如通过动画 TranslateAnimation 和 setFillAfter(true) 改变按钮的位置,动画仅仅在新的位置重绘按钮,而无法改变按钮在父视图中的位置。也就是说在新位置点击按钮无效。
基于上述(还有其它没有提到的)原因,Honeycomb 提供了全新的动画机制,新机制建立于属性动画(property animation)的概念之上。
Honeycomb中的属性动画
新动画机制不仅仅针对视图对象,也不仅仅针对对象的某些属性,也不局限于视觉效果。事实上,它所有的一切都是关于一段时间后值的变化,并把这些变化的值设置给任何目标对象和属性。
因此你可以完成很多事情,诸如:移动或者渐变视图;移动视图内的 Drawable;动态地改变 Drawable 的背景色;甚至动态地改变任何数据类型的值,只需要告诉新机制:动画时长、自定义类型的动画过程值的计算方法、动画的起止值,新机制就可以计算动画过程值并设置给目标对象和属性。
所以,通过新机制移动按钮是真的移动了按钮的位置。
接下来我会简单地介绍新机制中的几个关键类,适当时给出示例代码。想了解新机制工作的更多细节,就去研读 SDK sample ApiDemos.(译注:参考我这篇文章 从 Android Sample ApiDemos 中学习 android.animation API 的用法)
Animator
Animator 是新动画机制中的超类。子类 ValueAnimator 是核心计时引擎,子类 AnimatorSet 用来把多个动画编排成一个动画。一般不会直接使用 Animator,但它的一些属性和方法为子类所共有,诸如持续时间duration、开始延迟startDelay、监听器listener.
当你想在动画结束时执行一些操作,监听器就显得很重要了。为了监听动画的生命周期事件,需要实现接口 AnimatorListener 并注册给 animator。比如,
anim.addListener(new Animator.AnimatorListener() { public void onAnimationStart(Animator animation) {} public void onAnimationEnd(Animator animation) { // do something when the animation is done } public void onAnimationCancel(Animator animation) {} public void onAnimationRepeat(Animator animation) {} });
当然,考虑到你可能只需要监听某一个事件,意味着接口的其它方法不需要覆写,却占着空间可能会让「代码简洁控」抓狂,所以新机制提供了适配器类 AnimatorListenerAdapter,让你只需覆写关心的方法:
anim.addListener(new AnimatorListenerAdapter() { public void onAnimationEnd(Animator animation) { // do something when the animation is done } });
ValueAnimator
ValueAnimator 是整个新机制的「主力」。它运行的内部计时循环(timing loop 文末有注释),使程序内的所有动画,在每次计时脉冲(timing pulse 文末有注释)发生时,根据当前的时间计算动画过程值,并设置给目标对象和属性。
它拥有的一些核心特性可以帮助它做到这一点:
知道每一个动画的计时详情(诸如起/止时间、持续时间、当前执行了多少时间);
知道动画是否重复;
当前动画过程值算出来后,回调注册给它的监听器(AnimatorUpdateListener);
拥有计算不同类型值的能力(TypeEvaluator)。
属性动画包含两个步骤:计算动画过程值,然后把这些值设置给目标对象和属性。ObjectAnimator(稍后讲) 直接继承自 ValueAnimator,由它实现属性动画较为容易。但以下几种情况使用 ValueAnimator 反而更方便:
当对象没有提供某个属性的 setter 方法时;(译注:有疑惑不要紧,稍后讲到 ObjectAnimator 时就知道原因了。)
当你仅有一个动画,并想把动画过程值设置给多个属性时;
或者仅仅是想使用一个简单的计时机制;
怎么使用 ValueAnimator 呢?比如,在 500ms 内从 0 变到 1:
ValueAnimator anim = ValueAnimator.ofFloat(0f, 1f); anim.setDuration(500); anim.start();
如上述,新的动画机制不局限于视觉效果,而所有的一切都是关于一段时间后值的变化。那么通过什么方式才能知道动画是否发生了呢?——答案是监听器:实现一个监听器 AnimatorUpdateListener 并注册给 ValueAnimator 实例,就可以在每一个动画帧(animation frame 文末有注释)被回调到,从而获取当前的动画值:
anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { public void onAnimationUpdate(ValueAnimator animation) { Float value = (Float) animation.getAnimatedValue(); // do something with value... } });
除了浮点数,还可以给其它类型的值添加动画,比如整数:
ValueAnimator anim = ValueAnimator.ofInt(0, 100);
XML 文件也可以实现相同的动画:
<animator xmlns:android="http://schemas.android.com/apk/res/android" android:valueFrom="0" android:valueTo="100" android:valueType="intType"/>
新机制知道如何计算整数和浮点数的动画过程值,这是因为其内置了相关的计算方法。而对于其它类型的数据,比如 Point, Rect, 甚至自定义数据,新机制不知如何计算,但通过 TypeEvaluator(稍后讲),转手就把计算动画过程值的责任交给了你:
Point p0 = new Point(0, 0); Point p1 = new Point(100, 200); ValueAnimator anim = ValueAnimator.ofObject(pointEvaluator, p0, p1);
除了动画时长外,还可以设置的动画参数有:
setStartDelay(long):播放延迟的时间;
setRepeatCount(int):播放重复的次数,0(默认值)不重复,正整数或者 Animation.INFINITE;
setRepeatMode(int):播放重复的模式,Animation.REVERSE 从结束位置重复,Animation.RESTART 从开始位置重复;
setInterpolator(TimeInterpolator):设置插值器,用来定义动画改变的速率。TimeInterpolator 是 Interpolator 的父接口,因此这里也可以使用 Interpolator 相关的实现。
ObjectAnimator
ObjectAnimator 继承自 ValueAnimator。除了可以设置动画的时间参数和值参数外(这些 ValueAnimator 能做到的),它还可以设置目标对象和属性。比如,如果想渐隐某个对象 myObject,可以对属性 alpha 施加动画:
ObjectAnimator.ofFloat(myObject, "alpha", 0f).start();
这个例子中,虽然只设置了动画的终点值,但起始值默认是属性的当前值。
XML 文件也可以定义相同的动画:
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android" android:valueTo="0" android:propertyName="alpha"/>
但不能在 xml 中设置目标对象,只能在加载动画资源后,由代码设置:
ObjectAnimator anim = AnimatorInflator.loadAnimator(context, resID); anim.setTarget(myObject); anim.start();
在使用 ObjectAnimator 之前,必须要理解一个隐含的假定,关乎属性及其 set/get 方法。这个假定是说:
当对某个对象的某个属性施加动画时,动画机制假定这个对象具有和该属性名相关的 public set 方法,并且属性具有合适的数据类型。还有,在构造 ObjectAnimator 时如果只设置了终点值,比如上述例子,动画机制必须要知道属性的当前值,因此,该对象也应该具有相应的 public get 方法以返回合适类型的数据。
因此,上述关于 alpha 的示例代码若想执行成功,对象 myObject 必须要有两个 public 方法:
public void setAlpha(float value); public float getAlpha();
简言之,某个对象若想为它的某个属性使用 ObjectAnimator,该对象必须满足动画机制「没有明说」的一个条件:提供该属性的 public set/get 方法。
特别说明:set/get 方法在 animation 运行时调用,因此它们和动画之间是非常弱的关系。而如果你的程序中又没有任何地方显示地调用这些 set/get 方法,而你恰好又使用了 ProGuard(代码混淆工具)或者其它代码优化工具(code stripping 代码剥离),那么这些工具便不会知道 set/get 在运行时被调用,很有可能就被剥离掉了。所以当你使用这些工具时,你有义务确保这些 set/get 方法的存在。
View properties
敏锐的读者可能已经发现了新动画机制的「瑕疵」:围绕属性做文章的新机制,如何处理那些连一个 public set/get 属性方法都没有的视图对象呢?
好问题!继续读下去。
类 View(查阅文档中 Animation 相关的说明) 在 Honeycomb 中得到了升级,增添了很多新属性,可以通过 set/get 方法访问这些属性,使得新动画机制可以应用于视图对象:
translationX 和 translationY
rotation, rotationX, 和 rotationY
scaleX 和 scaleY
pivotX 和 pivotY
x 和 y
alpha
AnimatorSet
AnimatorSet 用来把多个动画编排成一个动画(作用类似于3.0以前的 AnimationSet)。比如你想编排一个这样的动画:先渐隐一个视图,结束后从侧边滑入另一个视图,滑入的同时渐显出来。为了实现这样的编排,首先需要拆分成几个独立的动画,接下来就有好几种选择了:(1) 在正确的时间手动播放对应的动画,或者给每个动画设置合适的播放延迟,总之你需要显示地、主动地设置合适的时间;(2) 如果觉得时间不好掌控,或者嫌麻烦,就使用 AnimatorSet,它提供的 APIs 使这些变得很简单:
同时播放:playTogether(Animator...);
顺序播放:playSequentially(Animator...);
通过 AnimatorSet.Builder 设置动画间的相对关系:with(), before(), after();
play(Animator) 会创建一个 Builder;
因此上述动画可以这样实现:
ObjectAnimator fadeOut = ObjectAnimator.ofFloat(v1, "alpha", 0f); ObjectAnimator mover = ObjectAnimator.ofFloat(v2, "translationX", -500f, 0f); ObjectAnimator fadeIn = ObjectAnimator.ofFloat(v2, "alpha", 0f, 1f); AnimatorSet animSet = new AnimatorSet().play(mover).with(fadeIn).after(fadeOut);; animSet.start();
也可以在 XML 文件中实现上述动画。
TypeEvaluator
上述 ValueAnimator 一节有说过一段话:「新机制知道如何计算整数和浮点数的动画过程值,这是因为其内置了相关的计算方法。而对于其它类型的数据,比如 Point, Rect, 甚至自定义数据,新机制不知如何计算,但通过 TypeEvaluator,转手就把计算动画过程值的责任交给了你。」
而 TypeEvaluator 是只定义了一个方法的接口:
public interface TypeEvaluator<T> { public T evaluate(float fraction, T startValue, T endValue);}
内置的处理浮点数的 FloatEvaluator 继承了该接口,而它的方法仅仅是 y = kx + b 的实现,非常简单:
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); } }
那么这个方法是如何被调用到的呢?
// ValueAnimator.class void animateValue(float fraction) { // 篡改播放进度 turns the elapsed fraction into an interpolated fraction. fraction = mInterpolator.getInterpolation(fraction); for (int i = 0; i < numValues; ++i) { // 根据篡改后的进度计算动画过程值 turn the interpolated fraction into an animated value. // 继续追代码,会追到 PropertyValuesHolder 和 Keyframes 两个类,深究不下去了 orz TODO mValues[i].calculateValue(fraction); } }
如果不是浮点数和整数,或者内置的处理整数的 IntEvaluator 和处理浮点数的 FloatEvaluator 不能满足你的要求,则必须显示地设置 TypeEvaluator,可以调用 setEvaluator(TypeEvaluator) 或者通过构造器 ValueAnimator.ofObject(TypeEvaluator, Object...),比如计算 Point 的动画过程值:
public class PointEvaluator implements TypeEvaluator { public Object evaluate(float fraction, Object startValue, Object endValue) { Point startPoint = (Point) startValue; Point endPoint = (Point) endValue; return new Point(startPoint.x + fraction * (endPoint.x - startPoint.x), startPoint.y + fraction * (endPoint.y - startPoint.y)); } }
现在使用它把在 ValueAnimator 这节内容中提到的例子补充完整:
Point p0 = new Point(0, 0); Point p1 = new Point(100, 200); ValueAnimator anim = ValueAnimator.ofObject(new PointEvaluator(), p0, p1);
译注:这篇文章发布于 2011.02,针对于 API level 11 的 3.0 系统,此后 L18 加入了 RectEvaluator, L21 加入了 PointFEvaluator、IntArrayEvaluator、FloatArrayEvaluator。
所以上述几次提到的「不知道如何计算 Rect 的动画过程值」也并不是错误的,要特别注意技术文章的时效,查阅最新的文档和代码,以相互佐证。
查阅 TypeEvaluator Known Indirect Subclasses。
BUT WAIT, THERE'S MORE!
还有非常多的新特性可以说,但是限于文章篇幅和时间,这里就不再继续下去了。现在你应该和 ApiDemos「耍一耍」,潜心研究代码。
动画重复的一些特性;
动画生命周期事件的监听器;
在两个值以上做动画;
使用 Keyframe 定制更复杂的时值序列(time/value);
使用 PropertyValuesHolder 指定多个属性并行动画;
使用 LayoutTransition 定制简单的布局动画;
......
译注:从代码角度理解一些特定的英文词组
关于 animation system
动画机制,文中大多简称「新机制」,是指 3.0 引入的 android.animation。感觉此处译作「动画系统」不太合适,因为简称做系统时,很容易和 Android 系统混淆。
所以「新机制」在本文的语境下特指 android.animation.
关于 timing loop
计时循环。
android.animation.ValueAnimator 定义了一个静态内部类 AnimationHandler,它实现了 Runnable 以维护计时循环,从而产生计时脉冲(timing pulse),动画之所以能「动」,就是计时脉冲在起作用,可认为是动画的「心脏」,功能类似于单片机的晶振。
每次计时脉冲,每一个动画都会根据动画的当前时间、Interpolator 和 TypeEvaluator,计算出动画的过程值。
更多内容,查阅 http://li2.me/2016/01/android-animation-interpolator.html
关于 animation frame
animation frame,at each frame,on each frame
动画帧(这里肯定不能把 frame 当做框架来理解)。
上述讲到 timing loop 时提到了计时脉冲,每次计时脉冲都会计算出一个新的动画值。有了新值,就意味着动画发生了变化,所以,可以理解为一帧一帧的动画。
关于 animated values
动画过程值;动画中间值。
属性动画的本质是「值的变化」,
通过构造器(或者 xml)指定动画的起止值;
Interpolator 表征了「时间」的变化规律;
TypeEvaluator 表征了「值」的变化规律;(「值」即对象的物质属性,或者是透明度,或者是背景色,或者是运动轨迹)
那么在整个动画的生命周期中,每个动画帧都会计算出一个值,这些值就是「animated values」,因此对象的「时」「空」就发生了变化,因此就有了动画。
Interpolator 和 TypeEvaluator 的区别和联系
Interpolator 插值器:把处于某个区间(有起点值和终点值)的一个值,按照某种算法,映射为另一个值。
从上述定义的角度来看,Interpolator 和 TypeEvaluator 的作用是一样的。
非要说区别的话,应该是新的动画机制对它俩的「职责」做了定性,从名字也能看出来端倪:一个管 time value(Interpolator 实现了 TimeInterpolator的接口),一个管 type value。
具体到各自的方法:
Interpolator 的 float getInterpolation(float fraction) 虽然只有一个入口参数,是因为它的调用者已经把 @input 限定为 [0, 1.0]。而这个方法的作用是把动画的当前进度映射成另一个值,可谓是「篡改」,因此函数的名字「interpolation 篡改」起的非常合适。
而 TypeEvaluator 的 T evaluate(float fraction, T startValue, T endValue) 则是根据动画的初值、终值,和被篡改的播放进度,计算动画过程值,因此函数的名字「evaluate 评估」也是非常合适。
因此,从数学意义讲,二者有共性;从物理意义讲,二者有区别。
更多内容,查阅 http://li2.me/2016/01/android-animation-interpolator.html
关于 staggered animation
automatically staggered animation start times:自动错开动画的开始时间。
staggered adj. 错列的;吃惊的。 LayoutAnimationController 用于实现容器布局动画,容器内的子视图动画相同,但开始时间不同。所以这里的 staggered 应是「错开的」意思。
其它一些单词和词组
usher in 领进,引进。
choreograph 设计舞蹈动作;为...编舞。
choreograph multiple animations 为动画编舞;把多个动画优雅地编排成一个。(译注:真要好好学习动画,不辜负类名 Choreograph)。
play with the API demos 和...玩耍。可以 play with 什么东西,小朋友之间也可以 play with,但是成人之间不要 play with 噢 (~﹃~)~zZ。
这里有一个歪果仁特意提到了: 中国人总是用错“play”这个单词,哈哈哈,我来解释一下。
文中提到的一个哲学问题
If a tree falls in a forest
相关博文
Android属性动画完全解析(上),初识属性动画的基本用法 -- 郭霖的CSDN专栏
Android属性动画完全解析(中),初识属性动画的基本用法 -- 郭霖的CSDN专栏
Honeycomb 中引入的新 Animation —— Property Animation (这是全文翻译)
by
weiyi.li li2.me weiyi.just2@gmail.com
2016-01-28 ~ 2016-02-01
禁止转载
不把阅读英文文档当做翻译事业的程序员不是好厨子

HTML 코드는 온라인 유효성 검사기, 통합 도구 및 자동화 된 프로세스를 통해 깨끗할 수 있습니다. 1) w3cmarkupvalidationservice를 사용하여 온라인으로 HTML 코드를 확인하십시오. 2) 실시간 확인을 위해 VisualStudioCode에 HTMLHINT 확장을 설치하고 구성하십시오. 3) htmltidy를 사용하여 시공 프로세스에서 HTML 파일을 자동으로 확인하고 청소하십시오.

HTML, CSS 및 JavaScript는 최신 웹 페이지를 구축하기위한 핵심 기술입니다. 1. HTML 웹 페이지 구조를 정의합니다. 2. CSS는 웹 페이지의 모양을 담당합니다.

HTML의 기능은 웹 페이지의 구조와 내용을 정의하는 것이며, 그 목적은 정보를 표시하는 표준화 된 방법을 제공하는 것입니다. 1) HTML은 타이틀 및 단락과 같은 태그 및 속성을 통해 웹 페이지의 다양한 부분을 구성합니다. 2) 콘텐츠 및 성능 분리를 지원하고 유지 보수 효율성을 향상시킵니다. 3) HTML은 확장 가능하므로 사용자 정의 태그가 SEO를 향상시킬 수 있습니다.

HTML의 미래 트렌드는 의미론 및 웹 구성 요소이며 CSS의 미래 트렌드는 CSS-In-JS 및 CSShoudini이며, JavaScript의 미래 트렌드는 WebAssembly 및 서버리스입니다. 1. HTML 시맨틱은 접근성과 SEO 효과를 향상시키고 웹 구성 요소는 개발 효율성을 향상 시키지만 브라우저 호환성에주의를 기울여야합니다. 2. CSS-in-JS는 스타일 관리 유연성을 향상 시키지만 파일 크기를 증가시킬 수 있습니다. CSShoudini는 CSS 렌더링의 직접 작동을 허용합니다. 3. Webosembly는 브라우저 애플리케이션 성능을 최적화하지만 가파른 학습 곡선을 가지고 있으며 서버리스는 개발을 단순화하지만 콜드 스타트 문제의 최적화가 필요합니다.

웹 개발에서 HTML, CSS 및 JavaScript의 역할은 다음과 같습니다. 1. HTML은 웹 페이지 구조를 정의하고, 2. CSS는 웹 페이지 스타일을 제어하고 3. JavaScript는 동적 동작을 추가합니다. 그들은 함께 현대 웹 사이트의 프레임 워크, 미학 및 상호 작용을 구축합니다.

HTML의 미래는 무한한 가능성으로 가득합니다. 1) 새로운 기능과 표준에는 더 많은 의미 론적 태그와 WebComponents의 인기가 포함됩니다. 2) 웹 디자인 트렌드는 반응적이고 접근 가능한 디자인을 향해 계속 발전 할 것입니다. 3) 성능 최적화는 반응 형 이미지 로딩 및 게으른로드 기술을 통해 사용자 경험을 향상시킬 것입니다.

웹 개발에서 HTML, CSS 및 JavaScript의 역할은 다음과 같습니다. HTML은 컨텐츠 구조를 담당하고 CSS는 스타일을 담당하며 JavaScript는 동적 동작을 담당합니다. 1. HTML은 태그를 통해 웹 페이지 구조와 컨텐츠를 정의하여 의미를 보장합니다. 2. CSS는 선택기와 속성을 통해 웹 페이지 스타일을 제어하여 아름답고 읽기 쉽게 만듭니다. 3. JavaScript는 스크립트를 통해 웹 페이지 동작을 제어하여 동적 및 대화식 기능을 달성합니다.

Htmlisnotaprogramminglanguage; itisamarkuplanguage.1) htmlstructuresandformatswebcontentusingtags.2) itworksporstylingandjavaScriptOfforIncincivity, WebDevelopment 향상.


핫 AI 도구

Undresser.AI Undress
사실적인 누드 사진을 만들기 위한 AI 기반 앱

AI Clothes Remover
사진에서 옷을 제거하는 온라인 AI 도구입니다.

Undress AI Tool
무료로 이미지를 벗다

Clothoff.io
AI 옷 제거제

Video Face Swap
완전히 무료인 AI 얼굴 교환 도구를 사용하여 모든 비디오의 얼굴을 쉽게 바꾸세요!

인기 기사

뜨거운 도구

VSCode Windows 64비트 다운로드
Microsoft에서 출시한 강력한 무료 IDE 편집기

ZendStudio 13.5.1 맥
강력한 PHP 통합 개발 환경

맨티스BT
Mantis는 제품 결함 추적을 돕기 위해 설계된 배포하기 쉬운 웹 기반 결함 추적 도구입니다. PHP, MySQL 및 웹 서버가 필요합니다. 데모 및 호스팅 서비스를 확인해 보세요.

메모장++7.3.1
사용하기 쉬운 무료 코드 편집기

mPDF
mPDF는 UTF-8로 인코딩된 HTML에서 PDF 파일을 생성할 수 있는 PHP 라이브러리입니다. 원저자인 Ian Back은 자신의 웹 사이트에서 "즉시" PDF 파일을 출력하고 다양한 언어를 처리하기 위해 mPDF를 작성했습니다. HTML2FPDF와 같은 원본 스크립트보다 유니코드 글꼴을 사용할 때 속도가 느리고 더 큰 파일을 생성하지만 CSS 스타일 등을 지원하고 많은 개선 사항이 있습니다. RTL(아랍어, 히브리어), CJK(중국어, 일본어, 한국어)를 포함한 거의 모든 언어를 지원합니다. 중첩된 블록 수준 요소(예: P, DIV)를 지원합니다.
