Android 动画机制与使用技巧
- 动画效果一直是人机交互中非常重要的部分,与死板、突兀的显示效果不同,动画效果的加入,让交互变得更加友好,特别是在提示、引导类的场景中,合理地使用动画能让用户获得更加愉悦的使用体验
一、Android View动画框架
- Animation框架定义了透明度、旋转、缩放、位移等几种常见的动画
- 实现原理:
- 每次绘制View时,ViewGroup中的drawChild函数获取该view的Animation的Transformation值,然后调用canvas.concat(transformToApply.getMatrix())
- 通过矩阵运算完成帧动画,如果动画没有完成,就继续调用invalidate() 函数,启动下次绘制来驱动动画,从而完成整个动画的绘制。
二、帧动画
- 帧动画就是一张张图片不同的切换,形成的动画效果。
- 一般手机的开机动画,应用的等待动画等都是帧动画,因为只需要几张图片轮播,极其节省资源,如果真的设计成动画,那么是很耗费资源的事。
- 在res目录下新建一个drawable文件夹并定义xml文件,子节点为 animation-list,在这里定义要显示的图片和每张图片的显示时长。
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
android:oneshot="false"><!-- false表示循环播放,true表示只播放一次 -->
<item android:drawable="@drawable/g1" android:duration="200" />
<item android:drawable="@drawable/g2" android:duration="200" />
<item android:drawable="@drawable/g3" android:duration="200" />
<item android:drawable="@drawable/g4" android:duration="200" />
<item android:drawable="@drawable/g5" android:duration="200" />
<item android:drawable="@drawable/g6" android:duration="300" />
<item android:drawable="@drawable/g7" android:duration="400" /><!-- 慢动作 -->
<item android:drawable="@drawable/g8" android:duration="500" />
<item android:drawable="@drawable/g9" android:duration="200" />
<item android:drawable="@drawable/g10" android:duration="200" />
<item android:drawable="@drawable/g11" android:duration="200" />
</animation-list>
- 在屏幕上播放帧动画,需要布局文件有一个ImageView来显示动画图片
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ImageView iv = (ImageView) findViewById(R.id.iv);
//把动画文件设置为imageView的背景
iv.setBackgroundResource(R.drawable.frameanimation);
AnimationDrawable ad = (AnimationDrawable) iv.getBackground();
//播放动画
ad.start();
}
}
三、补间动画(视图动画)
- 组件由原始状态向终极状态转变时,为了让过渡更自然,而自动生成的动画叫做补间动画。
- 主要是在Android 3.0之前
- 最大的缺陷就是不具备交互性
- 位移、旋转、缩放、透明
public class MainActivity extends Activity {
private ImageView iv;
private TranslateAnimation ta;
private RotateAnimation ra;
private ScaleAnimation sa;
private AlphaAnimation aa;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
iv = (ImageView) findViewById(R.id.iv);
}
//位移动画
public void translate(View v){
//创建位移动画的对象,设置动画的初始位置和结束位置
//10,100表示,从imageview的真实坐标的左上角 x+10,移动到 x+100的位置
//20,200表示,从imageview的真实坐标的左上角 y+20,移动到 y+200的位置
//ta = new TranslateAnimation(10, 100, 20, 200);
//Animation.RELATIVE_TO_SELF相对于自己
//对于x,表示,相对于自己,起点是 imageview的真是坐标的左上角 x+ 0.5*iv的宽度 到 x+ 2*iv的宽度
//y 就是乘以 iv 的高度,也可以相对于父控件,但是用的比较少
ta = new TranslateAnimation(Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 2,
Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 2);
//设置播放时间
ta.setDuration(2000);
//设置重复次数,播放一次,重复一次
ta.setRepeatCount(1);
//设置重复放的模式
ta.setRepeatMode(Animation.REVERSE);
//动画播放完毕后,组件停留在动画结束的位置上
ta.setFillAfter(true);
//播放动画
iv.startAnimation(ta);
}
//旋转动画
public void rotate(View v){
//20表示开始的角度,180表示结束的角度,默认的旋转圆心在iv左上角
//ra = new RotateAnimation(20, 180);
//指定圆心坐标,相对于自己, iv真实的坐标左上角 x+ 0.5*iv宽度, y+0.5*iv高度
ra = new RotateAnimation(20, 360,
Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
ra.setDuration(2000);
ra.setRepeatCount(1);
ra.setRepeatMode(Animation.REVERSE);
iv.startAnimation(ra);
}
//缩放动画
public void scale(View v){
//sa = new ScaleAnimation(fromX, toX, fromY, toY);
//改变缩放的中心点,相对于自己的中心坐标,iv的真是坐标左上角 x+0.5*iv宽度, y+0.5*iv高度
sa = new ScaleAnimation(0.5f, 2, 0.1f, 3,
Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
sa.setDuration(2000);
sa.setRepeatCount(1);
sa.setRepeatMode(Animation.REVERSE);
iv.startAnimation(sa);
}
//透明动画
public void alpha(View v){
//0为完全透明,1为完全透明
aa = new AlphaAnimation(0, 1);
aa.setDuration(2000);
aa.setRepeatCount(1);
aa.setRepeatMode(Animation.REVERSE);
iv.startAnimation(aa);
}
//所有动画一起飞
public void fly(View v){
//创建动画集合 false表示每个动画的时间校准有动画自己决定, true表示有动画集合决定
AnimationSet set = new AnimationSet(false);
set.addAnimation(aa);
set.addAnimation(ra);
set.addAnimation(sa);
set.addAnimation(ta);
iv.startAnimation(set);
}
}
四、属性动画
- 补间动画,只是一个动画效果,组件其实还在原来的位置上,xy没有改变。
- 属性动画是组件的位置发生了真实的改变,而且在动画的过程中组件的位置是实时改变的,可以相应组件事件。
- 使用最多的就是 AnimatorSet和ObjectAnimator配合:
- 使用ObjectAnimator进行更精细化控制,只控制一个对象的一个属性值
- 使多个ObjectAnimator组合到AnimatorSet形成一个动画
- ObjectAnimator可以自动驱动:
- 调用setFrameDelay()设置动画帧之间的间隙时间,调整帧率,减少动画绘制过程中频繁绘制,在不影响动画效果的情况下减少CPU资源消耗
- 属性动画基本可以实现左右动画
- 但是View的该属性一定要具有 set和get 方法
1.ObjectAnimator
- 内部是通过反射机制实现的,所以该属性一定要具有 set和get方法
- 一些常用的可以直接使用的属性:
- translationX 和translationY: 控制View从父容器的左上角的偏移
- rotation、rotationX、rotationY : 控制View围绕 “支点”进行2D和3D旋转
- scaleX、scaleY : 控制View围绕 “支点”进行2D缩放
- pivotX、pivotY : 控制View的“支点”位置,默认为View的中心点
- x、y : 这两个属性描述了View对象在父容器中的最终位置,它是最初左上角坐标和translationX 、translationY值的累计和
- alpha : 透明度
public class MainActivity extends Activity {
private ImageView iv;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
iv = (ImageView) findViewById(R.id.iv);
iv.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(MainActivity.this, "点不到我", 0).show();
}
});
}
public void translate(View v){
//target:动画作用于哪个组件
//protertyName指定要改变组件的哪个属性,
//属性动画是真正的改变组件的属性的,每个组件对应一个java类,所以,protertyName是指组件Java类中具有get,set方法的成员变量
//iv.setXXX
//values 是可变参数,就是赋予属性的新的值,可以往回走
//ObjectAnimator oa = ObjectAnimator.ofFloat(target, propertyName, values);
ObjectAnimator oa = ObjectAnimator.ofFloat(iv, "translationX", 10, 70, 20, 100);
oa.setDuration(2000);
oa.setRepeatCount(1);
oa.setRepeatMode(ValueAnimator.REVERSE);
oa.start();
}
public void scale(View v){
ObjectAnimator oa = ObjectAnimator.ofFloat(iv,"scaleX", 1, 1.6f, 1.2f, 2);
oa.setDuration(2000);
oa.start();
}
public void alpha(View v){
ObjectAnimator oa = ObjectAnimator.ofFloat(iv, "alpha", 0, 0.6f, 0.2f, 1);
oa.setDuration(2000);
oa.start();
}
public void rotate(View v){
ObjectAnimator oa = ObjectAnimator.ofFloat(iv, "rotationY", 0, 180, 90, 360);
oa.setDuration(2000);
oa.setRepeatCount(1);
oa.setRepeatMode(ValueAnimator.REVERSE);
oa.start();
}
public void fly(View v){
AnimatorSet set = new AnimatorSet();
ObjectAnimator oa1 = ObjectAnimator.ofFloat(iv, "translationX", 10, 70, 20, 100);
oa1.setDuration(2000);
oa1.setRepeatCount(1);
oa1.setRepeatMode(ValueAnimator.REVERSE);
ObjectAnimator oa2 = ObjectAnimator.ofFloat(iv, "translationY", 10, 70, 20, 100);
oa2.setDuration(2000);
oa2.setRepeatCount(1);
oa2.setRepeatMode(ValueAnimator.REVERSE);
ObjectAnimator oa3 = ObjectAnimator.ofFloat(iv, "scaleX", 1, 1.6f, 1.2f, 2);
oa3.setDuration(2000);
oa3.setRepeatCount(1);
oa3.setRepeatMode(ValueAnimator.REVERSE);
ObjectAnimator oa4 = ObjectAnimator.ofFloat(iv, "rotation", 0, 180, 90, 360);
oa4.setDuration(2000);
oa4.setRepeatCount(1);
oa4.setRepeatMode(ValueAnimator.REVERSE);
//设置挨个飞
//set.playSequentially(oa1, oa2, oa3, oa4);
//设置一起飞
set.playTogether(oa1, oa2, oa3, oa4);
set.start();
}
//使用xml文件配置属性动画
public void xml(View v){
//加载属性动画文件
Animator animator = AnimatorInflater.loadAnimator(this, R.animator.objanimator);
//设置作用于哪个组件
animator.setTarget(iv);
animator.start();
}
}
- 可以用xml配置属性动画只需要在res目录下创建一个property animator属性动画文件
<set xmlns:android="http://schemas.android.com/apk/res/android" >
<objectAnimator
android:propertyName="translationX"
android:duration="200"
android:repeatCount="1"
android:repeatMode="reverse"
android:valueFrom="-100"
android:valueTo="100"
>
</objectAnimator>
</set>
2. 如果属性没有set 和 get方法的解决方法
- 方案一: 通过自定义一个属性类或者包装类,类间接的给这个属性增加 get、set方法
/**
* Created at: 2016/8/5 14:21.
* by author: mwp
* 描述:使用属性动画时,给没有set和get方法的属性包装工具
*/
public class WrapperView {
private View mTarget;
public WrapperView(View target) {
this.mTarget = target;
}
/**包装宽度属性*/
public int getWidth(){
return mTarget.getLayoutParams().width;
}
public void setWidth(int width){
mTarget.getLayoutParams().width = width;
mTarget.requestLayout();
}
//使用方法
public void use(){
WrapperView wrapper = new WrapperView(mButton);
ObjectAnimator.ofInt(wrapper,"width",500).setDuration(5000).start();
}
}
- 方案二: 通过ValueAnimator实现
3. ValueAnimator(值动画)
- ObjectAnimator 也是集成自ValueAnimator
- ValueAnimator本身不提供任何动画效果,它更像一个数值发生器,用来产生具有一定规律的数字,调用者通过这个过程来执行自己特定的动画逻辑
ValueAnimator animator = ValueAnimator.ofInt(0,100);
animator.setTarget(view);
animator.setDuration(1000).start();
//最核心的方法
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
// 进度百分比
float value = animation.getAnimatedFraction();
}
});
4. 动画监听
- 监听动画的过程执行一些操作
ObjectAnimator anim = ObjectAnimator.ofFloat(view, "alpha",0.5f);
anim.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
anim.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
super.onAnimationEnd(animation);
}
});
- 这里边有一个重要的思想,如果一个接口需要实现的方法比较多,而通常时候又不需要实现那么多方法,导致代码乱乱的,这个时候,可以写一个抽象类来实现这个接口的所有方法,由于接口的实现类也是接口类型,所以使用的时候就可以只复写这个抽象类中感兴趣的方法就好了
5. View的animate方法
- 在3.0之后,Google给View增加了animate方法来直接驱动属性动画
view.animate()
.alpha(0)
.scaleX(1)
.x(300)
.y(200)
.setDuration(1000)
.withStartAction(new Runnable() {
@Override
public void run() {
//动画开始
}
})
.withEndAction(new Runnable() {
@Override
public void run() {
//动画结束
runOnUiThread(new Runnable() {
@Override
public void run() {
//执行一些主线程方法
}
});
}
})
.start();
6. Android 布局动画
- 布局动画是指作用在ViewGroup上,给ViewGroup增加View时添加一个动画过渡效果
- 最简单的布局动画是在ViewGroup的xml中使用如下属性打开布局动画
android:animateLayoutChanges="true"
- 但是这个默认的动画效果无法替换
- 使用LayoutAnimationController类来自定义一个过渡效果
LinearLayout ll = (LinearLayout)findViewById(R.id.ll);
//设置过渡动画
ScaleAnimation sa = new ScaleAnimation(0,1,0,1);
sa.setDuration(2000);
//设置布局动画的显示属性,第二个参数是每个子View显示的delay时间
LayoutAnimationController lac = new LayoutAnimationController(sa,0.5f);
lac.setOrder(LayoutAnimationController.ORDER_NORMAL);//顺序,随机,倒序
//为ViewGroup设置布局动画
ll.setLayoutAnimation(lac);
7. Interpolators(插值器 )
- 插值器是动画中一个非常重要的概念,通过插值器可以定义动画变换速率,其作用主要是控制目标变量的变化值进行对应的变化
- AccelerateDecelerateInterpolator开始与结束的地方速率改变比较慢,在中间的时候加速
- AccelerateInterpolator开始的地方速率改变比较慢,然后开始加速
- AnticipateInterpolator开始的时候向后然后向前甩
- AnticipateOvershootInterpolator开始的时候向后然后向前甩一定值后返回最后的值
- BounceInterpolator动画结束的时候弹起
- CycleInterpolator循环播放特定的次数,速率改变沿着正弦曲线
- DecelerateInterpolator在开始的地方快然后慢
- 创建的时候,可以传factor值,如DecelerateInterpolator(2f):
- LinearInterpolator以常量速率改变
- OvershootInterpolator向前甩一定值后再回到原来位置
- 创建的时候,可以传tension值,OvershootInterpolator(0.8f):
五、自定义动画
- 就是现有的透明度,旋转,平移,缩放等行为组合起来仍然不能满足你的话,可以自定义一些更炫的动画
public class CustomAnim extends Animation {
private int mCenterWidth;
private int mCenterHeight;
private Camera mCamera = new Camera();
private float mRotateY = 0.0f;
@Override
public void initialize(int width,
int height,
int parentWidth,
int parentHeight) {
super.initialize(width, height, parentWidth, parentHeight);
// 设置默认时长
setDuration(2000);
// 动画结束后保留状态
setFillAfter(true);
// 设置默认插值器
setInterpolator(new BounceInterpolator());
mCenterWidth = width / 2;
mCenterHeight = height / 2;
}
// 暴露接口-设置旋转角度
public void setRotateY(float rotateY) {
mRotateY = rotateY;
}
@Override
protected void applyTransformation(
float interpolatedTime,
Transformation t) {
final Matrix matrix = t.getMatrix();
mCamera.save();
// 使用Camera设置旋转的角度
mCamera.rotateY(mRotateY * interpolatedTime);
// 将旋转变换作用到matrix上
mCamera.getMatrix(matrix);
mCamera.restore();
// 通过pre方法设置矩阵作用前的偏移量来改变旋转中心
matrix.preTranslate(mCenterWidth, mCenterHeight);
matrix.postTranslate(-mCenterWidth, -mCenterHeight);
}
}
六、Android 5.X SVG 矢量动画机制
- 可伸缩矢量图形(Scalable Vector Graphics)
- 定义用于网络的基于矢量的图形
- 使用XML格式定义图形
- 图像在放大或改变尺寸的情况下其图形质量不会有损失
- 与Bitmap对比,SVG最大的优点就是方法不失真,而且不需要为不同分辨率设计多套图标
七、点击view显示隐藏其他View带动画的一个小例子
public class DropTest extends Activity {
private LinearLayout mHiddenView;
private float mDensity;
private int mHiddenViewMeasuredHeight;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.drop);
mHiddenView = (LinearLayout) findViewById(R.id.hidden_view);
// 获取像素密度
mDensity = getResources().getDisplayMetrics().density;
// 获取布局的高度
mHiddenViewMeasuredHeight = (int) (mDensity * 40 + 0.5);
}
public void llClick(View view) {
if (mHiddenView.getVisibility() == View.GONE) {
// 打开动画
animateOpen(mHiddenView);
} else {
// 关闭动画
animateClose(mHiddenView);
}
}
private void animateOpen(final View view) {
view.setVisibility(View.VISIBLE);
ValueAnimator animator = createDropAnimator(
view,
0,
mHiddenViewMeasuredHeight);
animator.start();
}
private void animateClose(final View view) {
int origHeight = view.getHeight();
ValueAnimator animator = createDropAnimator(view, origHeight, 0);
animator.addListener(new AnimatorListenerAdapter() {
public void onAnimationEnd(Animator animation) {
view.setVisibility(View.GONE);
}
});
animator.start();
}
private ValueAnimator createDropAnimator(
final View view, int start, int end) {
ValueAnimator animator = ValueAnimator.ofInt(start, end);
animator.addUpdateListener(
new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
int value = (Integer) valueAnimator.getAnimatedValue();
ViewGroup.LayoutParams layoutParams =
view.getLayoutParams();
layoutParams.height = value;
view.setLayoutParams(layoutParams);
}
});
return animator;
}
}
Android 动画机制与使用技巧的更多相关文章
- Android群英传》读书笔记 (3) 第六章 Android绘图机制与处理技巧 + 第七章 Android动画机制与使用技巧
第六章 Android绘图机制与处理技巧 1.屏幕尺寸信息屏幕大小:屏幕对角线长度,单位“寸”:分辨率:手机屏幕像素点个数,例如720x1280分辨率:PPI(Pixels Per Inch):即DP ...
- Android群英传笔记——第七章:Android动画机制和使用技巧
Android群英传笔记--第七章:Android动画机制和使用技巧 想来,最 近忙的不可开交,都把看书给冷落了,还有好几本没有看完呢,速度得加快了 今天看了第七章,Android动画效果一直是人家中 ...
- 第三章 Android绘图机制与处理技巧
1.屏幕尺寸信息 屏幕大小:屏幕对角线长度,单位“寸”:分辨率:手机屏幕像素点个数,例如720x1280分辨率:PPI(Pixels Per Inch):即DPI(Dots Per Inch),它是对 ...
- Android群英传笔记——第六章:Android绘图机制与处理技巧
Android群英传笔记--第六章:Android绘图机制与处理技巧 一直在情调,时间都是可以自己调节的,不然世界上哪有这么多牛X的人 今天就开始读第六章了,算日子也刚好一个月了,一个月就读一半,这效 ...
- Android绘图机制和处理技巧
色彩特效处理 在色彩处理中,使用以下三个角度来描述一个图像. 色调: 物体传播的颜色 饱和度: 颜色的纯度, 从0(灰)到100%(饱和)来进行描述 亮度: 颜色的相对明暗程度 在Android中, ...
- 《Android开发艺术探索》读书笔记 (7) 第7章 Android动画深入分析
本节和<Android群英传>中的第七章Android动画机制与使用技巧有关系,建议先阅读该章的总结 第7章 Android动画深入分析 7.1 View动画 (1)android动画分为 ...
- android动画之通过子线程来实现动画
android动画之通过子线程来实现动画 使用android动画机制,往往是相对于原始位置来进行参照. 这里通过子线程修改物体位置实现动画. 布局文件: <RelativeLayout xmln ...
- Android绘图机制(四)——使用HelloCharts开源框架搭建一系列炫酷图表,柱形图,折线图,饼状图和动画特效,抽丝剥茧带你认识图表之美
Android绘图机制(四)--使用HelloCharts开源框架搭建一系列炫酷图表,柱形图,折线图,饼状图和动画特效,抽丝剥茧带你认识图表之美 这里为什么不继续把自定义View写下去呢,因为最近项目 ...
- Android动画的理解
基础知识 在我们开始讲Android动画这个知识点之前,我们了解下相应的基础知识点. Shape篇 一般用Shape定义的XML文件是存放在Drawable目录下,广泛应用于在Button.TextV ...
随机推荐
- First step of using junit---------Software Testing Lab 1---2016.03.18
1. Install junit a) Download “junit.jar” b) In eclipse, Windows->Preferences->Java-& ...
- 使用Python拼接多张图片
写机器学习相关博文,经常会碰到很多公式,而Latex正式编辑公式的利器.目前国内常用的博客系统,好像只有博客园支持,所以当初选择落户博客园.我现在基本都是用Latex写博文,然后要发表到博客园上与大家 ...
- MVC的路由
MVC的路由包括以下几部分 路由名称,路由URL,路由的初始值,路由的约束,路由的命名空间 routes.MapRoute( name: "Default", url: " ...
- Scrum之Sprint物件
产品订单(Product Backlog) 一个需求的列表. 一般情况使用用户故事来表示backlog条目 理想情况每个需求项都对产品的客户或用户有价值 Backlog条目按照商业价值排列优先级 优先 ...
- ets dets
相同点:ets和dets都提供“键—值”搜索表 不同点:ets驻留在内存,dets驻留在磁盘 特点:ets表和dets表可以被多个进程共享,因此通过这两个模块可以实现数据间的交换 一 ets表 实现 ...
- NServiceBus-网关和多站点分布
多站点部署的企业的数量.净系统由于增加的挑战高可用性和用户要求更快的响应时间,服务器和数据访问更接近. RPC技术迅速陷入困境在这些环境中,使机器在同一个站点和远程站点看起来是一样的. 在这些情况下, ...
- 阿里云存储OSS之九大使用技巧
http://www.biphp.com/cloud-computing/%E9%98%BF%E9%87%8C%E4%BA%91%E5%AD%98%E5%82%A8oss%E4%B9%8B%E4%B9 ...
- 数据库 CHECKDB 发现了x个分配错误和 x 个一致性错误
--1.在SQL查询分析器中执行以下语句:(注以下所用的POS为数据库名称,请用户手工改为自己的数据库名) use pos dbcc checkdb --2.查看查询结果,有很多红色字体显示,最后结果 ...
- 第三百五十五天 how can I 坚持
快一年了,三百五十五天了,等写个程序算算时间,看看日期和天数能不能对的上,哈哈. 计划还是未制定,天气预报还是没有写完,立马行动,发完这个博客,立马行动. 计划:设计模式1个月,三大框架3个月,计算机 ...
- vim显示历史命令
[vim显示历史命令] q: 进入命令历史编辑.类似的还有 q/ 可以进入搜索历史编辑.注意 q 后面如果跟随其它字母,是进入命令记录. 可以像编辑缓冲区一样编辑某个命令,然后回车执行.也可以用 ct ...