今日頭條文字漸變特效項目實戰(二):視圖動畫與屬性動畫

1、視圖動畫Animation (Tween Animation)

通過不斷的對view對象做圖像變換(漸變、平移、縮放、旋轉)而產生動畫效果,這種動畫只適用于View對象。
1、AlphaAnimation

AlphaAnimation(float fromAlpha, float toAlpha)

其中,1:不透明;0:透明

2、RotateAnimation

RotateAnimation(float fromDegrees, float toDegrees)
---
RotateAnimation(float fromDegrees, float toDegrees, float pivotX, float pivotY)
---
RotateAnimation(float fromDegrees, float toDegrees, int pivotXType, float pivotXValue, int pivotYType, float pivotYValue)

注意:

  • 旋轉的中心點,默認是view的左上角;
  • pivotX、pivotY,是旋轉的中心點的偏移量;
  • pivotXType的取值可以是RELATIVE_TO_PARENT、RELATIVE_TO_SELF,代表的是中心點(默認是view的左上角)偏移量大小的取值是相對于自身還是相對于parent,最終都是相對于view的左上角進行偏移。
image
image

即旋轉中心點從view的左上角,偏移到了右下角(相對于parent50%的地方)

image

3、TranslateAnimation

TranslateAnimation(float fromXDelta, float toXDelta, float fromYDelta, float toYDelta)
---
TranslateAnimation(int fromXType, float fromXValue, int toXType, float toXValue,
        int fromYType, float fromYValue, int toYType, float toYValue) 

4、ScaleAnimation

ScaleAnimation(float fromX, float toX, float fromY, float toY)
---
ScaleAnimation(float fromX, float toX, float fromY, float toY, 
       float pivotX, float pivotY) 
---
ScaleAnimation(float fromX, float toX, float fromY, float toY,
        int pivotXType, float pivotXValue, int pivotYType, float pivotYValue)

相對于某個中心點縮放,和RotateAnimation類似

image

代碼如下:

    //透明動畫效果
    AlphaAnimation alphaAnimation=new AlphaAnimation(0,1);
    alphaAnimation.setDuration(1000);
    view.startAnimation(alphaAnimation);

---

    //旋轉動畫效果
    RotateAnimation rotateAnimation=new RotateAnimation(0,360,RotateAnimation.RELATIVE_TO_PARENT,0.1f,
                        RotateAnimation.RELATIVE_TO_PARENT,0.1f);
    rotateAnimation.setDuration(1000);
    v.startAnimation(rotateAnimation);

---

    //平移動畫效果
    TranslateAnimation translateAnimation=new TranslateAnimation(TranslateAnimation.RELATIVE_TO_PARENT,0,
            TranslateAnimation.RELATIVE_TO_PARENT,0.2f,
            TranslateAnimation.RELATIVE_TO_PARENT,0,
            TranslateAnimation.RELATIVE_TO_PARENT,0.2f);
    translateAnimation.setDuration(1000);
    v.startAnimation(translateAnimation);

---

    //縮放動畫效果
    ScaleAnimation scaleAnimation=new ScaleAnimation(1,0,1,0,ScaleAnimation.RELATIVE_TO_PARENT,0.2f,
                    ScaleAnimation.RELATIVE_TO_PARENT,0.2f);
    scaleAnimation.setDuration(2000);
    v.startAnimation(scaleAnimation);   
---

 v.startAnimation(AnimationUtils.loadAnimation(this,R.anim.rotate));

    //透明動畫效果
    <alpha android:duration="1000"
        android:fromAlpha="1"
        android:toAlpha="0"/>

--- 

    //旋轉動畫效果
    <rotate android:duration="1000"
        android:fromDegrees="0"
        android:toDegrees="360"
        android:repeatCount="infinite"
        android:repeatMode="reverse"
        android:pivotX="50%"
        android:pivotY="50%"/>

---

    //平移動畫效果
    <translate android:duration="1000"
        android:fromXDelta="0"
        android:toXDelta="20%p"
        android:fromYDelta="0"
        android:toYDelta="30%p"/>

