Material Design中的動畫將為用戶提供操作反饋并在用戶與您的應用進行互動時提供視覺連續性。 Material Design將為按鈕與操作行為轉換提供一些默認動畫,而 Android 5.0(API Level 21)及更高版本可讓您定制這些動畫,同時也可創建新動畫:
一、觸摸反饋動畫
效果圖:
Material Design的觸摸反饋可在用戶與 UI 元素互動時,在接觸點上提供即時視覺確認。 適用于按鈕的默認觸摸動畫使用全新 RippleDrawable類別,以波紋效果實現不同狀態間的轉換。
在大多數情況下,應以下列方式指定視圖背景,在您的視圖 XML 中應用此功能:
- ?android:attr/selectableItemBackground 指定有界的波紋。
- ?android:attr/selectableItemBackgroundBorderless 指定越過視圖邊界的波紋。 它將由一個非空背景的視圖的最近父項所繪制和設定邊界。
任何view處于可點擊狀態,都可以使用RippleDrawable來達到水波紋特效,而且必須處于可點擊狀態,才會出現波紋動畫效果。
在代碼中可以這樣設置:
RippleDrawableColorStateList stateList = getResources().getColorStateList(R.color.tint_state_color);
RippleDrawable rippleDrawable = new RippleDrawable(stateList, null, null);
view.setBackground(rippleDrawable);
注意:selectableItemBackgroundBorderless是 API Level 21 中推出的新屬性。
此外,您可利用 ripple元素將 RippleDrawable定義為一個 XML 資源。
您可以為 RippleDrawable對象指定一種顏色。如果要改變默認觸摸反饋顏色,請使用主題的 android:colorControlHighlight屬性。
如果要了解更多信息,請參閱 RippleDrawable類別的 API 參考文檔。
我們來看看系統自帶的觸摸反饋動畫是怎么實現的,為什么只需要在view的background
或者foreground
屬性設置成?android:attr/selectableItemBackground
或者?android:attr/selectableItemBackgroundBorderless
就可以實現波紋動畫的效果?這兩個屬性點進去,可以看到在路徑sdk/platforms/android-xx/data/res/values/attrs.xml
文件中有定義這么兩個屬性:
<!-- Background drawable for bordered standalone items that need focus/pressed states. -->
<attr name="selectableItemBackground" format="reference" />
<!-- Background drawable for borderless standalone items that need focus/pressed states. -->
<attr name="selectableItemBackgroundBorderless" format="reference" />
我們想到,這兩個屬性既然是整個app中有效的,那可能會是在Theme中的屬性吧,那就去AndroidManifest文件中跟這個Theme一步步看下去,最后在Base.V21.Theme.AppCompat.Light
這個style中看到確實是有這兩個item屬性:
<item name="selectableItemBackground">?android:attr/selectableItemBackground</item>
<item name="selectableItemBackgroundBorderless">?android:attr/selectableItemBackgroundBorderless</item>
但是這里還是調用的系統的定義的屬性,繼續往下追,在android:Theme.Material
和android:Theme.Material.Light
中,可以看到:
<item name="selectableItemBackground">@drawable/item_background_material</item>
<item name="selectableItemBackgroundBorderless">@drawable/item_background_borderless_material</item>
然后sdk路徑下platforms\\android-xx\\data\\res\\drawable可以找到這些資源文件如下圖:
item_background_material的內容是:
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="?attr/colorControlHighlight">
<item android:id="@id/mask">
<color android:color="@color/white" />
</item>
</ripple>
item_background_borderless_material的內容是:
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="?attr/colorControlHighlight" />
系統的做法是用ripple元素將 RippleDrawable定義為一個 XML 資源,而通過看View的源碼中在構造方法中是這樣獲取background屬性的:
public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
this(context);
final TypedArray a = context.obtainStyledAttributes(
attrs, com.android.internal.R.styleable.View, defStyleAttr, defStyleRes);
if (mDebugViewAttributes) {
saveAttributeData(attrs, a);
}
Drawable background = null;
switch (attr) {
case com.android.internal.R.styleable.View_background:
background = a.getDrawable(attr);
break;
.
.
.
}
也就是說,這個background實際上就是RippleDrawable類。那我們就來看看這個RippleDrawable內部到底是怎么做的吧。
首先官方文檔對RippleDrawable解釋是
Drawable that shows a ripple effect in response to state changes. The anchoring position of the ripple for a given state may be specified by calling setHotspot(float, float)
with the corresponding state attribute identifier.
通過顯示出波紋效果來響應狀態的改變,對于給定狀態的波紋的錨定位置可以通過調用具有對應的狀態屬性標識符的setHotspot(float,float)
來指定。
RippleDrawable繼承自LayerDrawable
,而LayerDrawable
是繼承Drawable
,RippleDrawable又是為了響應View的statechange,那就看看Drawable類中對點擊時的狀態處理吧。
public boolean setState(@NonNull final int[] stateSet) {
if (!Arrays.equals(mStateSet, stateSet)) {
mStateSet = stateSet;
return onStateChange(stateSet);
}
return false;
}
給Drawable設置狀態屬性時,會把狀態的數組傳給onStateChange方法,在RippleDrawable中重寫了onStateChange。
@Override
protected boolean onStateChange(int[] stateSet) {
final boolean changed = super.onStateChange(stateSet);
boolean enabled = false;
boolean pressed = false;
boolean focused = false;
boolean hovered = false;
for (int state : stateSet) {
if (state == R.attr.state_enabled) {
enabled = true;
} else if (state == R.attr.state_focused) {
focused = true;
} else if (state == R.attr.state_pressed) {
pressed = true;
} else if (state == R.attr.state_hovered) {
hovered = true;
}
}
setRippleActive(enabled && pressed);
setBackgroundActive(hovered || focused || (enabled && pressed), focused || hovered);
return changed;
}
看到setRippleActive
和setBackgroundActive
這兩個方法應該可以猜到是什么意思了,接著看。
private void setRippleActive(boolean active) {
if (mRippleActive != active) {
mRippleActive = active;
if (active) {
tryRippleEnter();
} else {
tryRippleExit();
}
}
}
如果Drawable是enable=true且pressd=true時,會調用tryRippleEnter
方法
/**
* Attempts to start an enter animation for the active hotspot. Fails if
* there are too many animating ripples.
*/
private void tryRippleEnter() {
if (mExitingRipplesCount >= MAX_RIPPLES) {
// This should never happen unless the user is tapping like a maniac
// or there is a bug that's preventing ripples from being removed.
return;
}
if (mRipple == null) {
final float x;
final float y;
if (mHasPending) {
mHasPending = false;
x = mPendingX;
y = mPendingY;
} else {
x = mHotspotBounds.exactCenterX();
y = mHotspotBounds.exactCenterY();
}
final boolean isBounded = isBounded();
mRipple = new RippleForeground(this, mHotspotBounds, x, y, isBounded, mForceSoftware);
}
mRipple.setup(mState.mMaxRadius, mDensity);
mRipple.enter(false);
}
看到這里,我們可以知道要開始做波紋動畫的效果了。mRipple 是RippleForeground類的實例,然而我沒有在RippleForeground類中找到setup和enter方法,但是RippleForeground繼承自RippleComponent類,于是,我在這個類中發現了這兩個方法。
public final void setup(float maxRadius, int densityDpi) {
if (maxRadius >= 0) {
mHasMaxRadius = true;
mTargetRadius = maxRadius;
} else {
mTargetRadius = getTargetRadius(mBounds);
}
mDensityScale = densityDpi * DisplayMetrics.DENSITY_DEFAULT_SCALE;
onTargetRadiusChanged(mTargetRadius);
}
/**
* Starts a ripple enter animation.
*
* @param fast whether the ripple should enter quickly
*/
public final void enter(boolean fast) {
cancel();
mSoftwareAnimator = createSoftwareEnter(fast);
if (mSoftwareAnimator != null) {
mSoftwareAnimator.start();
}
}
setup是初始化一系列參數,enter創建一個動畫并開始動畫。
@Override
protected Animator createSoftwareEnter(boolean fast) {
// Bounded ripples don't have enter animations.
if (mIsBounded) {
return null;
}
final int duration = (int) (1000 * Math.sqrt(mTargetRadius / WAVE_TOUCH_DOWN_ACCELERATION * mDensityScale) + 0.5);
final ObjectAnimator tweenRadius = ObjectAnimator.ofFloat(this, TWEEN_RADIUS, 1);
tweenRadius.setAutoCancel(true);
tweenRadius.setDuration(duration);
tweenRadius.setInterpolator(LINEAR_INTERPOLATOR);
tweenRadius.setStartDelay(RIPPLE_ENTER_DELAY);
final ObjectAnimator tweenOrigin = ObjectAnimator.ofFloat(this, TWEEN_ORIGIN, 1);
tweenOrigin.setAutoCancel(true);
tweenOrigin.setDuration(duration);
tweenOrigin.setInterpolator(LINEAR_INTERPOLATOR);
tweenOrigin.setStartDelay(RIPPLE_ENTER_DELAY);
final ObjectAnimator opacity = ObjectAnimator.ofFloat(this, OPACITY, 1);
opacity.setAutoCancel(true);
opacity.setDuration(OPACITY_ENTER_DURATION_FAST);
opacity.setInterpolator(LINEAR_INTERPOLATOR);
final AnimatorSet set = new AnimatorSet();
set.play(tweenOrigin).with(tweenRadius).with(opacity);
return set;
}
從上面創建動畫的代碼可以看到,實際上是一個組合的屬性動畫,然后自定義了三個屬性波紋半徑TWEEN_RADIUS
、波紋中心點TWEEN_ORIGIN
和波紋的不透明度OPACITY
。通過這三個屬性的過渡變化得到一個復合的動畫。以上就是前景波紋動畫效果的實現過程。
private void setBackgroundActive(boolean active, boolean focused) {
if (mBackgroundActive != active) {
mBackgroundActive = active;
if (active) {
tryBackgroundEnter(focused);
} else {
tryBackgroundExit();
}
}
}
mBackground
是RippleBackground類的實例,與RippleForeground不同的是,背景動畫只是改變了不透明度。
@Override
protected Animator createSoftwareEnter(boolean fast) {
// Linear enter based on current opacity.
final int maxDuration = fast ? OPACITY_ENTER_DURATION_FAST : OPACITY_ENTER_DURATION;
final int duration = (int) ((1 - mOpacity) * maxDuration);
final ObjectAnimator opacity = ObjectAnimator.ofFloat(this, OPACITY, 1);
opacity.setAutoCancel(true);
opacity.setDuration(duration);
opacity.setInterpolator(LINEAR_INTERPOLATOR);
return opacity;
}
以上分析的都是手指觸摸view時產生的enter波紋動畫,當手指抬起時state也會改變,會產生一個exit動畫,這里就不詳細分析了。
二、使用揭露效果
效果圖:
當需要顯示或隱藏一組UI元素時,揭露動畫可為用戶提供視覺連續性。[ViewAnimationUtils.createCircularReveal()](https://developer.android.com/reference/android/view/ViewAnimationUtils.html#createCircularReveal(android.view.View, int, int, float, float))方法能夠為裁剪區域添加動畫以揭露或隱藏視圖。
/* @param view The View will be clipped to the animating circle.要隱藏或顯示的view
* @param centerX The x coordinate of the center of the animating circle, relative to <code>view</code>.動畫開始的中心點X
* @param centerY The y coordinate of the center of the animating circle, relative to <code>view</code>.動畫開始的中心點Y
* @param startRadius The starting radius of the animating circle.動畫開始半徑
* @param endRadius The ending radius of the animating circle.動畫結束半徑
*/
public static Animator createCircularReveal(View view,
int centerX, int centerY, float startRadius, float endRadius) {
return new RevealAnimator(view, centerX, centerY, startRadius, endRadius);
}
RevealAnimator和之前的動畫使用沒什么區別,同樣可以設置監聽器和加速器來實現各種各樣的特效,該動畫主要用在隱藏或者顯示一個view,改變view的大小等過渡效果。
顯示view:
final TextView tv9 = (TextView) findViewById(R.id.tv9);
findViewById(R.id.content_main).setOnClickListener(new View.OnClickListener() {
@Override public void onClick(View v) {
// get the center for the clipping circle
int cx = (tv9.getRight() - tv9.getLeft()) / 2;
int cy = (tv9.getBottom() - tv9.getTop()) / 2;
// get the final radius for the clipping circle
int finalRadius = Math.max(tv9.getWidth(), tv9.getHeight());
// create the animator for this view (the start radius is zero)
final Animator anim = ViewAnimationUtils.createCircularReveal(tv9, cx, cy, 0, finalRadius);
tv9.setVisibility(View.VISIBLE);
anim.start();
}
});
隱藏view:
final TextView tv9 = (TextView) findViewById(R.id.tv9);
tv9.setOnClickListener(new View.OnClickListener() {
@Override public void onClick(View v) {
// get the center for the clipping circle
int cx = (tv9.getRight() - tv9.getLeft()) / 2;
int cy = (tv9.getBottom() - tv9.getTop()) / 2;
// get the final radius for the clipping circle
int initRadius = Math.max(tv9.getWidth(), tv9.getHeight());
// create the animator for this view (the start radius is zero)
final Animator anim = ViewAnimationUtils.createCircularReveal(tv9, cx, cy, initRadius, 0);
anim.addListener(new AnimatorListenerAdapter() {
@Override public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
// make the view visible and start the animation
tv9.setVisibility(View.INVISIBLE);
}
});
anim.start();
}
});
沿著中心縮小:
Animator animator = ViewAnimationUtils.createCircularReveal(view, view.getWidth() / 2, view.getHeight() / 2, view.getWidth(), 0);
animator.setInterpolator(new LinearInterpolator());
animator.setDuration(1000);
animator.start();
從左上角擴展:
Animator animator = ViewAnimationUtils.createCircularReveal(view,0,0,0,(float) Math.hypot(view.getWidth(), view.getHeight()));
animator.setDuration(1000);
animator.start();
三、使用轉場動畫
效果圖以共享元素的轉場動畫為例:
MaterialDesign應用中的操作行為轉換透過通用元素之間的移動和轉換提供不同狀態之間的視覺連接。可為進入、退出轉換以及操作行為之間的共享元素轉換指定定制動畫。在5.0之前,我們可以在startActivity之后調用overridePendingTransition來指定Activity的轉場動畫。
- 進入轉換將決定操作行為中視圖如何進入場景。例如,在分解進入轉換中,視圖將從屏幕外進入場景并飛往屏幕中心。
- 退出轉換將決定操作行為中應用行為的顯示視圖如何退出場景。例如,在分解退出轉換中,視圖將從屏幕中心退出場景。
- 共享元素轉換將決定兩個操作行為轉換之間共享的視圖如何在這些操作行為中轉換。 例如,如果兩個操作行為擁有相同的圖像,但其位置與大小不同,changeImageTransform共享元素轉換將在這些操作行為之間平滑地轉換與縮放圖像。
Android 5.0(API Level 21)支持這些進入與退出轉換:(普通過渡動畫)
- 分解 - 從場景中心移入或移出視圖。
- 滑動 - 從場景邊緣移入或移出視圖。
- 淡入淡出 - 通過調整透明度在場景中增添或移除視圖。
也支持這些共享元素轉換:(共享元素的過渡動畫)
- changeBounds - 為目標視圖的大小添加動畫。
- changeClipBounds - 為目標視圖的裁剪大小添加動畫。
- changeTransform - 為目標視圖的縮放、旋轉和位移添加動畫。
- changeImageTransform - 為目標圖片的縮放、旋轉和位移添加動畫。
指定轉場動畫
要想使用新的轉場動畫,可以繼承Material Design主題后在style風格中指定:
<!-- 允許使用transitions -->
<item name="android:windowContentTransitions">true</item>
<!-- 指定進入、退出、返回、重新進入時的transitions -->
<item name="android:windowEnterTransition">@transition/explode</item>
<item name="android:windowExitTransition">@transition/explode</item>
<item name="android:windowReturnTransition">@transition/explode</item>
<item name="android:windowReenterTransition">@transition/explode</item>
<!-- 指定進入、退出、返回、重新進入時的共享transitions -->
<item name="android:windowSharedElementEnterTransition">@transition/change_image_transform</item>
<item name="android:windowSharedElementExitTransition">@transition/change_image_transform</item>
<item name="android:windowSharedElementReturnTransition">@transition/change_image_transform</item>
<item name="android:windowSharedElementReenterTransition">@transition/change_image_transform</item>
其中,change_image_transform定義如下:
<!-- res/transition/change_image_transform.xml -->
<!-- (see also Shared Transitions below) -->
<transitionSet xmlns:android="http://schemas.android.com/apk/res/android">
<changeImageTransform/>
</transitionSet>
如果要帶代碼中開啟窗口內容轉換,需要調用Window.requestFeature()
方法。
// 允許使用transitions
getWindow().requestFeature(Window.FEATURE_CONTENT_TRANSITIONS);
// 指定進入、退出、返回、重新進入時的transitions
getWindow().setEnterTransition(new Explode());
getWindow().setExitTransition(new Explode());
getWindow().setReturnTransition(new Explode());
getWindow().setReenterTransition(new Explode());
// 指定進入、退出、返回、重新進入時的共享transitions
getWindow().setSharedElementEnterTransition(new ChangeTransform());
getWindow().setSharedElementExitTransition(new ChangeTransform());
getWindow().setSharedElementReturnTransition(new ChangeTransform());
getWindow().setSharedElementReenterTransition(new ChangeTransform());
普通轉場動畫:
所有繼承自visibility類都可以作為進入、退出的過度動畫。如果我們想自定義進入和退出時的動畫效果,只需要繼承Visibility,重載onAppear和onDisappear方法來定義進入喝退出的動畫。系統提供了三種默認方式:
- explode 從屏幕中心移入或移出視圖
- slide 從屏幕邊緣移入或移出視圖
- fade 改變視圖的透明度
想在xml中指定自定義的進入、退出的過度動畫需要先對動畫進行定義:
<transition class="my.app.transition.CustomTransition"/>
注意:其中CustomTransition是我們自定義的動畫,它必須繼承自Visibility。
想以普通轉場動畫的方式啟動一個Activity,必須在startActivity函數中傳遞一個ActivityOptions的Bundle對象:
ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation(activity);
startActivity(intent, options.toBundle());
如果想讓返回也具備轉場效果,那么在返回的Activity中不要再調用finish函數,而是應該使finishAfterTransition來結束一個Activity,該函數會等待動畫執行完畢才結束該Activity。
共享轉場動畫:
如果要在兩個具有共享元素的Activity之間使用轉場動畫,那么:
- 1、在題中啟用窗口內容轉換。android:windowContentTransitions
- 2、在Theme中指定一個共享元素轉換。
- 3、將transitions定義為xml資源。
- 4、利用 android:transitionName屬性對兩個布局中的共享元素指定一個通用名稱。
- 5、使用
ActivityOptions.makeSceneTransitionAnimation()
方法。
// get the element that receives the click event
final View imgContainerView = findViewById(R.id.img_container);
// get the common element for the transition in this activity
final View androidRobotView = findViewById(R.id.image_small);
// define a click listener
imgContainerView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent intent = new Intent(this, Activity2.class);
// create the transition animation - the images in the layouts
// of both activities are defined with android:transitionName="robot"
ActivityOptions options = ActivityOptions
.makeSceneTransitionAnimation(this, androidRobotView, "robot");
// start the new activity
startActivity(intent, options.toBundle());
}
});
如果要在代碼中生成共享view,那么需要調用View.setTransitionName()
方法對兩個布局中的共享元素指定一個通用名稱。
如果有多個共享元素,則可以通過Pair進行包裝處理:
ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation(activity,
Pair.create(view1, "name1"),//這里view1、view2如果是TextView或者ImageView等,需要轉成View類型才可以
Pair.create(view2, "name2"));
startActivity(intent,.toBundle());
返回時如果需要具備轉場動畫,那么也需要用finish函數替代finishAfterTransition來結束一個Activity。
使用曲線運動
因為曲線運動和屬性動畫以及貝塞爾曲線這些東西混雜在一起,所以準備把這節拿出來單獨寫。這里就不多說了。
視圖狀態改變
Android 5.0在原有的圖片選擇器和顏色選擇器上進行了增強,不僅是控件能根據不同的狀態顯示不同的背景圖片,還能在兩種狀態切換時指定一個動畫,來增加過渡效果,吸引用戶眼球,以突出重點內容。
StateListAnimator類和圖片選擇器,顏色選擇器類似,可以根據view的狀態改變呈現不同的動畫效果,通過xml我們可以構建對應不同狀態的動畫合集,其使用方式也非常簡單,在對應的狀態指定一個屬性動畫即可:
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<set>
<objectAnimator android:propertyName="translationZ"
android:duration="200"
android:valueTo="20dp"
android:valueType="floatType"/>
</set>
</item>
<item android:state_enabled="true" android:state_pressed="false">
<set>
<objectAnimator android:propertyName="translationZ"
android:duration="200"
android:valueTo="0"
android:valueType="floatType"/>
</set>
</item>
</selector>
代碼中這樣加載即可:
TextView tv11 = (TextView) findViewById(R.id.tv11);
StateListAnimator stateLAnim = AnimatorInflater.loadStateListAnimator(this,R.drawable.selector_for_button);
tv11.setStateListAnimator(stateLAnim);
繼承了Material主題后,按鈕默認擁有了z屬性動畫。如果想取消這種默認狀態,可以把狀態動畫指定為null。
除了StateListAnimator類指定狀態切換的屬性動畫外,還可以通過AnimatedStateListDrawable來指定狀態切換的幀動畫:
<animated-selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/pressed" android:drawable="@drawable/btn_check_15" android:state_pressed="true"/>
<item android:id="@+id/normal" android:drawable="@drawable/btn_check_0"/>
<transition android:fromId="@+id/normal" android:toId="@+id/pressed">
<animation-list>
<item android:duration="20" android:drawable="@drawable/btn_check_0"/>
<item android:duration="20" android:drawable="@drawable/btn_check_1"/>
<item android:duration="20" android:drawable="@drawable/btn_check_2"/>
</animation-list>
</transition>
</animated-selector>
幀動畫的資源文件直接在xml中作為view的background即可。
矢量圖動畫
效果圖:
先在drawable中定義一張矢量圖:
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="200dp"
android:width="200dp"
android:viewportHeight="400"
android:viewportWidth="400">
<group
android:name="rotationGroup"
android:pivotX="0"
android:pivotY="0">
<path
android:name="star"
android:pathData="M 100,100 h 200 l -200 150 100 -250 100 250 z"
android:strokeColor="@color/colorPrimary"
android:strokeLineCap="round"
android:strokeWidth="10"/>
</group>
</vector>
然后在anim中定義動畫:
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<objectAnimator
android:propertyName="trimPathStart"
android:valueFrom="0"
android:valueTo="1"
android:valueType="floatType"
android:duration="2000"
android:repeatMode="reverse"
android:repeatCount="-1"
android:interpolator="@android:interpolator/accelerate_decelerate"/>
</set>
最后在drawable中定義一個animated-vector
:將動畫資源指定給drawable屬性值的矢量圖。
<?xml version="1.0" encoding="utf-8"?>
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/vector_drawable">
<target
android:name="star"
android:animation="@anim/animation"/>
</animated-vector>
注意:這里drawable屬性值是前面我們定義的矢量圖,target中name要和矢量圖中path的name一樣,animation就是前面定義的動畫資源文件。
在view的xml中使用以及在代碼中開始動畫:
<ImageView
android:id="@+id/iv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="20dp"
app:srcCompat="@drawable/anim_vector_drawable"
android:layout_gravity="center"/>
ImageView iv = (ImageView) findViewById(R.id.iv);
Drawable drawable = iv.getDrawable();
if (drawable instanceof Animatable) {
((Animatable) drawable).start();
}
文章中演示的Demo地址