自定義 View 實(shí)現(xiàn)數(shù)據(jù)加載動(dòng)效

前言:你的問(wèn)題在于讀書(shū)不多而想得太多 。 -------楊絳

沒(méi)想到2019年的第一篇文章是在情人節(jié)這天更新了,回顧2018年,覺(jué)得自己花在健身房的時(shí)間太多了,反而在專(zhuān)業(yè)上面沒(méi)有那么用心,2019年還是保持初心,一步一個(gè)腳印按時(shí)更新專(zhuān)業(yè)方面的技能點(diǎn),做到勞逸結(jié)合,厚積薄發(fā),與你們共勉。

屬性動(dòng)畫(huà)的知識(shí)大家可以看看郭霖的三篇屬性動(dòng)畫(huà)理論知識(shí),已經(jīng)屬于非常全面的了。所以屬性動(dòng)畫(huà)這塊打算舉一些例子,并結(jié)合設(shè)計(jì)模式分析一下屬性動(dòng)畫(huà)的源碼。本文實(shí)現(xiàn)一個(gè)數(shù)據(jù)加載動(dòng)效,先看 gif 實(shí)現(xiàn)效果圖:


image

那么就一點(diǎn)點(diǎn)帶領(lǐng)大家實(shí)現(xiàn)這個(gè)效果吧。


一、實(shí)現(xiàn)“紅、黃、藍(lán)”三個(gè)圖形的切換效果

1、實(shí)現(xiàn)基本自定義View

由于很簡(jiǎn)單,就直接把代碼貼在下面了:
首先自定義 View 代碼:

public class ShapeView extends View {

    private static String TAG = ShapeView.class.getSimpleName();

    public enum ShapeType{
        Circular,//圓形
        Square,//正方形
        Triangle//三角形
    }
    //默認(rèn)圖形
    private ShapeType mCurrentShape = Circular;

    private Paint mPaint;
    private Path mPath;

    public ShapeView(Context context) {
        this(context,null);
    }

    public ShapeView(Context context, AttributeSet attrs) {
        this(context, attrs,0);
    }

    public ShapeView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int width = MeasureSpec.getSize(widthMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);

        //設(shè)置控件的大小就為手動(dòng)設(shè)置的大小
        setMeasuredDimension(Math.min(width,height),Math.min(width,height));
    }

    /**
     * 根據(jù)當(dāng)前枚舉類(lèi)型繪制對(duì)應(yīng)圖形
     * @param canvas
     */
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        switch (mCurrentShape) {
            case Circular:
                    //繪制圓形
                int center = getWidth() / 2;
                mPaint.setColor(Color.RED);
                canvas.drawCircle(center,center,center,mPaint);
                break;
            case Square:
                    //繪制正方形
                mPaint.setColor(Color.BLUE);
                canvas.drawRect(0, 0, getWidth(), getWidth(), mPaint);//直接構(gòu)造
                break;
            case Triangle:
                    //繪制三角形
                mPaint.setColor(Color.YELLOW);
                if (mPath == null) {
                    // 畫(huà)路徑
                    mPath = new Path();
                    mPath.moveTo(getWidth() / 2, 0);
                    mPath.lineTo(0, (float) ((getWidth()/2)*Math.sqrt(3)));
                    mPath.lineTo(getWidth(), (float) ((getWidth()/2)*Math.sqrt(3)));
                    // path.lineTo(getWidth()/2,0);
                    mPath.close();// 把路徑閉合
                }
                canvas.drawPath(mPath, mPaint);
                break;
            }
    }

    /**
     * 改變形狀
     */
    public void changeShape() {
        switch (mCurrentShape) {
            case Circular:
                mCurrentShape = Square;
                break;
            case Square:
                mCurrentShape = Triangle;
                break;
            case Triangle:
                mCurrentShape = Circular;
                break;
        }
        invalidate();
    }

}

上面是自定義 ShapeView,動(dòng)畫(huà)里面的圖片是繪制上去的。代碼很簡(jiǎn)單,首先,測(cè)量出控件的大小,這里僅僅支持布局寫(xiě)死的大小,并且設(shè)置為正方形大小。然后是 onDraw() 方法,在這里使用枚舉定義了三種狀態(tài)。分別是:圓形、矩形、方形狀態(tài)。且在對(duì)應(yīng)狀態(tài)繪制對(duì)應(yīng)的圖形就好了。我們看到有一個(gè)改變行狀的方法:

    /**
     * 改變形狀
     */
    public void changeShape() {
        switch (mCurrentShape) {
            case Circular:
                mCurrentShape = Square;
                break;
            case Square:
                mCurrentShape = Triangle;
                break;
            case Triangle:
                mCurrentShape = Circular;
                break;
        }
        invalidate();
    }