注意:

  • num%、num%p表示相對于自身或者父控件,跟RELATIVE_TO_PARENT、RELATIVE_TO_SELF效果類似;如果以浮點數字表示,是一個絕對值,代表相對自身原始位置的像素值;
  • RepeatMode:
    Animation.RESTART,表示動畫重新從頭開始執行,當一次動畫執行結束之后,圖片將重新從頭開始執行。
    Animation.REVERSE,表示動畫反方向執行,當一次動畫執行結束之后,圖片將向反方向運動。
  • RepeatCount:動畫的重復次數,Animation.INFINITE 無限循環
  • fillBefore:動畫結束時畫面停留在此動畫的第一幀
    fillAfter:動畫結束是畫面停留在此動畫的最后一幀
  • 加載xml文件中的動畫
    AnimationUtils類調用loadAnimation(context, id)方法來加載xml文件

5、可以通過AnimationSet,將動畫以組合的形式展示

        AnimationSet animationSet=new AnimationSet(true);
        animationSet.setDuration(2000);

        ScaleAnimation scaleAnimation=new ScaleAnimation(1,0,1,0,ScaleAnimation.RELATIVE_TO_PARENT,0.2f,
              ScaleAnimation.RELATIVE_TO_PARENT,0.2f);
        scaleAnimation.setDuration(2000);
        animationSet.addAnimation(scaleAnimation);

        ...

        v.startAnimation(animationSet);

image

6、自定義動畫
實現Animation 接口的applyTransformation()方法,還可以覆蓋initialize()方法,做一些初始化的操作

public class ReversalAnimation extends Animation {
    private int height;
    private int width;

    @Override
    public void initialize(int width, int height, int parentWidth, int parentHeight) {
        super.initialize(width, height, parentWidth, parentHeight);
        setDuration(1000);
        setInterpolator(new CycleInterpolator(0.25f));
        this.height = height * 2;
        this.width = width;
    }

    @Override
    protected void applyTransformation(float interpolatedTime, Transformation t) {
        Matrix matrix = t.getMatrix();
        matrix.preScale(1 - (float) Math.sin((interpolatedTime) * Math.PI), 1, width / 2, 0);
        matrix.preTranslate(0, -(float) Math.sin(interpolatedTime * Math.PI) * height);
    }
}

其中interpolatedTime的取值為0~1,0標識動畫開始執行,1標識動畫執行完了
Transformation,可以通過它獲得當前矩陣對象matrix,通過matrix將動畫效果執行出來

image

7、插值器
如下

8、監聽器
可以設置AnimationListener,獲取動畫開始、結束、重復事件

    translateAnimation.setAnimationListener(new Animation.AnimationListener() {
        @Override
        public void onAnimationStart(Animation animation) {
            Toast.makeText(Main2Activity.this,"onAnimationStart",Toast.LENGTH_SHORT).show();
        }

        @Override
        public void onAnimationEnd(Animation animation) {
            Toast.makeText(Main2Activity.this,"onAnimationEnd",Toast.LENGTH_SHORT).show();
        }

        @Override
        public void onAnimationRepeat(Animation animation) {

        }
    });

9、執行動畫
通過view的startAnimation

但是視圖動畫不具備交互性,在縮放和平移一個View的時候,并沒有改變控件內部的屬性值,它的有效點擊區域,依然保持原來的大小和位置,如圖在動畫的結束位置點擊,是沒有效果的,只有在原始位置(矩形區域),點擊才有效果

image

參考:Android 動畫學習 View Animation

2、屬性動畫Animator

在Android3.0(level 11)引入的,通過動態地改變對象的屬性值而達到動畫效果,可以為任何的對象添加動畫(當然也包括View在內)。
屬性動畫改變了View的屬性,比如對于View的最終位置x=left+translationX,所以在最終位置就可以響應點擊事件;

image

1、ValueAnimator

ValueAnimator本身不提供任何的動畫效果,它可以產生有一定規律的數字,可以在AnimatorUpdateListener中監聽這些數字的變化,從而完成動畫效果

image

ValueAnimator動畫的整個過程如下:
(0)、 ofInt(0,400)表示指定動畫的數字區間,是從0運動到400;
(1)、 在動畫過程中, Animator會根據動畫總時間和已進行的時間自動計算出一個時間比例因子,大小介于0和1之間,0表示開始。

