Android中的動畫詳析-kotlin的demo
Android中的動畫可以分為三種,View動畫,幀動畫,以及屬性動畫,實際上幀動畫也是View動畫的一種,只不過二者表現形式不同,View動畫是通過不斷地對場景裡的動畫做影象轉換從而產生動畫效果是一種漸進式的動畫,並且View動畫支援自定義,幀動畫是通過順序的播放一系列的影象從而產生動畫效果,很明顯如果圖片過大就會造成OOM,而屬性動畫是通過動態的改變物件的屬性從而達到動畫效果,低版本無法直接使用,需要通過相容庫。
1.View動畫
view動畫的作用物件是View,它支援四種動畫效果,分別是平移,旋轉,縮放和透明度動畫
1.1 View動畫的種類
View動畫的四種變換效果對應著Animation的四個子類:TranslateAnimation、ScaleAnimation、RotateAnimation和AlphaAnimation,這四種動畫既可以通過Xml形式定義,也可以使用程式碼動態建立。
動畫名稱 | xml中的標籤 | 子類 | 效果 |
---|---|---|---|
平移動畫 | translate | TranslateAnimation | 平移效果 |
縮放動畫 | scale | ScaleAnimation | 放大或縮小View |
旋轉動畫 | rotate | RotateAnimation | 旋轉View |
透明度動畫 | alpha | AlphaAnimation | 透明度變化 |
1.2 View動畫的使用
平移 res/anim/view_translate.xml
<translate xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="1000"
android:fillAfter="true"
android:fromXDelta="0.0"
android:fromYDelta="0.0"
android:interpolator="@android:anim/anticipate_interpolator"
android:toXDelta="100.0"
android:toYDelta="100.0" />
屬性 | 含義 |
---|---|
android:fromXDelta | 表示x的起始值 |
android:fromYDelta | 表示y的起始值 |
android:toXDelta | 表示x的結束值 |
android:toYDelta | 表示y的結束值 |
android:interpolator | 表示差值器 |
android:duration | 表示動畫持續的時間 |
縮放 res/anim/view_scale.xml
<scale xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="1000"
android:fillAfter="true"
android:fromXScale="1.0"
android:fromYScale="1.0"
android:interpolator="@android:anim/anticipate_interpolator"
android:pivotX="50%"
android:pivotY="50%"
android:toXScale="0.0"
android:toYScale="0.0" />
屬性 | 含義 |
---|---|
android:fromXScale | 水平方向縮放的起始值 |
android:fromYScale | 豎直方向縮放的起始值 |
android:toXScale | 水平方向縮放的結束值 |
android:toYScale | 豎直方向縮放的結束值 |
android:pivotX | 縮放的軸點的x座標,會影響縮放的效果 |
android:pivotY | 縮放的軸點的y座標 |
軸點:縮放的中心點,預設為View的中心點,如果人為修改至View的右邊界,View的會向左邊進行縮放,反之亦然,可以自己體會一下
旋轉 res/anim/view_rotate.xml
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="1000"
android:fromDegrees="0"
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
android:pivotX="50%"
android:pivotY="50%"
android:toDegrees="+360"/>
屬性 | 含義 |
---|---|
android:fromDegrees | 旋轉開始的角度,如0 |
android:toDegrees | 旋轉結束的角度,如360,用+,-表示旋轉方向 |
android:pivotX | 旋轉的軸心點的x座標 |
android:pivotY | 旋轉的軸心點的y座標 |
軸心點:旋轉的中心點,預設為View的中心點,不同的情況,旋轉的軌跡不同,可以自己體會一下
透明度變化
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="2000"
android:fromAlpha="1.0"
android:toAlpha="0.0" />
屬性 | 含義 |
---|---|
android:fromAlpha | 透明度變化的起始值,如0.1 |
android:toAlpha | 透明度變化的結束值,如1 |
系列動畫
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@android:anim/anticipate_interpolator"
android:shareInterpolator="true"
android:duration="1000"
android:fillAfter="true">
<translate android:fromXDelta="0.0"
android:fromYDelta="0.0"
android:toXDelta="100.0"
android:toYDelta="100.0"/>
<scale android:fromXScale="1.0"
android:fromYScale="1.0"
android:toXScale="0.0"
android:toYScale="0.0"
android:pivotX="50%"
android:pivotY="50%"/>
<rotate android:pivotY="50%"
android:pivotX="50%"
android:fromDegrees="0"
android:toDegrees="+360"/>
<alpha android:fromAlpha="1.0"
android:toAlpha="0.0"/>
</set>
set標籤屬性 | 含義 |
---|---|
android:shareInterpolator | 表示動畫集合是否共享同一個差值器,true為共享 |
android:fillAfter | 是指動畫結束是畫面停留在此動畫的最後一幀 |
android:fillBefore | 是指動畫結束時畫面停留在此動畫的第一幀 |
android:interpolator | 表示動畫使用的差值器,其影響動畫的速度,可以不指定,預設為@android:anim/accelerate_interpolator |
從上面可以看到,View動畫即可以是一個耽擱動畫,也可以是一系列的動畫組成,標籤白哦是動畫的集合,對應AnimationSet類,他可以包含若干個動畫,並且它的內部可以包含其他的動畫集合。
1.3 自定義View動畫
自定義View動畫是通過繼承Animation這個抽象類,重寫他的initialize和applyTransformation方法,在initialize方法中做一些初始化的操作,然後在applyTransformation中進行相應的矩陣變換,很多時候需要通過Camera來簡化矩陣變化的過程。這裡使用了Android原生的一個3d動畫翻轉效果作為例子來看,initialize和applyTransformation方法如下:
override fun initialize(width: Int, height: Int, parentWidth: Int, parentHeight: Int) {
super.initialize(width, height, parentWidth, parentHeight)
mCamera= Camera()
}
override fun applyTransformation(interpolatedTime: Float, t: Transformation?) {
super.applyTransformation(interpolatedTime, t)
var fromDegrees=mFromDegrees
var degrees= fromDegrees!! + ((mToDegrees!! - mFromDegrees!!) * interpolatedTime)
var matix: Matrix?=t!!.matrix
mCamera!!.save()
if(mReverse!!){
mCamera!!.translate(0.0f,0.0f,mDepthZ!!*interpolatedTime)
}else{
mCamera!!.translate(0.0f,0.0f,mDepthZ!!*(1.0f-interpolatedTime))
}
if (direction!! == DIRECTION.Y) mCamera!!.rotateY(degrees) else {
mCamera!!.rotateX(degrees)
}
mCamera!!.getMatrix(matix)
mCamera!!.restore()
matix!!.preTranslate(-mCenterX!!,-mCenterY!!)
matix!!.postTranslate(mCenterX!!,mCenterY!!)
}
上面的程式碼主要是在initialize中例項化了一個Camera的物件,在applyTransformation中通過計算翻轉的角度,例項化矩陣類,然後通過Camera來簡化矩陣變化的過程。整個的demo通過kotlin來寫的。
1.4 View動畫的特殊使用場景
1.4.1 LayoutAnimation
LayoutAnimation作用於ViewGroup,為ViewGroup指定一個動畫。這樣當他的子元素出場時都會具有這種動畫效果。這種效果常常作用在ListView上,首先來看看它的實現過程:
res/anim/anim_layout.xml
<layoutAnimation xmlns:android="http://schemas.android.com/apk/res/android"
android:animationOrder="normal"
android:delay="0.5"
android:animation="@anim/anim_item"/>
res/anim/anim_item
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="500"
android:shareInterpolator="true"
android:interpolator="@android:anim/accelerate_interpolator">
<alpha android:toAlpha="0.0"
android:fromAlpha="1.0"/>
<translate android:fromYDelta="500"
android:toYDelta="0"/>
</set>
解釋一下幾個關鍵的屬性:
1. android:animationOrder :表示子元素動畫的順序,normal,reverse,random,順序,逆向和隨機播放入場動畫;
2. android:delay :表示子元素開始動畫的時間延遲
3. android:animation :指定具體的入場動畫
然後給ViewGroup指定android:layoutAnimation屬性即可。
<ListView
android:id="@+id/list_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layoutAnimation="@anim/anim_layout"
android:cacheColorHint="#00000000"
android:divider="#dddbdb"
android:dividerHeight="1.0px"
android:listSelector="@android:color/transparent"/>
也可以在程式碼中動態實現:
var animLayout : Animation = AnimationUtils.loadAnimation(this,R.anim.anim_item)
var control=LayoutAnimationController(animLayout)
control!!.delay=0.5f
control!!.order=LayoutAnimationController.ORDER_NORMAL
listView!!.layoutAnimation=control
listView!!.adapter= ArrayAdapter(this,R.layout.list_view_item,date)
1.4.2 Activity的切換效果
activity有預設的切換效果,但是這個效果我們完全可以通過自定義的方式來z實現,主要用到的是overridePeddingTransition(int enterAnim,int exitAnim)這個方法來實現,這個方法必須在startActivity()或者finish()之後呼叫才能生效。
overridePendingTransition(R.anim.push_right_in,R.anim.push_right_out)
2.幀動畫
幀動畫是順序的播放一組預先定義好的圖片來實現動畫效果,不同於View動畫,系統提供了AnimationDrawable來使用幀動畫,使用起來相對簡單,通過一個例子來了解一下:
res/drawable/frame_animation.xml
<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
android:oneshot="false">
<item android:duration="50" android:drawable="@drawable/ic2"/>
<item android:duration="50" android:drawable="@drawable/ic3"/>
<item android:duration="50" android:drawable="@drawable/ic4"/>
<item android:duration="50" android:drawable="@drawable/ic5"/>
<item android:duration="50" android:drawable="@drawable/ic6"/>
<item android:duration="50" android:drawable="@drawable/ic7"/>
<item android:duration="50" android:drawable="@drawable/ic8"/>
<item android:duration="50" android:drawable="@drawable/ic9"/>
</animation-list>
在activity中的使用方法:
iv!!.setBackgroundResource(R.drawable.frame_animation)
var frameAnim= iv!!.background as AnimationDrawable
frameAnim.start()
使用幀動畫記憶體溢位解決辦法:
幀動畫的原理就是從xml中讀取到圖片id列表後就去硬碟中找這些圖片資源,將圖片全部讀出來後按順序設定給ImageView,利用視覺暫留效果實現了動畫。一次拿出這麼多圖片,而系統都是以Bitmap點陣圖形式讀取的;而動畫的播放是按順序來的,大量Bitmap就排好隊等待播放然後釋放,然而這個排隊的地方卻很小。東西多了沒地方存,就會導致OOM,解決方法是:既然來這麼多Bitmap,一次卻只能拿走一個,那麼就翻牌子吧,輪到誰就派個執行緒去叫誰,bitmap1叫到了得叫上下一位bitmap2做準備,這樣更迭效率高一些。為了避免某個bitmap已被叫走了執行緒白跑一趟的情況,加個Synchronized同步下資料資訊,具體的優化程式碼已在demo的FrameAnimation中
3.屬性動畫
屬性動畫是API11也就是Android3.0之後新加入的特性,他對作用的的物件進行了擴充套件,屬性動畫可以對任何物件做動畫,甚至可以沒有物件,並且作用的效果也得到了加強
2.1 使用屬性動畫
屬性動畫可以對任意物件的屬性進行動畫而不僅僅是View動畫,動畫預設時間間隔300ms,預設幀率10ms/幀,其可以達到在一個時間間隔內完成物件從一個屬性值到另一個屬性值的改變。舉幾個使用屬性動畫的小例子:
- 改變一個物件的translationY屬性,讓其沿著Y軸向上平移一段距離:
var ofFloat = ObjectAnimator.ofFloat(view!!,"translationY", (-view!!.height).toFloat()) as ObjectAnimator
ofFloat!!.start()
- 改變一個物件的背景色屬性,典型的情形是改變View的背景色。
var value :ValueAnimator = ObjectAnimator.ofInt(view!!,"backgroundColor", Color.RED,Color.BLUE)
value!!.duration = 500
value!!.setEvaluator(ArgbEvaluator())
value!!.repeatMode=ValueAnimator.REVERSE
value!!.repeatCount=ValueAnimator.INFINITE
value!!.start()
- 動畫集合,達到一個平移加翻轉的效果
var animSet= AnimatorSet()
animSet!!.playTogether(
ObjectAnimator.ofFloat(view!!,"rotationX",0f,360f),
ObjectAnimator.ofFloat(view!!,"rotationY",0f,180f),
ObjectAnimator.ofFloat(view!!,"rotation",0f,-90f),
ObjectAnimator.ofFloat(view!!,"translationX",0f,90f),
ObjectAnimator.ofFloat(view!!,"translationY",0f,90f),
ObjectAnimator.ofFloat(view!!,"scaleX",1f,1.5f),
ObjectAnimator.ofFloat(view!!,"scaleY",1f,0.5f),
ObjectAnimator.ofFloat(view!!,"alpha",1f,0.25f,1f)
)
animSet!!.duration=5*1000
animSet!!.start()
這是在程式碼中直接實現,也可以和View動畫一樣在XMl中實現,定義位置在res/animator/目錄下,實現過程較為較為簡單,不再贅述。
2.2屬性動畫中的重要屬性介紹
- android:propertyName –表示屬性動畫的作用物件的屬性的名稱
- android:duration –表示動畫的時長
- android:valueFrom –表示屬性的起始值
- android:valueTo –表示屬性的結束值
- android:startOffset –表示動畫的延遲時間,當動畫開始後,需要延遲多少毫秒才會真正播放此動畫
- android:repeatCount –表示動畫的重複次數 預設為1,-1為無限迴圈
- android:repeatMode – 表示動畫的重複模式 restart 連續重複,reverse 逆向重複
- android:valueType – 表示android:propertyName所指定的屬性的型別,有整型和浮點型
在實際開發中,通常使用動態的在程式碼中使用屬性動畫,更方便和簡單。
2.3 插值器和估值器
屬 性 | TimeInterpolator | TypeEvaluator |
---|---|---|
名稱 | 時間差值器 | 型別估值演算法,也叫估值器 |
作用 | 是根據時間的流逝的百分比來計算出當前屬性值改變的百分比 | 根據當前屬性改變的百分比來計算改變後的屬性值 |
系統中預置的例子 | AccelerateDecelerateInterpolator(加速減速差值器,兩頭慢中間快),LinearInterpolator(線性差值器)等等 | IntEvaluator(針對整型屬性),FloatEvaluator(針對浮點型屬性),ArgbEvaluator(針對Color屬性)等。 |
屬性動畫中二者都是非常重要的,他們是實現非勻速的重要手段。我們看一下TimeInterpolator和以及TypeEvaluator的原始碼:
public interface TimeInterpolator {
float getInterpolation(float input);
}
public interface TypeEvaluator<T> {
public T evaluate(float fraction, T startValue, T endValue);
}
上面的原始碼只是將註釋刪掉了,可以看到,我們要是想自己定義差值器和估值器的話,只需要派生一個類實現介面就可以了,然後就可以做出千奇百怪的動畫效果。
2.4屬性動畫的監聽器
屬性動畫提供了監聽器用於監聽動畫的播放過程,主要有如下兩個介面,AnimatorUpdateListener和 AnimatorListener。
public static interface AnimatorListener {
/**
* @param 開始動畫
* @param isReverse 表示動畫是否是扭轉
*/
default void onAnimationStart(Animator animation, boolean isReverse) {
onAnimationStart(animation);
}
/**
* @param 結束動畫
* @param isReverse 表示動畫是否是扭轉
*/
default void onAnimationEnd(Animator animation, boolean isReverse) {
onAnimationEnd(animation);
}
void onAnimationStart(Animator animation);
void onAnimationEnd(Animator animation);
void onAnimationCancel(Animator animation);
void onAnimationRepeat(Animator animation);
}
從這個介面的原始碼可以看出,他可以監聽動畫的開始,結束,取消,重複,同時為了方便,系統還提供了了AnimatorListenerAdapter介面卡,可以有選擇的實現上面的方法。接著來看AnimatorUpdateListener
public static interface AnimatorUpdateListener {
void onAnimationUpdate(ValueAnimator animation);
}
AnimatorUpdateListener會監聽動畫的整個過程,動畫是由很多幀組成的,每播放一幀,onAnimationUpdate就會被呼叫一次。
2.5 對任意的屬性做動畫
屬性動畫的原理:屬性動畫要求作用的物件提供該屬性的set和get方法,然後根據外界傳遞的該屬性的初始值和最終值,以動畫的效果多次去呼叫set方法,每次傳遞給set方法的值不一樣,確切的來說就是隨著時間的推移,所傳遞的值越來越接近最終值。如果想讓動畫生效,要滿足兩個條件
- object必須要提供set方法,如果動畫的時候沒有傳遞初始值,那麼還需要提供get方法,因為系統要去取屬性的初始值。
- object的set方法對屬性所做的改變必須能夠通過某種方法反映出來,比如會帶來UI的改變之類的。
上面的條件第一個是必須要滿足的,如果不滿足,直接Cash,第二個條件不滿足,動畫會無效果但不會報Crash,針對以上兩個條件,官方文件提供了三種解決方式
1. 如果你有許可權,給你的物件加上get和set方法
2. 用一個類來包裹原始物件,間接提供get和set方法
/**
* 讓Button的寬度在2秒鐘之內增加到200px
*/
private fun sample4() {
var wrapper= ViewWrapper(btn4 as View)
ObjectAnimator.ofInt(wrapper!!,"width",200).setDuration(2000).start()
}
private class ViewWrapper constructor(target:View){
private var mTarget : View?=null
init {
this.mTarget=target
}
fun getWidth() : Int{
return mTarget!!.layoutParams.width
}
fun setWidth(width:Int){
mTarget!!.layoutParams.width=width
mTarget!!.requestLayout()
}
}
由於Button是繼承自TextView的,而TextView的setWidth方法的作用並不是設定View的寬度,而是設定TextView的最大寬度和最小寬度的,所以直接給Button新增改變其寬度的屬性動畫剛開始是沒效果的,因為它不滿足第二個條件,因此通過用一個 ViewWrapper將其包裹起來,為其增加set和get方法,故而動畫開始有效。
3. 採用ValueAnimator,監聽動畫過程,自己實現屬性的改變
private fun sample5(target: View,start:Int,end:Int) {
var value=ValueAnimator.ofInt(1,100)
value.addUpdateListener(ValueAnimator.AnimatorUpdateListener {
var intEvaluator= IntEvaluator()
//獲得當前的進度值
var currentValue:Int= it.animatedValue as Int
Log.e("sample5", "當前的進度$currentValue")
//獲得當前進度佔整個動畫的比例
var fraction:Float=it.animatedFraction
target.layoutParams.width=intEvaluator.evaluate(fraction,start,end)
target.requestLayout()
})
value.setDuration(2000).start()
}
2.6屬性動畫的工作原理
屬性動畫要求作用的物件提供該屬性的set方法,屬性動畫根據你傳遞的該屬性的初始值和最終值,以動畫的效果多次呼叫set方法,每次傳遞給set方法的值都不一樣,確切的來說是隨著時間的推移,所傳遞的值越來越接近最終值。如果動畫沒有傳遞初始值,那麼還要提供get方法,因為系統要去獲取該屬性的初始值。對於屬性動畫來說,其動畫過程所做的就這麼多。分析一下原始碼:
從ObjectAnimator的start方法作為入口:
@Override
public void start() {
AnimationHandler.getInstance().autoCancelBasedOn(this);
if (DBG) {
Log.d(LOG_TAG, "Anim target, duration: " + getTarget() + ", " + getDuration());
for (int i = 0; i < mValues.length; ++i) {
PropertyValuesHolder pvh = mValues[i];
Log.d(LOG_TAG, " Values[" + i + "]: " +
pvh.getPropertyName() + ", " + pvh.mKeyframes.getValue(0) + ", " +
pvh.mKeyframes.getValue(1));
}
}
super.start();
}
可以看到這個方法也不長,分為兩段,一個是 AnimationHandler.getInstance().autoCancelBasedOn(this);if裡面的是log日誌,接著就呼叫了父類的start()方法,所以點進去看一下autoCancelBasedOn方法
void autoCancelBasedOn(ObjectAnimator objectAnimator) {
for (int i = mAnimationCallbacks.size() - 1; i >= 0; i--) {
AnimationFrameCallback cb = mAnimationCallbacks.get(i);
if (cb == null) {
continue;
}
if (objectAnimator.shouldAutoCancel(cb)) {
((Animator) mAnimationCallbacks.get(i)).cancel();
}
}
}
這個方法也很簡單,首先mAnimationCallbacks是一個存有AnimationFrameCallback的ArrayList的集合,這個方法主要是取出集合中的AnimationFrameCallback,判斷是否有和當前動畫相同的動畫,如果有,將其取消。接著來看ValueAnimator中的start()方法
private void start(boolean playBackwards) {
//可以看出屬性動畫需執行在有Looper的執行緒中
if (Looper.myLooper() == null) {
throw new AndroidRuntimeException("Animators may only be run on Looper threads");
}
......
//當start()方法被呼叫之後重置mLastFrameTime,如果動畫正在執行,呼叫start()讓動畫處於已經開始但尚未達到第一幀的階段
mLastFrameTime = -1;
mFirstFrameTime = -1;
mStartTime = -1;
addAnimationCallback(0);
//沒有開始延遲
if (mStartDelay == 0 || mSeekFraction >= 0 || mReversing) {
//初始化動畫,並通知開始監聽並與之前的行為一致,否則,推遲初始化直到第一幀開始延遲後
startAnimation();
if (mSeekFraction == -1) {
setCurrentPlayTime(0);
} else {
setCurrentFraction(mSeekFraction);
}
}
}
這個方法主要的還是來看一下setCurrentFraction(mSeekFraction);
public void setCurrentFraction(float fraction) {
//初始化
initAnimation();
...... 省略
animateValue(currentIterationFraction);
}
看一下初始化
@CallSuper
void initAnimation() {
if (!mInitialized) {
int numValues = mValues.length;
for (int i = 0; i < numValues; ++i) {
mValues[i].init();
}
mInitialized = true;
}
}
這裡我們可以看到mValues通過JNI的方式被init初始化了,而mValues是一個PropertyValuesHolder的陣列,這裡我們進入PropertyValuesHolder類找到setupValue和setAnimatedValue的方法,我們可以看到我們最關心的set和get通過反射的凡是來呼叫了。
private void setupValue(Object target, Keyframe kf) {
if (mProperty != null) {
//屬性的初始值沒有被提供,get方法將會呼叫
Object value = convertBack(mProperty.get(target));
kf.setValue(value);
} else {
try {
if (mGetter == null) {
Class targetClass = target.getClass();
setupGetter(targetClass);
if (mGetter == null) {
// Already logged the error - just return to avoid NPE
return;
}
}
Object value = convertBack(mGetter.invoke(target));
kf.setValue(value);
} catch (InvocationTargetException e) {
Log.e("PropertyValuesHolder", e.toString());
} catch (IllegalAccessException e) {
Log.e("PropertyValuesHolder", e.toString());
}
}
}
//當動畫的下一幀來的時候。這個方法會將新的屬性值設定給物件,呼叫其set方法。
void setAnimatedValue(Object target) {
if (mProperty != null) {
mProperty.set(target, getAnimatedValue());
}
if (mSetter != null) {
try {
mTmpValueArray[0] = getAnimatedValue();
mSetter.invoke(target, mTmpValueArray);
} catch (InvocationTargetException e) {
Log.e("PropertyValuesHolder", e.toString());
} catch (IllegalAccessException e) {
Log.e("PropertyValuesHolder", e.toString());
}
}
}
然後接著來看setCurrentFraction中的animateValue(currentIterationFraction);
@CallSuper
void animateValue(float fraction) {
fraction = mInterpolator.getInterpolation(fraction);
mCurrentFraction = fraction;
int numValues = mValues.length;
for (int i = 0; i < numValues; ++i) {
//計算每幀動畫所對應的的屬性的值
mValues[i].calculateValue(fraction);
}
//動畫的進度的狀態監聽
if (mUpdateListeners != null) {
int numListeners = mUpdateListeners.size();
for (int i = 0; i < numListeners; ++i) {
mUpdateListeners.get(i).onAnimationUpdate(this);
}
}
}
這樣整個過程就看完了。附上文中使用的demo地址,有需要的話可以下載下來,自己的微信公眾號,偶爾更新文章,生活感悟,好笑的段子,歡迎訂閱