這個(gè)方法中沒(méi)有在 View 內(nèi)部調(diào)用,是一個(gè)公共的方法給外面調(diào)用的。然后判定當(dāng)前狀態(tài),而且修改為別的狀態(tài),比如:當(dāng)前圓形,下一個(gè)就是矩形;當(dāng)前矩形,下一個(gè)就是三角......最后調(diào)用重繪,系統(tǒng)就會(huì)去調(diào)用 onDraw 方法再走其中的邏輯。這樣就實(shí)現(xiàn)了圖形的改變。
可能一個(gè)地方稍微有一點(diǎn)點(diǎn)“卡殼”的地方就是繪制三角形,我們單獨(dú)拿出來(lái)分析一下:

    //繪制三角形
 mPaint.setColor(Color.YELLOW);
 if (mPath == null) {
     // 畫(huà)路徑
     mPath = new Path();
     mPath.moveTo(getWidth() / 2, 0);
     mPath.lineTo(0, (float) ((getWidth()/2)*Math.sqrt(3)));
     mPath.lineTo(getWidth(), (float) ((getWidth()/2)*Math.sqrt(3)));
     // path.lineTo(getWidth()/2,0);
     mPath.close();// 把路徑閉合
 }
 canvas.drawPath(mPath, mPaint);

使用 path 來(lái)進(jìn)行化畫(huà)路線操作,講一個(gè):

mPath.lineTo(0, (float) ((getWidth()/2)*Math.sqrt(3)));

x表示相對(duì)坐標(biāo)為0,y=((getWidth()/2)*Math.sqrt(3))
看圖解:


image

這里是需要畫(huà)一個(gè)正三角形,因此 x 邊和 y 邊夾角是60°。利用正比關(guān)系,容易得到計(jì)算 y 的公式。

2、測(cè)試View改變效果:

(為了測(cè)試效果,以下代碼不求規(guī)范)
在 xml 中:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.itydl.property.MainActivity">

    <Button
        android:onClick="changeShape"
        android:text="測(cè)試"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

    <com.itydl.property.view.ShapeView
        android:id="@+id/shapeView"
        android:layout_centerInParent="true"
        android:layout_height="45dp"
        android:layout_width="45dp">
    </com.itydl.property.view.ShapeView>

</RelativeLayout>

然后在 Activity 中使用:

public class MainActivity extends AppCompatActivity {

    private ShapeView mShapeView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mShapeView = (ShapeView) findViewById(R.id.shapeView);
    }

    public void changeShape(View view){
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true){
                    SystemClock.sleep(1000);
                    runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            mShapeView.changeShape();
                        }
                    });
                }
            }
        }).start();
    }
}

這里重要的是按鈕點(diǎn)擊事件,讓其不斷循環(huán),每隔1s就調(diào)用一次上述 View 的 changeShape 方法(還是注意,這里只是測(cè)試功能)。運(yùn)行效果:


image

上面動(dòng)畫(huà)有點(diǎn)掉幀,實(shí)際運(yùn)行起來(lái)效果不是這樣的。


二、動(dòng)畫(huà)的實(shí)現(xiàn)

2.1、先實(shí)現(xiàn)下落和回彈效果

代碼如下:

public class LoadingView extends LinearLayout {

    private final int mTranslationDis;
    private View mShadowView;//陰影
    private ShapeView mShapeView;//圖形View
    // 動(dòng)畫(huà)執(zhí)行的時(shí)間
    private final long ANIMATOR_DURATION = 500;

    public LoadingView(Context context) {
        this(context,null);
    }

    public LoadingView(Context context, AttributeSet attrs) {
        this(context, attrs,0);
    }

