Android自定义view之属性动画熟悉

序言:最近项目中有一个这样的需求,当用户填写完数据之后,传后台去计算,然后需要跑一个这样的动画,由于这个动画效果同事用的是后台切的图组成的帧动画,然后一向强迫症如我就非常不喜欢那么多图片就是为了成就这么一个动画,于是乎我决定自己用最近学习的属性动画来写一个,如有错误或者更好的解决办法,请及时指正,话不多说,先看图:

录制图片的时候可能会有点卡顿

我们可以来分析一下,首先呢我们可以把这个图分成四部分,三串数字一个圆,每一串数字一个接一个的从圆的上侧滚动到下侧,但是注意并不是同时的,我们可以用三个动画为三串数字来实现这个效果(这是我的想法),先从中间开始,因为中间部分坐标什么的都比较简单:

在这里把自定义view的几个步骤讲的稍微详细一点,为了自己能得到复习的同时也为了能帮助需要了解自定义view的同学,顺便说一下,如果你还没有看我的另外一篇关于自定义view之属性动画的,请移步:Android自定义view之属性动画初见

1、先自定义属性,以便以后可以自己定制,圆的颜色,半径,字体颜色及大小,我我们暂时就需要这么几个属性:

<declare-styleable name="CustomNumAnimView">
    <attr name="round_radius" format="dimension" />
    <attr name="round_color" format="color" />
    <attr name="text_color" format="color" />
    <attr name="text_size" format="dimension" />
</declare-styleable>

2、好了,然后我们得在构造方法中获得属性所对应的值:

private int roundColor;    //圆的颜色
private int textColor;    //数字的颜色
private float textSize;    //数字字体大小
private float roundRadius;    //圆的半径

private Paint mPaint;     //画笔
private Rect textRect;    //包裹数字的矩形

public CustomNumAnimView(Context context, AttributeSet attrs) {
    super(context, attrs);
    //获取自定义属性
    TypedArray array = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CustomNumAnimView, defStyleAttr, 0);
    roundColor = array.getColor(R.styleable.CustomNumAnimView_round_color, ContextCompat.getColor(context, R.color.colorPrimary));
    roundRadius = array.getDimension(R.styleable.CustomNumAnimView_round_radius, 50);
    textColor = array.getColor(R.styleable.CustomNumAnimView_text_color, Color.WHITE);
    textSize = array.getDimension(R.styleable.CustomNumAnimView_text_size, 30);
    array.recycle();
    mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    mPaint.setTextSize(textSize);
    textRect = new Rect();
    //得到数字矩形的宽高,以用来画数字的时候纠正数字的位置
    mPaint.getTextBounds(middleNum, 0, middleNum.length(), textRect);
}

3、获取完属性之后,我们得要画一个圆,画在哪里呢?当然是屏幕的中心了,在onDraw方法中做如下操作:

 @Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    mPaint.setAntiAlias(true);    //设置抗锯齿
    mPaint.setStyle(Paint.Style.FILL_AND_STROKE);    //设置画笔填充,画实心圆
    mPaint.setColor(roundColor);     //设置圆的颜色
    canvas.drawCircle(getMeasuredWidth() / 2, getMeasuredHeight() / 2, roundRadius, mPaint);    //画圆
}

第一步:画圆

4、我们可以画数字了,在画数字之前我想让大家知道Android中手机屏幕的坐标系的结构,如下图所示:
Android手机中的坐标系

好了,对坐标系有一定了解之后,我们开始画数字,先把中间数字的效果做出来,我记得我在上一篇关于动画的博客中有讲到TypeEvaluator,这真是个好东西,有不清楚的同学请移步我之前的博客,在这里也给大家推荐一个学习属性动画的博客:Android自定义控件三部曲文章索引

public class CustomPointEvaluator implements TypeEvaluator {

