DoTween使用总结

一.介绍

Tween(补间):

一般指补间动画,例如unity的Animation窗口,插入两个图片或者对一个物体做变换时,unity会根据两个状态之间的帧,创建相应的动画,这就是补间。

安装

1.DOTween官方下载安装

2.Unity Asset Store安装

3.安装完成会显示安装面板,点击 Setup DOTween 会自动根据unity的版本导入/重新导入内部的一些文件,激活或者停用一些模块。如果不小心关闭或者关闭了想再次打开,你可以在unity的工具栏的Tools/Demigiant/DOTween Utility Panel 打开该面板。

4.在使用DOTween需引入对应的命名空间using DG.Tweening;初始化DOTween,设置一些全局设置;如果在每次创建DOTween脚本时不调用该方法进行初始化,DOTween也会根据默认的设置自动进行初始化设置 ,如果进行初始化必须在每次使用前调用该方法,否则不会有任何、效果。默认的设定可以在初始面板中的首选项

DOTWeen.Init();

参数 功能
recycleAllByDefault bool类型,如果为true,所有的 动画补间将会在调用完后被回收放入一个对象池中,而不是直接被销毁。
useSafeMode bool类型,如果为true,补间会变慢,但是会更加安全,允许补间照顾一些东西,当一个物体被销毁时,补间仍然执行。
logBehaviour 据所选模式,DOTween 将只记录错误(ErrorsOnly)或者错误和警告(Default),或所有内容以及其他信息(Verbose)

命名法

  • Tweener :补间,控制值并为其生成动画。
  • Sequence:序列,控制多个补间作为组来处理。
  • Tween:通用词,通用词表示补间和序列。
  • Nested tween:嵌套补间,序列中包含的补间。

前缀

  • DO:所有快速实现一些补间效果的前缀(如变换Transform)的前缀。
  • Set:对当前效果进行设置。
  • On:一系的列回调。

二. Unity常用组件拓展方法

1.Transform拓展方法

Move(移动)

// 作用:移动到某一指定点。(世界坐标)
// 参数:to:位置,duration:时间,snapping:每次移动整数值默认为false,为true时平滑地将所有值变为整数
transform.DOMove(new Vector3(10, 0, 0), 10, false);
// 作用:沿着某一轴移动到指定位置。
transform.DOMoveX()/DOMoveY()/DOMoveZ(10, 10f,false);
// 作用:移动自身坐标到指定位置。
transform.DOLocalMove(new Vector3(10,0.5f,0),10f,false);
// 作用:移动自身坐标到指定轴的指定位置。
transform.DOLocalMoveX()/DOLocalMoveY()/DOLocalMoveZ();
// 作用:实现跳跃到指定位置。
// endValue:最终要跳跃到的位置
// jumpPower:跳跃的强度,决定跳跃的高度(当前位置Y加上该值)
// numJumps:跳跃的次数
// duration:总持续时间
// snapping:为true时平滑地将所有值变为整数。(每次移动整数值)默认为false
transform.DOJump(new Vector3(10, 0, 0),10,3,10,false);
//作用:实现跳跃到指定位置(自身坐标)。
transform.DOLocalJump(new Vector3(10, 0, 0),10,3,10,false);

Rotate(旋转)

//作用:旋转到指定的值(根据欧拉角)。
//to:旋转目标值,duration:旋转时间,
//RotateMode: 
//Fast:旋转采用最短路线,切旋转不会超过360°,
//FastBeyond360:旋转将超过360°;
//WorldAxisAdd:类似于使用transform.Rotate(new Vector3(20, 0, 0),Space.World)
//LocalAxisAdd:类似于使用transform.Rotate(new Vector3(20, 0, 0),Space.Self)
transform.DORotateQuaternion();
//作用:旋转到指定的值(四元数)。
transform.DORotate(Rote, 0.1f, RotateMode.Fast);
//作用:自身坐标旋转。
transform.DOLocalRotate();
//作用:自身坐标旋转。
transform.DOLocalRotateQuaternion();
//作用:旋转目标,使其朝向给定位置。
//towards:旋转目标值,duration:旋转总用时,axisConstraint:旋转最终轴约束,只旋转此轴, up:              定义向上方向的矢量
transform.DOLookAt();
// 作用:旋转目标,使其朝向给定位置,每帧更新 lookAt 位置(与此相反,当补间开始时,只计算一次 lookAt 旋转
transform.DODynamicLookAt();