時間比例因子 = 當前已進行的時間/動畫執行的總時間

(2)、Interpolator(差值器/加速器):Animator會根據時間比例因子,返回當前動畫進度所對應的數字進度:這個數字進度是百分制的,以小數表示。

publicinterfaceTimeInterpolator{
floatgetInterpolation(floatinput);
}

但是我們通過監聽器拿到的是當前動畫所對應的具體數值,而不是百分制的進度。那么就必須有一個地方會根據當前的數字進度,將其轉化為對應的數值,這個地方就是Evaluator;
(3)、 Evaluator:Evaluator將從加速器返回的數字進度轉成對應的數字值。

publicinterfaceTypeEvaluator<T>{
T  evaluate(floatfraction,T startValue,T endValue)
}

(4)、 監聽器:通過在AnimatorUpdateListener監聽器使用animation.getAnimatedValue()函數拿到Evaluator中返回的數字值,為需要執行動畫的對象設置對應屬性的屬性值。onAnimationUpdate()在動畫每播放一幀時都會調用。

動畫在的整個過程中,會根據我們當初設置的TimeInterpolator 和TypeEvaluator的計算方式計算出的不同的屬性值,從而不斷地改變對象屬性值的大小,進而產生各式各樣的動畫效果。

創建ValueAnimator的常用方法如下:

  • ValueAnimator ofInt(int... values)
  • ValueAnimator ofArgb(int... values)
  • ValueAnimator ofFloat(float... values)
  • ValueAnimator ofPropertyValuesHolder(PropertyValuesHolder... values)

這些方法都有默認的加速器和Evaluator,不指定則使用默認的;
其中ofInt()的默認Evaluator是IntEvaluator,ofFloat是FloatEvalutor,ofArgb是ArgbEvaluator;
參數的類型是定值

代碼如下:

    ValueAnimator valueAnimator=ValueAnimator.ofFloat(0,1);
    valueAnimator.setTarget(v);
    valueAnimator.setDuration(1000);
    valueAnimator.start();
    valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            float timeAnim= (float) animation.getAnimatedValue();
            if(timeAnim<0.4f){
                imageView.setTranslationY(-1000*timeAnim);
            }else if(timeAnim>=0.4f && timeAnim<=0.8f){
                imageView.setScaleX((float) Math.abs((timeAnim-0.6)/0.2));
            }else {
                imageView.setTranslationY(-1000*(1-timeAnim));
            }
        }
    });

image
  • ValueAnimator ofObject(TypeEvaluator evaluator, Object... values)
    ofInt()只能傳入Integer類型的值,而ofFloat()只能傳入Float類型的值。如果我們需要操作其它類型的變量,可以使用以上方法

TypeEvaluator

public class PointXYEvaluator implements TypeEvaluator<PointXY> {
    @Override
    public PointXY evaluate(float fraction, PointXY startValue, PointXY endValue) {
        int pointX= (int) (startValue.getPointX()+(endValue.getPointX()-startValue.getPointX())*fraction);
        int pointy= (int) (startValue.getPointY()+(endValue.getPointY()-startValue.getPointY())*fraction);
        return new PointXY(pointX,pointy);
    }
}

自定義參數類型

public class PointXY {
    private int pointX;
    private int pointY;

    public PointXY(int pointX, int pointY) {
        this.pointX = pointX;
        this.pointY = pointY;
    }

    public int getPointX() {
        return pointX;
    }

    public void setPointX(int pointX) {
        this.pointX = pointX;
    }

    public int getPointY() {
        return pointY;
    }

    public void setPointY(int pointY) {
        this.pointY = pointY;
    }
}

使用

ValueAnimator valueAnimator=ValueAnimator.ofObject(new PointXYEvaluator(),
            new PointXY(pointXY.getLeft(),pointXY.getTop()),new PointXY(pointXY.getLeft()+200,pointXY.getTop()+200));
    valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            PointXY pointValue= (PointXY) animation.getAnimatedValue();
            pointXY.setTranslationX(pointValue.getPointX());
            pointXY.setTranslationY(pointValue.getPointY());
        }
    });

    valueAnimator.start();