    public LoadingView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mTranslationDis = dip2px(80);
        initLayout();
    }

    /**
     * 初始化組合控件布局
     */
    private void initLayout() {
        // 第三個(gè)參數(shù)為this,表示布局解析完畢直接添加到LoadingView中(它是一個(gè)擴(kuò)展的LinearLayout)
        inflate(getContext(), R.layout.layout_loading_view, this);
        mShadowView = findViewById(R.id.shadowView);
        mShapeView = (ShapeView) findViewById(R.id.shapeView);

        /**---------  直接開(kāi)啟動(dòng)畫(huà)  ---------**/
        post(new Runnable() {
            @Override
            public void run() {
                //讓開(kāi)啟動(dòng)畫(huà)邏輯在onResume()之后
                startPullDownAnimation();
            }
        });

    }

    /**
     * 開(kāi)啟下落動(dòng)畫(huà)
     */
    private void startPullDownAnimation() {
        ObjectAnimator shapeViewDownAnimator = ObjectAnimator.ofFloat(mShapeView,"TranslationY",0,mTranslationDis);
        ObjectAnimator shadowViewDownAnimator = ObjectAnimator.ofFloat(mShadowView,"ScaleX",1.0f,0.3f);

        AnimatorSet animatorSet = new AnimatorSet();
        animatorSet.setDuration(ANIMATOR_DURATION);
        // 下落的速度應(yīng)該是越來(lái)越快,使用加速度插值器
        animatorSet.setInterpolator(new AccelerateInterpolator());
        animatorSet.playTogether(shapeViewDownAnimator,shadowViewDownAnimator);
        animatorSet.start();

        animatorSet.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                mShapeView.changeShape();
                //開(kāi)啟回彈動(dòng)畫(huà)
                startSpringBackAnimation();
            }
        });
    }

    /**
     * 開(kāi)啟彈起動(dòng)畫(huà)
     */
    private void startSpringBackAnimation() {
        ObjectAnimator shapeViewDownAnimator = ObjectAnimator.ofFloat(mShapeView,"TranslationY",mTranslationDis,0);
        ObjectAnimator shadowViewDownAnimator = ObjectAnimator.ofFloat(mShadowView,"ScaleX",0.3f,1.0f);

        AnimatorSet animatorSet = new AnimatorSet();
        animatorSet.setDuration(ANIMATOR_DURATION);
        // 下落的速度應(yīng)該是越來(lái)越快,使用加速度插值器
        animatorSet.setInterpolator(new DecelerateInterpolator());
        animatorSet.playTogether(shapeViewDownAnimator,shadowViewDownAnimator);

        animatorSet.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                //開(kāi)啟回彈動(dòng)畫(huà)
                startPullDownAnimation();
            }

            @Override
            public void onAnimationStart(Animator animation) {
                //動(dòng)畫(huà)開(kāi)始,開(kāi)啟旋轉(zhuǎn)動(dòng)畫(huà)
                startRotateAnimation();
            }
        });

        //開(kāi)啟動(dòng)畫(huà)要放在后面,否則onAnimationStart監(jiān)聽(tīng)不到
        animatorSet.start();

    }

    /**
     * 旋轉(zhuǎn)動(dòng)畫(huà)。
     */
    private void startRotateAnimation() {
        switch (mShapeView.getCurrentShape()) {
            case Circular:
                break;
            case Square:
                break;
            case Triangle:
                break;
            }
    }

    private int dip2px(int dip) {
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,dip,getResources().getDisplayMetrics());
    }
}

在這里又重新做了一個(gè) View——LoadingView,這 View 是一個(gè)組合控件形式,即加載布局的方式然后把加載的布局放入這個(gè) LoadingView 控件里面。
要加載的布局如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="vertical"
              android:gravity="center"
              android:background="#ffffffff"
              android:layout_width="match_parent"
              android:layout_height="match_parent">

    <!--圖形變換View-->
    <com.itydl.property.view.ShapeView
        android:layout_marginBottom="85dp"
        android:id="@+id/shapeView"
        android:layout_centerInParent="true"
        android:layout_height="30dp"
        android:layout_width="30dp">
    </com.itydl.property.view.ShapeView>

    <!--陰影-->
    <View
        android:id="@+id/shadowView"
        android:background="@drawable/shadow_bg"
        android:layout_width="40dp"
        android:layout_height="3dp"/>

    <!--文本-->
    <TextView
        android:layout_marginTop="10dp"
        android:text="正在加載中..."
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

</LinearLayout>

布局還是很簡(jiǎn)單的,就不細(xì)說(shuō)了。咱們看看自定義 LoadingView 的邏輯:
1、初始化布局和孩子控件
2、同時(shí)直接開(kāi)啟下落動(dòng)畫(huà):

 private void initLayout() {
        // 第三個(gè)參數(shù)為this,表示布局解析完畢直接添加到LoadingView中(它是一個(gè)擴(kuò)展的LinearLayout)
        inflate(getContext(), R.layout.layout_loading_view, this);
        mShadowView = findViewById(R.id.shadowView);
        mShapeView = (ShapeView) findViewById(R.id.shapeView);

        /**---------  直接開(kāi)啟動(dòng)畫(huà)  ---------**/
        post(new Runnable() {
            @Override
            public void run() {
                //讓開(kāi)啟動(dòng)畫(huà)邏輯在onResume()之后
                startPullDownAnimation();
            }
        });

    }