  /**
   *
   * @param fraction 系数
   * @param startValue 起始值
   * @param endValue 终点值
   * @return
   */
  @Override
  public Object evaluate(float fraction, Object startValue, Object endValue) {
      CustomPoint startPoint = (CustomPoint) startValue;
      CustomPoint endPoint = (CustomPoint) endValue;
      float x = startPoint.getX() + fraction * (endPoint.getX() - startPoint.getX());
      float y = startPoint.getY() + fraction * (endPoint.getY() - startPoint.getY());
      CustomPoint point = new CustomPoint(x, y);
      return point;
  }
}

这个类帮助我们告诉系统如何在设置的时间内从初始值过渡到结束值,并获取中间的状态

5、上面的类中还有一个东西CustomPoint,这个表示每一个数字行进过程中的坐标,我们把它当作一个点来处理,这样更加方便:

public class CustomPoint {
    private float x;   //点的x坐标
    private float y;  //点的y坐标

    public CustomPoint(float x, float y) {
        this.x = x;
        this.y = y;
    }

    public float getX() {
        return x;
    }

    public void setX(float x) {
        this.x = x;
    }

    public float getY() {
        return y;
    }

    public void setY(float y) {
        this.y = y;
    }
}

6、然后我们就开始动画的过程:

private boolean isFirstInit = false;   //是否是第一次初始化
private CustomPoint middlePoint;   //中间的数字的实时点
private ValueAnimator middleAnim;   //中间数字动画
private String middleNum = "9";
private boolean isMiddleNumInvalidate = false;    //中间数字是否重绘界面

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    if (!isFirstInit) {
        middlePoint = new CustomPoint(getMeasuredWidth() / 2 - textRect.width() / 2, getMeasuredHeight() / 2 - roundRadius - textRect.height() / 2);
        drawText(canvas);
        startAnimation();   //开始动画
        isFirstInit = true;
    } else {
        drawText(canvas);
    }
}

/**
 * 画数字
 * @param canvas
 */
private void drawText(Canvas canvas) {
    mPaint.setAntiAlias(true);
    mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
    mPaint.setColor(roundColor);
    canvas.drawCircle(getMeasuredWidth() / 2, getMeasuredHeight() / 2, roundRadius, mPaint);
    mPaint.setColor(textColor);
    mPaint.setTextSize(textSize);
    if (isMiddleNumInvalidate) {
        canvas.drawText(middleNum, middlePoint.getX(), middlePoint.getY(), mPaint);
        isMiddleNumInvalidate = false;
    }
}

7、这个过程还是比较好理解的,首先所有的点我们只初始化一遍,然后开始动画也只在第一次初始化中执行,因为我们设置的动画是无限循环的,然后就是我们的startAnimation()方法了:

private void startAnimation() {
    //初始化中间数字的开始点的位置
    final CustomPoint startPoint = new CustomPoint(getMeasuredWidth() / 2 - textRect.width() / 2, getMeasuredHeight() / 2 - roundRadius - textRect.height() / 2);
    //初始化中间数字的结束点的位置
    final CustomPoint endPoint = new CustomPoint(getMeasuredWidth() / 2 - textRect.width() / 2, getMeasuredHeight() / 2 + roundRadius + textRect.height() / 2);
    middleAnim = ValueAnimator.ofObject(new CustomPointEvaluator(), startPoint, endPoint);
    //监听从起始点到终点过程中点的变化,并获取点然后重新绘制界面
    middleAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            middlePoint = (CustomPoint) animation.getAnimatedValue();
            isMiddleNumInvalidate = true;
            invalidate();
        }
    });
    middleAnim.setDuration(300);
    middleAnim.setRepeatCount(ValueAnimator.INFINITE);
}

8、这个过程也还是比较好理解的,首先我们创建了两个点,也就是数字的初始位置以及结束位置,然后再利用ValueAnimatorofObject方法来对数字行进路线进行分析,然后每一次监听的时候都让界面进行重绘,这样就能感觉数字一直在移动,让我们来看一下效果:

单一数字