image

參考:
自定義控件三部曲之動畫篇(六)——ValueAnimator高級進階(二)
Android動畫學習(三)之使用ValueAnimator和ObjectAnimator實現動畫實例

2、 ObjectAnimator

image

在ObjectAnimator中,則是先根據屬性值拼裝成對應的set函數的名字,比如這里的scaleY的拼裝方法就是將屬性的第一個字母強制大寫后,與set拼接,所以就是setScaleY。然后通過反射找到對應控件的setScaleY(float scaleY)函數,將當前數字值做為setScaleY(float scale)的參數將其傳入。

常用的構造函數有:

  • ObjectAnimator ofFloat(Object target, String propertyName, float... values)
    其中,target指定這個動畫要操作的是哪個控件
    propertyName指定這個動畫要操作這個控件的哪個屬性
    values是可變長參數,就是指這個屬性值是從哪變到哪
  • ObjectAnimator ofFloat(Object target, String xPropertyName, String yPropertyName,Path path)
  • ObjectAnimator ofObject(Object target, String propertyName, TypeEvaluator evaluator, Object... values)

代碼如下:

ObjectAnimator translationAnimator=ObjectAnimator.ofFloat(view,"translationY",0,-400).setDuration(400);
translationAnimator.start();

---

Path translationPath=new Path();
translationPath.moveTo(0,0);
translationPath.lineTo(200, 300);
ObjectAnimator translationAnimator=ObjectAnimator.ofFloat(v,"translationX","translationY",translationPath);
translationAnimator.start();

也可以在xml中進行配置

    <objectAnimator android:duration="400"
        android:propertyName="translationY"
        android:valueFrom="0dp"
        android:valueTo="-200dp"
        android:valueType="floatType"/>

---

    Animator animator= AnimatorInflater.loadAnimator(Main2Activity.this,R.animator.object_animator);
    animator.setTarget(v);
    animator.start();

1、View屬性動畫默認的屬性值:translationX、translationY、x 、 y(view對象在它容器中的最終位置)、rotation、rotationX、rotationY、scaleX、scaleY、pivotX、pivotY(旋轉和縮放都是以此為中心展開的,缺省值是 View 對象的中心點)、alpha...
2、如果在調用 ObjectAnimator 的某個工廠方法時,我們只為 values... 參數指定了一個值,那此值將被認定為動畫屬性的結束值。 這樣的話,動畫顯示的屬性必須帶有一個 getter 方法,用于獲取動畫的起始值。
3、還可以自定義屬性值,但要在操作的控件中,實現對應的屬性的set方法

  • ObjectAnimator ofObject(T target, @NonNull Property<T, V> property, @Nullable TypeConverter<PointF, V> converter, Path path)

  • <T> ObjectAnimator ofFloat(T target, Property<T, Float> xProperty, Property<T, Float> yProperty, Path path)

  • <T, V> ObjectAnimator ofObject(T target, @NonNull Property<T, V> property, @Nullable TypeConverter<PointF, V> converter, Path path)
    其中Property<T, V>是一個屬性包裝器,如下

    image

自定義Property,代碼如下:

public class ImgMoveView extends View {
    private static final String PROPERTY_NAME="position";
    public ImgMoveView(Context context) {
        super(context);
    }
    public ImgMoveView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    public void setPosition(PointF position){
        int x = Math.round(position.x);
        int y = Math.round(position.y);
        setTranslationX(x);
        setTranslationY(y);
    }
    public PointF getPosition(){
        return new PointF(getX(),getY());
    }
    /**
     * A Property wrapper around the position functionality handled by the
     * ImgMoveView#setPosition(PointF) and ImgMoveView#getPosition() methods.
     */
    public static final Property<ImgMoveView, PointF> POSITION = new Property<ImgMoveView, PointF>(PointF.class,PROPERTY_NAME) {
        @Override
        public PointF get(ImgMoveView object) {
            return object.getPosition();
        }
        @Override
        public void set(ImgMoveView object, PointF value) {
            object.setPosition(value);
        }
    };
}