Scale(比例)

//将物体放大/缩小到指定大小
//to:浮点数为倍数,向量为指定大小;duration:放大/缩小总消耗时间
transform.DOScale();
//作用:对某一轴方向进行放大缩小
transform.DOScaleX/DOScaleY/DOScaleZ();

Punch(冲击)

//作用:受到冲击后的回弹效果
//Vector3   punch:要被击打到的最远位置(相对值,相对于局部坐标)
//float duration:总持续时间
//int vibrato:物体振动频率
//float elasticity:值一般在0到1之间,0表示起点到冲击方向的震荡,1表示为一个完整的震荡,可能会超过起点,个人感觉后者效果更好。
//bool snapping:是否进行平滑插值
transform.DOPunchPosition();
//作用:受到冲击后旋转效果
transform.DOPunchRotation()
//作用:实现一个弹性效果,反复弹,最终复原。
transform.DOPunchScale()

Shake(摇晃)

//参数:持续时间,力量,震动,随机性,淡出
//力量:实际就是震动的幅度,可以理解成相机施加的力的大小 使用Vector3可以选择每个轴向不同的强度
//震动:震动次数
//随机性:改变震动方向的随机值(大小:0~180)
//淡出:就是运动最后是否缓慢移动回到原本位置
transform.DOShakePosition(1, 5, 10, 50, true);
transform.DOShakeRotation(3);
transform.DOShakeScale(3);

Blend

 带Blend名称的方法,允许混合动画
 原本同时执行两个Move方法,只会执行最新的一个动画命令
 例如:
  transform.DOMove(Vector3.one, 2);
  transform.DOMove(Vector3.one * 2, 2);
 //结果是物体运动到了(2,2,2)坐标上

 DOBlendableMoveBy方法有两个特点
 1)允许多个同时执行
 例如:
  transform.DOBlendableMoveBy(new Vector3(1, 1, 1), 1);
  transform.DOBlendableMoveBy(new Vector3(-1, 0, 0), 1);
  假设其实点为(0,0,0),最后动画停止时的坐标就是(0,1,1)
 2)它是增量动画
  transform.DOBlendableMoveBy(new Vector3(1, 1, 1), 1);
  假设其实点为(1,1,1),最后动画停止时的坐标就是(2,2,2)
  它的参数不是目标点,而是要移动的量

  以下三个函数同理
  transform.DOBlendableRotateBy()
  transform.DOBlendableScaleBy()
  transform.DOBlendablePunchRotation()

2.Camera拓展方法

//调整屏幕视角的宽高比 第一个参数是宽高的比值
camera.DOAspect(0.6f, 2);

//改变相机background参数的颜色
camera.DOColor(Color.blue, 2);

//改变相机近切面的值
camera.DONearClipPlane(200, 2);

//改变相机远切面的值
camera.DOFarClipPlane(2000, 2);

//改变相机FOV的值
camera.DOFieldOfView(30, 2);

//改变相机正交大小
camera.DOOrthoSize(10, 2);

//按照屏幕像素计算的显示范围
camera.DOPixelRect(new Rect(0f, 0f, 600f, 500f), 2);

//按照屏幕百分比计算的显示范围
camera.DORect(new Rect(0.5f, 0.5f, 0.5f, 0.5f), 2);
//相机震动
//相机震动效果 参数:持续时间,力量,震动,随机性,淡出
//力量:实际就是震动的幅度,可以理解成相机施加的力的大小 使用Vector3可以选择每个轴向不同的强度
//震动:震动次数
//随机性:改变震动方向的随机值(大小:0~180)
//淡出:就是运动最后是否缓慢移动回到原本位置
camera.DOShakePosition(1, 10, 10, 50, false);

3.Material拓展方法

//改变颜色
material.DOColor(Color.black, 2);

//按照shader的属性名,修改颜色
material.DOColor(Color.clear, "_Color", 2);

//修改alpha值
material.DOFade(0, 2);

//颜色渐变
//Gradient是unity的渐变编辑器(下面有渐变编辑器的图)
material.DOGradientColor(Gradient, "_Color", 3);

//改变材质offset的值
material.DOOffset(new Vector2(1, 1), 2);

//改变提供的shader属性的名称对应的Vector4值
material.DOVector(new Vector4(0, 0, 0, 1), "_Color", 3);