9、OMG,图片录制的不太友好,实际效果可不是这样子的,但是我们不难发现,数字好像没有变,只是单一的数字,下面我们要做的就是在一次动画结束之后,取随机数,怎么样才能知道动画一次运行完成了呢?万能的google肯定会有方法的,我们只需要再加一个监听就好了:

middleAnim.addListener(new CustomAnimListener() {
        @Override
        public void onAnimationRepeat(Animator animation) {
            middleNum = getRandom();
        }
    });

 /**
 * 获取0-9之间的随机数
 *
 * @return
 */
private String getRandom() {
    int random = (int) (Math.random() * 9);
    return String.valueOf(random);
}

10、这个监听就是当动画重复之后会执行这个方法,我们也可以认为每当动画执行完一遍之后都会执行这个方法。好了,让我们再来看看效果吧:(可能录屏软件都有点问题,看起来并不和谐>_<)

随即数字

11、完成一个之后,剩下的两个就简单了,我们只需要找到旁边两个点的坐标就行了,我是这么分析的,我们来看一张图:

三串数字的动画路线

首先呢,左右两边肯定是关于Y轴对称的,而且我的想法是,这三个数字所在的点将X轴平分成了四段(只包括整个圆),然后根据这个可以算出左边点的横纵坐标,纵左边是横坐标的根号三倍,算出坐标之后就好办了,依照中间点的动画原则,我们来看一下完整的代码:

public class CustomNumAnimView extends View {

    private int roundColor;    //圆的颜色
    private int textColor;    //数字的颜色
    private float textSize;    //数字字体大小
    private float roundRadius;    //圆的半径

    private Paint mPaint;     //画笔
    private Rect textRect;    //包裹数字的矩形
    private boolean isFirstInit = false;   //是否是第一次初始化

    private CustomPoint leftPoint;    //左边的数字的实时点
    private ValueAnimator leftAnim;   //左边数字动画
    private String leftNum = "9";
    private boolean isLeftNumInvalidate = false;  //左边数字是否重绘界面

    private CustomPoint middlePoint;   //中间的数字的实时点
    private ValueAnimator middleAnim;   //中间数字动画
    private String middleNum = "9";
    private boolean isMiddleNumInvalidate = false;    //中间数字是否重绘界面

    private CustomPoint rightPoint;    //右边的数字的实时点
    private ValueAnimator rightAnim;   //右边数字动画
    private String rightNum = "9";
    private boolean isRightNumInvalidate = false;    //右边数字是否重绘界面

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

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