調用:

    /*ObjectAnimator pathMoveAnim=ObjectAnimator.ofFloat(imgMove, "translationX", "translationY", path);*/
    //ObjectAnimator pathMoveAnim=ObjectAnimator.ofFloat(imgMove, View.TRANSLATION_X, View.TRANSLATION_Y, path);
    ObjectAnimator pathMoveAnim=ObjectAnimator.ofObject(imgMove, ImgMoveView.POSITION,
            null, path);

image

參考:自定義控件三部曲之動畫篇(七)——ObjectAnimator基本使用自定義控件三部曲之動畫篇(八)——PropertyValuesHolder與Keyframe

3、監聽器
屬性動畫提供了AnimatorListener(監聽Start、End、Cancel、Repeat事件)、AnimatorUpdateListener (onAnimationUpdate())兩個監聽器用于動畫在播放過程中的重要動畫事件

    animatorSet.addListener(new AnimatorListenerAdapter() {
        @Override
        public void onAnimationEnd(Animator animation) {
        }
    });

4、AnimatorSet
如果一個動畫用到了一個對象的多個屬性,可以使用AnimatorSet;
可以通過playTogether()、playSequentially()、play().with()、after()、before()來控制執行的順序

  • AnimatorSet
  • playTogether:同時開始執行
  • playSequentially:依次按先后順序執行,前一個動畫執行完,再執行后一個動畫
  • AnimatorSet.Builder
  • play():調用AnimatorSet中的play方法,獲取AnimatorSet.Builder對象,
  • with():和前面動畫同時開始執行
  • after(A):A先執行,然后執行前面的動畫
  • before(A):前面的動畫先執行,A后執行

代碼如下:

AnimatorSet animatorSet=new AnimatorSet();
animatorSet.setDuration(1000);
Path translationPath=new Path();
translationPath.moveTo(0,0);
translationPath.lineTo(200, 300);
Path scalePath=new Path();
scalePath.moveTo(1,1);
scalePath.lineTo(0.5f,0.5f);
scalePath.lineTo(1f,1f);
ObjectAnimator translationAnimator=ObjectAnimator.ofFloat(v,"translationX","translationY",translationPath);
ObjectAnimator rotationAnimator=ObjectAnimator.ofFloat(v,"rotation",0,360);
ObjectAnimator scaleAnimator=ObjectAnimator.ofFloat(v,"scaleX","scaleY",scalePath);
//animatorSet.playTogether(translationAnimator,rotationAnimator);//同時
//animatorSet.playSequentially(translationAnimator,rotationAnimator);//相繼的
animatorSet.play(rotationAnimator).after(translationAnimator).before(scaleAnimator);
animatorSet.start();

---

AnimatorSet animatorSet=new AnimatorSet();
animatorSet.setDuration(1000);
ObjectAnimator translationAnimator=ObjectAnimator.ofFloat(v,"translationY",0,-400).setDuration(400);
ObjectAnimator translationAnimator2=ObjectAnimator.ofFloat(v,"translationY",-400,0).setDuration(400);
ObjectAnimator scaleAnimator=ObjectAnimator.ofFloat(v,"scaleX",1,0.1f,1).setDuration(200);
animatorSet.playSequentially(translationAnimator,scaleAnimator,translationAnimator2);//相繼的
animatorSet.start();

image
image

或者使用XML定義

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:ordering="sequentially">

    <objectAnimator android:duration="400"
        android:propertyName="translationY"
        android:valueFrom="0dp"
        android:valueTo="-200dp"
        android:valueType="floatType"/>

    <objectAnimator android:duration="100"
        android:propertyName="scaleX"
        android:valueFrom="1.0"
        android:valueTo="0.1"
        android:valueType="floatType"/>

        ...

</set>

當然,對于同一個對象的多個屬性,同時作用動畫效果,也可以使用PropertyValuesHolder,代碼如下:


    PropertyValuesHolder translationHolder=PropertyValuesHolder.ofFloat("translationY",-200);
    PropertyValuesHolder rotationHolder=PropertyValuesHolder.ofFloat("rotation",360);
    ObjectAnimator objectAnimator=ObjectAnimator.ofPropertyValuesHolder(v, translationHolder, rotationHolder);
    objectAnimator.setDuration(1000).start();
    objectAnimator.setInterpolator(new BounceInterpolator());