//颜色混合
//跟位置混合动画同理,可以同时执行而不干扰,产生混合在一起的颜色
material.DOBlendableColor(Color.red, "_Color", 3);

4.Text拓展方法

//颜色渐变
text.DOColor(Color.black, 2);
//透明度变化
text.DOFade(0, 2);
// 混合颜色渐变
text.DOBlendableColor(Color.black, 2);
//逐个显示文字
text.DOText("context", 2);

三. Unity常用组件拓展方法

1. Sequence

Sequence quence = DOTween.Sequence();

//1)添加动画到队列中
quence.Append(transform.DOMove(Vector3.one, 2));

//2)添加时间间隔
quence.AppendInterval(1);

//3)按时间点插入动画
//第一个参数为时间,此方法把动画插入到规定的时间点
//以这句话为例,它把DORotate动画添加到此队列的0秒时执行,虽然它不是最先添加进队列的
quence.Insert(0, transform.DORotate(new Vector3(0, 90, 0), 1));

//4)加入当前动画
//Join会加入和让动画与当前正在执行的动画一起执行
//如下两行代码,DOMove会和DOScale一起执行
quence.Append(transform.DOScale(new Vector3(2, 2, 2), 2));
quence.Join(transform.DOMove(Vector3.zero, 2));

//5)预添加动画
//预添加 会直接添加动画到Append的前面,也就是最开始的时候
quence.Prepend(transform.DOScale(Vector3.one * 0.5f, 1));
//这里需要特别说一下预添加的执行顺序问题
//它这里也采取了队列的性质,不过,预添加与原本的的队列相比是一个反向队列

 Sequence quence = DOTween.Sequence();
 quence.Append(transform.DOMove(Vector3.one, 2));
 quence.Prepend(transform.DOMove(-Vector3.one*2, 2));
 quence.PrependInterval(1);

 //执行顺序是 PrependInterval----Prepend-----Append
 //就是最后添加的会在队列的最顶端

//6)预添加时间间隔
quence.PrependInterval(1);

//回调函数

//1)预添加回调
quence.PrependCallback(PreCallBack);

//2)在规定的时间点加入回调
quence.InsertCallback(0, InsertCallBack);

//3)添加回调
quence.AppendCallback(CallBack);

2. Tweener的设置

  TweenParams para = new TweenParams();

//1)设置动画循环 
//  第一个参数是循环次数  -1代表无限循环
// 第二个参数是循环方式 
//  Restart  重新开始  
//  Yoyo   从起始点运动到目标点,再从目标点运动回来,这样循环 
//  Incremental   一直向着运动方向运动
para.SetLoops(-1, LoopType.Yoyo);

//2)设置参数
transform.DOMove(Vector3.one, 2).SetAs(para);

//3)设置自动杀死动画
transform.DOMove(Vector3.one, 1).SetAutoKill(true);

//4)from补间
// From参数 isRelative(相对的):
// 为true,传入的就是偏移量,即当前坐标 + 传入值 = 目标值
// 为falese,传入的就是目标值,即传入值 = 目标值
transform.DOMove(Vector3.one, 2).From(true);
   

//5)设置动画延时 
transform.DOMove(Vector3.one, 2).SetDelay(1);

//6)设置动画运动以速度为基准
//  使用SetSpeedBased时,移动方式就变成以速度为基准
//  原本表示持续时间的第二个参数,就变成表示速度的参数,每秒移动的单位数
transform.DOMove(Vector3.one, 1).SetSpeedBased();
   

//7)设置动画ID
transform.DOMove(Vector3.one, 2).SetId("Id");

//8)设置是否可回收
 // 为true的话,动画播放完会被回收,缓存下来,不然播完就直接销毁
transform.DOMove(Vector3.one, 2).SetRecyclable(true);

//9)设置动画为增量运动
//  SetRelative参数 isRelative(相对的):
//  为true,传入的就是偏移量,即当前坐标 + 传入值 = 目标值
//  为falese,传入的就是目标值,即传入值 = 目标值
 transform.DOMove(Vector3.one, 2).SetRelative(true);
     

//10)设置动画的帧函数
//第一个参数 UpdateType :选择使用的帧函数
//UpdateType.Normal:更新每一帧中更新要求。 
//UpdateType.Late:在LateUpdate调用期间更新每一帧。 
//UpdateType.Fixed:使用FixedUpdate调用进行更新。 
//UpdateType.Manual:通过手动DOTween.ManualUpdate调用进行更新。
//第二个参数:为TRUE,则补间将忽略Unity的Time.timeScale
transform.DOMove(Vector3.one, 2).SetUpdate(UpdateType.Normal, true);