    public CustomNumAnimView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        //获取自定义属性
        TypedArray array = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CustomNumAnimView, defStyleAttr, 0);
        roundColor = array.getColor(R.styleable.CustomNumAnimView_round_color, ContextCompat.getColor(context, R.color.colorPrimary));
        roundRadius = array.getDimension(R.styleable.CustomNumAnimView_round_radius, 50);
        textColor = array.getColor(R.styleable.CustomNumAnimView_text_color, Color.WHITE);
        textSize = array.getDimension(R.styleable.CustomNumAnimView_text_size, 30);
        array.recycle();
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setTextSize(textSize);
        textRect = new Rect();
        //得到数字矩形的宽高,以用来画数字的时候纠正数字的位置
        mPaint.getTextBounds(middleNum, 0, middleNum.length(), textRect);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (!isFirstInit) {
            //初始化三串数字
            leftPoint = new CustomPoint(getMeasuredWidth() / 2 - textRect.width() / 2 - roundRadius / 2, (float) (getMeasuredHeight() / 2 - roundRadius * (Math.sqrt(3) / 2) - textRect.height() / 2));
            middlePoint = new CustomPoint(getMeasuredWidth() / 2 - textRect.width() / 2, getMeasuredHeight() / 2 - roundRadius - textRect.height() / 2);
            rightPoint = new CustomPoint(getMeasuredWidth() / 2 - textRect.width() / 2 + roundRadius / 2, (float) (getMeasuredHeight() / 2 - roundRadius * (Math.sqrt(3) / 2) - textRect.height() / 2));
            drawText(canvas);
            startAnimation();   //开始动画
            isFirstInit = true;
        } else {
            drawText(canvas);
        }
    }

    /**
     * 画数字
     * @param canvas
     */
    private void drawText(Canvas canvas) {
        mPaint.setAntiAlias(true);
        mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
        mPaint.setColor(roundColor);
        canvas.drawCircle(getMeasuredWidth() / 2, getMeasuredHeight() / 2, roundRadius, mPaint);
        mPaint.setColor(textColor);
        mPaint.setTextSize(textSize);
        if (isLeftNumInvalidate) {
            canvas.drawText(leftNum, leftPoint.getX(), leftPoint.getY(), mPaint);
            isLeftNumInvalidate = false;
        }
        if (isMiddleNumInvalidate) {
            canvas.drawText(middleNum, middlePoint.getX(), middlePoint.getY(), mPaint);
            isMiddleNumInvalidate = false;
        }
        if (isRightNumInvalidate) {
            canvas.drawText(rightNum, rightPoint.getX(), rightPoint.getY(), mPaint);
            isRightNumInvalidate = false;
        }
    }

    public void startAnim() {
        if (isAnimStart(leftAnim)) {
            leftAnim.start();
        }
        if (isAnimStart(middleAnim)) {
            middleAnim.start();
        }
        if (isAnimStart(rightAnim)) {
            rightAnim.start();
        }
    }

    private boolean isAnimStart(ValueAnimator anim) {
        return !anim.isStarted() || anim.isPaused();
    }

    public void pauseAnim() {
        if (isAnimStop(leftAnim)) {
            leftAnim.pause();
        }
        if (isAnimStop(middleAnim)) {
            middleAnim.pause();
        }
        if (isAnimStop(rightAnim)) {
            rightAnim.pause();
        }
    }

    /**
     * 在onDestroy方法中调用
     */
    public void stopAnim() {
        leftAnim.end();
        middleAnim.end();
        rightAnim.end();
        leftAnim = null;
        middleAnim = null;
        rightAnim = null;
    }

    private boolean isAnimStop(ValueAnimator anim) {
        return null != anim && anim.isRunning();
    }

    //开始动画
    private void startAnimation() {
        startLeft();
        startMiddle();
        startRight();
    }

    private void startLeft() {
        final CustomPoint startPoint = new CustomPoint(getMeasuredWidth() / 2 - textRect.width() / 2 - roundRadius / 2, (float) (getMeasuredHeight() / 2 - roundRadius * (Math.sqrt(3) / 2) - textRect.height() / 2));
        final CustomPoint endPoint = new CustomPoint(getMeasuredWidth() / 2 - textRect.width() / 2 - roundRadius / 2, (float) (getMeasuredHeight() / 2 + roundRadius * (Math.sqrt(3) / 2) + textRect.height() / 2));
        leftAnim = ValueAnimator.ofObject(new CustomPointEvaluator(), startPoint, endPoint);
        leftAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                leftPoint = (CustomPoint) animation.getAnimatedValue();
                isLeftNumInvalidate = true;
                invalidate();
            }
        });
        leftAnim.addListener(new CustomAnimListener() {
            @Override
            public void onAnimationRepeat(Animator animation) {
                leftNum = getRandom();
            }
        });
        leftAnim.setStartDelay(100);
        leftAnim.setDuration(300);
        leftAnim.setRepeatCount(ValueAnimator.INFINITE);
    }

    private void startMiddle() {
        //初始化中间数字的开始点的位置
        final CustomPoint startPoint = new CustomPoint(getMeasuredWidth() / 2 - textRect.width() / 2, getMeasuredHeight() / 2 - roundRadius - textRect.height() / 2);
        //初始化中间数字的结束点的位置
        final CustomPoint endPoint = new CustomPoint(getMeasuredWidth() / 2 - textRect.width() / 2, getMeasuredHeight() / 2 + roundRadius + textRect.height() / 2);
        middleAnim = ValueAnimator.ofObject(new CustomPointEvaluator(), startPoint, endPoint);
        //监听从起始点到终点过程中点的变化,并获取点然后重新绘制界面
        middleAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                middlePoint = (CustomPoint) animation.getAnimatedValue();
                isMiddleNumInvalidate = true;
                invalidate();
            }
        });
        middleAnim.addListener(new CustomAnimListener() {
            @Override
            public void onAnimationRepeat(Animator animation) {
                middleNum = getRandom();
            }
        });
        middleAnim.setDuration(300);
        middleAnim.setRepeatCount(ValueAnimator.INFINITE);
    }

    private void startRight() {
        final CustomPoint startPoint = new CustomPoint(getMeasuredWidth() / 2 - textRect.width() / 2 + roundRadius / 2, (float) (getMeasuredHeight() / 2 - roundRadius * (Math.sqrt(3) / 2) - textRect.height() / 2));
        final CustomPoint endPoint = new CustomPoint(getMeasuredWidth() / 2 - textRect.width() / 2 + roundRadius / 2, (float) (getMeasuredHeight() / 2 + roundRadius * (Math.sqrt(3) / 2) + textRect.height() / 2));
        rightAnim = ValueAnimator.ofObject(new CustomPointEvaluator(), startPoint, endPoint);
        rightAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                rightPoint = (CustomPoint) animation.getAnimatedValue();
                isRightNumInvalidate = true;
                invalidate();
            }
        });
        rightAnim.addListener(new CustomAnimListener() {
            @Override
            public void onAnimationRepeat(Animator animation) {
                rightNum = getRandom();
            }
        });
        rightAnim.setStartDelay(150);
        rightAnim.setDuration(300);
        rightAnim.setRepeatCount(ValueAnimator.INFINITE);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int size;
        int mode;
        int width;
        int height;
        size = MeasureSpec.getSize(widthMeasureSpec);
        mode = MeasureSpec.getMode(widthMeasureSpec);
        if (mode == MeasureSpec.EXACTLY) {    //确定的值或者MATCH_PARENT
            width = size;
        } else {    //表示WARP_CONTENT
            width = (int) (2 * roundRadius);
        }

        mode = MeasureSpec.getMode(heightMeasureSpec);
        size = MeasureSpec.getSize(heightMeasureSpec);
        if (mode == MeasureSpec.EXACTLY) {    //确定的值或者MATCH_PARENT
            height = size;
        } else {    //表示WARP_CONTENT
            height = (int) (2 * roundRadius);
        }
        setMeasuredDimension(width, height);
    }

    /**
     * 获取0-9之间的随机数
     *
     * @return
     */
    private String getRandom() {
        int random = (int) (Math.random() * 9);
        return String.valueOf(random);
    }
}

12、好了,这就是完整的代码,该有的注释我都加上去了,有不懂得地方可以私信我,如果还有更好的解决方法也请私戳我,下面来看一下最后的效果:

完整的

好吧!看起来也不怎么和谐了,不过大家可以下载代码去跑一遍,真正运行起来的不是这个样子的,代码我已上传至GitHub,有需要的同学可以下载,star

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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,259评论 25 707
  • 1 背景 不能只分析源码呀,分析的同时也要整理归纳基础知识,刚好有人微博私信让全面说说Android的动画,所以今...
    未聞椛洺阅读 2,673评论 0 10
  • 看这阴沉沉的天,怕是要下雨了。 下雨前的房间,总是异常沉闷。我坐在书桌前,看着面前的六张数学卷,心情异常烦躁。我与...
    是温酒呐阅读 564评论 0 2
  • 新的一年来了,应该在开年之际谈谈爱情。我知道,在这个神圣、伟大的两个字面前,我依旧是个莽撞少年,但谈谈过往时光对这...
    流动盛宴爱码士阅读 603评论 2 1
  • 【鹧鸪天.闺怨】 作者:紫色 湖畔寻芳独步过 ,柳堤新影乱红多。不迷百媚留...
    紫翼惠瑄阅读 307评论 1 9