注意的是,使用 post 把動(dòng)畫(huà)開(kāi)啟在 Activity 的 onResume 之后執(zhí)行。
3、具體下落動(dòng)畫(huà):

/**
     * 開(kāi)啟下落動(dòng)畫(huà)
     */
    private void startPullDownAnimation() {
        ObjectAnimator shapeViewDownAnimator = ObjectAnimator.ofFloat(mShapeView,"TranslationY",0,mTranslationDis);
        ObjectAnimator shadowViewDownAnimator = ObjectAnimator.ofFloat(mShadowView,"ScaleX",1.0f,0.3f);

        AnimatorSet animatorSet = new AnimatorSet();
        animatorSet.setDuration(ANIMATOR_DURATION);
        // 下落的速度應(yīng)該是越來(lái)越快,使用加速度插值器
        animatorSet.setInterpolator(new AccelerateInterpolator());
        animatorSet.playTogether(shapeViewDownAnimator,shadowViewDownAnimator);
        animatorSet.start();

        animatorSet.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                mShapeView.changeShape();
                //開(kāi)啟回彈動(dòng)畫(huà)
                startSpringBackAnimation();
            }
        });
    }

下落動(dòng)畫(huà)使用到了屬性動(dòng)畫(huà),這里都是最最進(jìn)本的使用方式。看到是分別對(duì) mShapeView 做Y軸的平移動(dòng)畫(huà),對(duì) mShadowView 做縮放動(dòng)畫(huà)。下落的時(shí)候,讓 mShadowView 縮小。使用了 animatorSet 讓動(dòng)畫(huà)同時(shí)播放。
需要監(jiān)聽(tīng)動(dòng)畫(huà)狀態(tài),當(dāng)下落動(dòng)畫(huà)結(jié)束,立即改變當(dāng)前 ShapeView 的圖形效果,然后開(kāi)啟回彈效果:

 /**
     * 開(kāi)啟彈起動(dòng)畫(huà)
     */
    private void startSpringBackAnimation() {
        ObjectAnimator shapeViewDownAnimator = ObjectAnimator.ofFloat(mShapeView,"TranslationY",mTranslationDis,0);
        ObjectAnimator shadowViewDownAnimator = ObjectAnimator.ofFloat(mShadowView,"ScaleX",0.3f,1.0f);

        AnimatorSet animatorSet = new AnimatorSet();
        animatorSet.setDuration(ANIMATOR_DURATION);
        // 下落的速度應(yīng)該是越來(lái)越快,使用加速度插值器
        animatorSet.setInterpolator(new DecelerateInterpolator());
        animatorSet.playTogether(shapeViewDownAnimator,shadowViewDownAnimator);

        animatorSet.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                //開(kāi)啟回彈動(dòng)畫(huà)
                startPullDownAnimation();
            }

            @Override
            public void onAnimationStart(Animator animation) {
                //動(dòng)畫(huà)開(kāi)始,開(kāi)啟旋轉(zhuǎn)動(dòng)畫(huà)
                startRotateAnimation();
            }
        });

        //開(kāi)啟動(dòng)畫(huà)要放在后面,否則onAnimationStart監(jiān)聽(tīng)不到
        animatorSet.start();

    }

這塊代碼跟下落基本很相似,注意點(diǎn)仍然是動(dòng)畫(huà)監(jiān)聽(tīng)。當(dāng)動(dòng)畫(huà)剛開(kāi)啟的時(shí)候開(kāi)啟旋轉(zhuǎn)動(dòng)畫(huà),看到旋轉(zhuǎn)動(dòng)畫(huà)沒(méi)有任何邏輯,我們會(huì)在下一節(jié)單獨(dú)講。然后動(dòng)畫(huà)結(jié)束的時(shí)候,在此開(kāi)啟下落動(dòng)畫(huà)。這里需要把 animatorSet.start(); 放在監(jiān)聽(tīng)器的后面,否則動(dòng)畫(huà)開(kāi)啟監(jiān)聽(tīng)拿不到。
此時(shí)運(yùn)行程序看看效果吧:


image

看到基本效果都快實(shí)現(xiàn)了,最后就是完成旋轉(zhuǎn)動(dòng)畫(huà)了。

2.2旋轉(zhuǎn)動(dòng)畫(huà)

/**
 * 旋轉(zhuǎn)動(dòng)畫(huà)。
 */