3.Ease 运动曲线的设置

//1)以Ease枚举作为参数
//第二个参数 Amplitude(振幅):实际就是移动次数,起始点移动到目标算移动一次,再移动回来移动两次
//第三个参数 period 值的范围是 - 1~1
//值 > 0时,为活动范围的衰减值,活动范围会由大变小
//值 = 0时,就是均匀的在起始坐标和目标坐标之间运动
//值 < 0时,会施加一个向目标坐标的一个力,活动范围一点点增大,最后逼近目标点
//这两个参数只对Flash,InFlash,OutFlash,InOutFlash这四种曲线有用,其他的曲线起作用的就只有Ease枚举参数
transform.DOMove(Vector3.one, 2).SetEase(Ease.Flash, 3, 0f);
   
//2)使用AnimationCurve当作参数
//AnimationCurve 横轴是时间,不过不是具体的时间,而是时间比例
//AnimationCurve 纵轴是倍数
//假设纵轴的值为v,传入DOMove的第一个参数endValue是e,起始点坐标是s
//此物体最后动画结束时的实际坐标即为 v* (e -s)+ s
transform.DOMove(Vector3.one * 2, 1).SetEase(curve);

//以回调函数为参数
transform.DOMove(Vector3.one * 2, 1).SetEase(MyEaseFun);
//返回值是运动距离的百分比 值应为0~1之间,最后的值需为1,不然停留的位置不会是目标位置
private float EaseFunc(float time, float duration, float overshootOrAmplitude, float period){
    return time / duration;
}

4.回调函数

//动画完成回调
transform.DOMove(Vector3.one, 2).OnComplete(() => { });
    
//动画被杀死时回调
transform.DOMove(Vector3.one, 2).OnKill(() => { });

//动画播放时回调,暂停后重新播放也会调用
transform.DOMove(Vector3.one, 3).OnPlay(() => { });
    
//动画暂停时回调
transform.DOMove(Vector3.one, 2).OnPause(() => { });
    
//动画回退时回调
//以下情况会被调用
//使用DORestart重新播放时
//使用Rewind倒播动画完成时
//使用DOFlip翻转动画完成时
//使用DOPlayBackwards反向播放动画完成时
transform.DOMove(Vector3.one, 2).OnRewind(() => { });
    
//只在第一次播放动画时调用,在play之前调用
transform.DOMove(Vector3.one, 2).OnStart(() => { });
    
//完成单个循环周期时触发
transform.DOMove(Vector3.one, 2).OnStepComplete(() => { });
//帧回调
transform.DOMove(Vector3.one, 2).OnUpdate(() => { });
    
//在路径动画时,改变目标点时的回调,参数为当前目标点的下标
transform.DOMove(Vector3.one, 2).OnWaypointChange((value) => { });

5.动画控制方法

// 动画控制方法:动画执行完不销毁,因此要设置动画的SetAutoKill(false);
//播放
transform.DOPlay();

//暂停
transform.DOPause();

//重播
transform.DORestart();
//倒播,此方法会直接退回起始点
transform.DORewind();

//平滑倒播,此方法会按照之前的运动方式从当前位置退回起始点
transform.DOSmoothRewind();

//杀死动画
transform.DOKill();

//翻转补间的方向
transform.DOFlip();

//跳转时间点
//第一个参数跳转的时间点,第二个参数是跳转后是否播放动画
transform.DOGoto(1.5f, true);

//反向播放动画
//反向播放动画,在动画播放到一半时执行,会退回起始点,在一开始执行看不到效果是因为,物体本身就在起始点
transform.DOPlayBackwards();

//正向播放动画
//正向播放动画
transform.DOPlayForward();

//TogglePause
//当暂停时,执行就继续播放,播放时,执行就暂停
transform.DOTogglePause();

6.获取数据方法

//一、类方法
//1)返回所有暂停的动画,没有则返回null
DOTween.PausedTweens();

//2)返回所有真正播放的动画,没有则返回null
DOTween.PlayingTweens();

//3)获取给定ID的数组,返回满足条件的动画数组
//第一个参数是动画的ID
//第二个参数是是否只收集正在播放的动画  
DOTween.TweensById("id", true);