但是,它不能像AnimatorSet 一樣,控制動畫執行的順序

image

5、插值器
插值器定義了動畫變化過程中的變化規則,需要實現Interpolator接口

publicinterfaceTimeInterpolator{
floatgetInterpolation(floatinput);
}

其中

  • input參數:只與時間有關,取值范圍是0到1,表示當前動畫的進度,取0時表示動畫剛開始,取1時表示動畫結束
  • 返回值:動畫的當前 數值進度。取值可以超過1也可以小于0,超過1表示已經超過目標值,小于0表示小于開始位置。但這個數值是百分比的,不是具體數值,在監聽器返回之前,還需要Evaluator進行轉換

Android系統本身內置了一些通用的Interpolator(插值器),如下:
@android:anim/accelerate_interpolator: 越來越快
@android:anim/decelerate_interpolator:越來越慢
@android:anim/accelerate_decelerate_interpolator:先快后慢
@android:anim/anticipate_interpolator: 先后退一小步然后向前加速
@android:anim/overshoot_interpolator:快速到達終點超出一小步然后回到終點
@android:anim/anticipate_overshoot_interpolator:到達終點超出一小步然后回到終點
@android:anim/bounce_interpolator:到達終點產生彈球效果,彈幾下回到終點
@android:anim/linear_interpolator:均勻速度。

image
<!--@anim/cycle_2  -->
<!--定義cycleInterpolator  -->
<?xml version="1.0" encoding="utf-8"?>
<cycleInterpolator xmlns:android="http://schemas.android.com/apk/res/android"
    android:cycles="2"/>

<!--@anim/shake_error  -->
<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"
    android:fromXDelta="0%"
    android:toXDelta="2%"
    android:duration="300"
    android:interpolator="@anim/cycle_2"/>

//加載動畫
Login.startAnimation(AnimationUtils.loadAnimation(LoginActivity.this,R.anim.shake_error));

image

參考:
android動畫 Interpolator自定義控件三部曲之動畫篇——插值器

6、Evaluator
根據插值器返回的數字進度轉成具體數值;在AnimatorUpdateListener的animation.getAnimatedValue()函數拿到的就是Evaluator中返回的數值;系統本身內置了一些Evalutor,如IntEvaluator、FloatEvaluator、ArgbEvalutor

自定義Evaluator,需要實現TypeEvaluator<T>,要注意動畫數值類型

publicinterfaceTypeEvaluator<T>{
T  evaluate(floatfraction,T startValue,T endValue)
}

  • fraction:加速器中的返回值,表示當前動畫的數值進度
  • startValue和endValue分別對應ofInt(int start,int end)中的start和end的數值;

參考:自定義控件三部曲之動畫篇——EvaluatorAndroid 動畫學習(二)之Property Animation初步介紹

7、Keyframe關鍵幀
KeyframeSet是關鍵幀的集合,Keyframe是一個動畫保存time/value(時間與值)對

image

ofInt就是記錄了target,propName,values(是將我們傳入的int型values,輾轉轉化成了PropertyValuesHolder),以及一個mValueMap,這個map的key是propName,value是PropertyValuesHolder,在PropertyValuesHolder內部又存儲了proprName, valueType , keyframeSet等等

自定義控件三部曲之動畫篇(八)——PropertyValuesHolder與KeyframeAndroid 屬性動畫 源碼解析 深入了解其內部實現Android Property Animation(屬性動畫)原理分析

8、View的animate()方法

可以使用View的animate()方法直接驅動屬性動畫

    view.animate().y(100).scaleX(0.1f).setDuration(1000).withStartAction(new Runnable() {
        @Override
        public void run() {
        }
    }).withEndAction(new Runnable() {
        @Override
        public void run() {
        }
    }).start();

文末

歡迎關注我的簡書,分享Android干貨,交流Android技術。
對文章有何見解,或者有何技術問題,都可以在評論區一起留言討論,我會虔誠為你解答。
最后,如果你想知道更多Android的知識或需要其他資料我這里均免費分享,只需你多多支持我即可哦!

——可以直接點這里可以看到全部資料內容免費打包領取。

圖.jpeg

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容