private void startRotateAnimation() {
    ObjectAnimator rotationAnimator = null;
    switch (mShapeView.getCurrentShape()) {
        case Circular:
        case Square:
            //圓形和方形旋轉(zhuǎn)-180度
            rotationAnimator = ofFloat(mShapeView,"rotation",0,180);
            break;
        case Triangle:
            //三角形旋轉(zhuǎn)-120°
            rotationAnimator = ObjectAnimator.ofFloat(mShapeView,"rotation",0,-120);
            break;
        }
    rotationAnimator.setDuration(ANIMATOR_DURATION);
    rotationAnimator.setInterpolator(new DecelerateInterpolator());
    rotationAnimator.start();
}

當(dāng)處于圓形和方型的時(shí)候讓 ShapeView 旋轉(zhuǎn)180°,當(dāng)為三角形的時(shí)候旋轉(zhuǎn)-120°。

2.3添加讓動(dòng)畫(huà)消失的功能

為了模擬更真實(shí)的開(kāi)發(fā)環(huán)境,在加載網(wǎng)絡(luò)結(jié)束或者失敗都要讓正在加載的 View 消失,這里同樣提供一個(gè)消失的方法:

/**
 * 清空動(dòng)畫(huà),清空View
 * @param visibility
 */
@Override
public void setVisibility(int visibility) {
    super.setVisibility(View.INVISIBLE);
    mShapeView.clearAnimation();
    mShadowView.clearAnimation();

    ViewGroup parent = (ViewGroup) getParent();
    if(parent != null){
        //因?yàn)樽约貉b到了父View中了
        parent.removeView(this);
        //移除自己的Views
        removeAllViews();
    }
}

發(fā)現(xiàn)主要是清空動(dòng)畫(huà)和 View 視圖。1、清空自己在父 View(也就是 LinearLayout )中;2、清空自己的孩子控件。
然后這個(gè)控件如果在 Activity 中使用的話:

    mLoadingView = (LoadingView) findViewById(R.id.loadingView);
    new Thread(new Runnable() {
        @Override
        public void run() {
            SystemClock.sleep(5000);
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    mLoadingView.setVisibility(View.GONE);
                }
            });
        }
    }).start();

模擬數(shù)據(jù)加載5S鐘后 gone 掉加載動(dòng)畫(huà)。
此時(shí)運(yùn)行程序:


image

三、一點(diǎn)點(diǎn)優(yōu)化

可以看到上面已經(jīng)完成了開(kāi)始的功能,但是呢。即使是移除了動(dòng)畫(huà),此時(shí)的監(jiān)聽(tīng)仍然在跑,不信你可以在啟動(dòng)動(dòng)畫(huà)里面加一行 log,發(fā)現(xiàn)即使 Activity 退出了,仍然在打印 log。那么就會(huì)導(dǎo)致 Activity 的實(shí)例無(wú)法被回收從而導(dǎo)致內(nèi)存泄漏。只需要加一行代碼即可:
加一個(gè)標(biāo)志位:


image

然后在啟動(dòng)動(dòng)畫(huà)開(kāi)始加上一個(gè)判斷:


image

再運(yùn)行程序,就不會(huì)隨便打印 log 了。
到此為止,這個(gè)動(dòng)效也就實(shí)現(xiàn)完畢了。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • ¥開(kāi)啟¥ 【iAPP實(shí)現(xiàn)進(jìn)入界面執(zhí)行逐一顯】 〖2017-08-25 15:22:14〗 《//首先開(kāi)一個(gè)線程,因...
    小菜c閱讀 7,328評(píng)論 0 17
  • 【Android 動(dòng)畫(huà)】 動(dòng)畫(huà)分類(lèi)補(bǔ)間動(dòng)畫(huà)(Tween動(dòng)畫(huà))幀動(dòng)畫(huà)(Frame 動(dòng)畫(huà))屬性動(dòng)畫(huà)(Property ...
    Rtia閱讀 6,383評(píng)論 1 38
  • Android UI相關(guān)開(kāi)源項(xiàng)目庫(kù)匯總OpenDigg 抽屜菜單MaterialDrawer ★7337 - 安卓...
    黃海佳閱讀 8,825評(píng)論 3 77
  • 開(kāi)始行動(dòng)! 今天是2016年的最后一個(gè)工作日,感慨萬(wàn)千。回顧2016年,對(duì)個(gè)人而言這是思維轉(zhuǎn)變里程杯式的一年,20...
    然媽Miya閱讀 633評(píng)論 0 4
  • 秋天,很多時(shí)候都是我們抒發(fā)情感的一個(gè)季節(jié),而熱愛(ài)攝影的我,喜歡用相機(jī)展示美麗的秋天 一車(chē)一人,,一半綠一半黃的樹(shù),...
    羊小翼閱讀 161評(píng)論 0 3

友情鏈接更多精彩內(nèi)容