//4)返回给定对象的数组,返回满足条件的动画数组
//第一个参数是播放动画的对象
//第二个参数是是否只收集正在播放的动画
transform.DOMove(Vector3.one, 2); 第一个参数就传入transform
material.DOColor(Color.White, 2); 第一个参数就传入材质对象material
DOTween.TweensByTarget(transform, true);
//5)收集传入的对象是否有动画在活动
//第一个参数为检测的对象
//第二个参数为是否检测动画在播放状态
//为true时,给定对象在播放状态时 返回true
//为false时,只检测给定对象是否有动画(在pause状态时也算)有则返回true
DOTween.IsTweening(transform);

//6)正在播放的动画的总数,目前处于延迟播放状态的动画也算
DOTween.TotalPlayingTweens();

//二、实例方法
_tweener = transform.DOMove(Vector3.one, 2)

//1)表示动画执行时间的属性,可读可写
_tweener.fullPosition = 1;

//2)表示动画执行完的次数
_tweener.CompletedLoops()

//3)获取动画的延迟时间
_tweener.Delay();

//4)获取动画的持续时间
//参数为true 表示计算循环的时间,无限循环为Infinity
_tweener.Duration(false)

//5)动画已播放的时间
//参数为true 表示计算循环的时间
_tweener.Elapsed()

//6)返回动画进度的百分比
//起始点为0 目标点为1 当yoyo循环模式下,值会从0变到1再从1变到0
_tweener.ElapsedDirectionalPercentage()

//7)返回动画区间已用的百分比
//单次循环的数值为0到1
//参数为 是否包含循环 为true时 返回值是循环总区间的已用百分比 若为无限循环 返回值为0
_tweener.ElapsedPercentage(true)

//8)动画是否在活动
_tweener.IsActive();

//9)是否是反向动画
_tweener.IsBackwards();

//10)动画是否完成
_tweener.IsComplete();

//11)是否以初始化
_tweener.IsInitialized();

//12)是否正在播放
_tweener.IsPlaying();

//13)返回循环次数,  无限循环为Infinity
_tweener.Loops();

7.携程方法

private IEnumerator Wait()
{
    _tweener = transform.DOMove(Vector3.one, 2);
    
  //1)等待动画执行完
    yield return _tweener.WaitForCompletion();
    
  //2)等待指定的循环次数
   //参数为执行次数,等待传入的循环次数后,继续执行
   //若是传入的次数大于动画的循环次数 则在动画结束时继续执行
    yield return _tweener.WaitForElapsedLoops(2);
    
  //3)等待动画被杀死
    yield return _tweener.WaitForKill();
    
  //4)等待动画执行指定时间
    参数为时间,动画执行传入的时间之后或动画执行完毕,继续执行
    yield return _tweener.WaitForPosition(0.5f);
    
  //5)等待动画回退
  //  以下情况会继续执行函数
  // 使用DORestart重新播放时
  //  使用Rewind倒播动画完成时
  //  使用DOFlip翻转动画完成时
  //  使用DOPlayBackwards反向播放动画完成时
    yield return _tweener.WaitForRewind();
    
  //6)等待Start执行后继续执行
    yield return _tweener.WaitForStart();

8.Sequence联合动画

//在Sequence的最后添加一个tween。
Append(Tween tween)

//在Sequence的最后添加一个回调函数。
AppendCallback(TweenCallback callback)

//在Sequence的最后添加一段时间间隔。
AppendInterval(float interval)

//在给定的时间位置上放置一个tween,可以实现同时播放多个tween的效果,而不是一个接一个播放。
Insert(float atPosition,Tween tween)

//在给定的时间位置上放置一个回调函数。
InsertCallback(float atPosition, TweenCallback callback)

//在Sequence的最后一个tween的开始处放置一个tween。可以实现同时播放多个tween的效果,而不是一个接一个播放。
Join(Tween tween)

//在Sequence开始处插入一个tween,原先的内容根据时间往后移。
Prepend(Tween tween)

//在Sequence开始处插入一个回调函数
PrependCallback(TweenCallback callback)

//在Sequence开始处插入一段时间间隔,原先的内容根据时间往后移。
PrependInterval(float interval)
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,482评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,377评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,762评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,273评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,289评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,046评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,351评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,988评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,476评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,948评论 2 324
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,064评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,712评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,261评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,264评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,486评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,511评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,802评论 2 345

推荐阅读更多精彩内容