• 带你体验Android自定义圆形刻度罗盘 仪表盘 实现指针动态改变


    带你体验Android自定义圆形刻度罗盘 仪表盘 实现指针动态改变

    近期有一个自定义View的功能,类似于仪表盘的模型,可以将指针动态指定到某一个刻度上,话不多说,先上图

    先说下思路

    1.先获取自定义的一些属性,初始化一些资源

    2.在onMeasure中测量控件的具体大小

    3.然后就在onDraw中先绘制有渐变色的圆弧形色带

    4.再绘制几个大的刻度和刻度值

    5.再绘制两个大刻度之间的小刻度

    6.再绘制处于正中间的圆和三角形指针

    7.最后绘制实时值

    其实这也从侧面体现了一个自定义view的流程

    1.继承View,重写构造方法

    2.加载自定义属性和其它资源

    3.重写onMeasure方法去确定控件的大小

    4.重写onDraw方法去绘制

    5.如果有点击事件的话,还得重写onTouchEvent或者dispatchTouchEvent去处理点击事件

    来上代码吧,具体注释已经写的很详细了

    1.  
      public class NoiseboardView extends View {
    2.  
       
    3.  
      final String TAG = "NoiseboardView";
    4.  
       
    5.  
      private int mRadius; // 圆弧半径
    6.  
      private int mBigSliceCount; // 大份数
    7.  
      private int mScaleCountInOneBigScale; // 相邻两个大刻度之间的小刻度个数
    8.  
      private int mScaleColor; // 刻度颜色
    9.  
      private int mScaleTextSize; // 刻度字体大小
    10.  
      private String mUnitText = ""; // 单位
    11.  
      private int mUnitTextSize; // 单位字体大小
    12.  
      private int mMinValue; // 最小值
    13.  
      private int mMaxValue; // 最大值
    14.  
      private int mRibbonWidth; // 色条宽
    15.  
       
    16.  
      private int mStartAngle; // 起始角度
    17.  
      private int mSweepAngle; // 扫过角度
    18.  
       
    19.  
      private int mPointerRadius; // 三角形指针半径
    20.  
      private int mCircleRadius; // 中心圆半径
    21.  
       
    22.  
      private float mRealTimeValue = 0.0f; // 实时值
    23.  
       
    24.  
      private int mBigScaleRadius; // 大刻度半径
    25.  
      private int mSmallScaleRadius; // 小刻度半径
    26.  
      private int mNumScaleRadius; // 数字刻度半径
    27.  
       
    28.  
      private int mViewColor_green; // 字体颜色
    29.  
      private int mViewColor_yellow; // 字体颜色
    30.  
      private int mViewColor_orange; // 字体颜色
    31.  
      private int mViewColor_red; // 字体颜色
    32.  
       
    33.  
      private int mViewWidth; // 控件宽度
    34.  
      private int mViewHeight; // 控件高度
    35.  
      private float mCenterX;//中心点圆坐标x
    36.  
      private float mCenterY;//中心点圆坐标y
    37.  
       
    38.  
      private Paint mPaintScale;//圆盘上大小刻度画笔
    39.  
      private Paint mPaintScaleText;//圆盘上刻度值画笔
    40.  
      private Paint mPaintCirclePointer;//绘制中心圆,指针
    41.  
      private Paint mPaintValue;//绘制实时值
    42.  
      private Paint mPaintRibbon;//绘制色带
    43.  
       
    44.  
      private RectF mRectRibbon;//存储色带的矩形数据
    45.  
      private Rect mRectScaleText;//存储刻度值的矩形数据
    46.  
      private Path path;//绘制指针的路径
    47.  
       
    48.  
      private int mSmallScaleCount; // 小刻度总数
    49.  
      private float mBigScaleAngle; // 相邻两个大刻度之间的角度
    50.  
      private float mSmallScaleAngle; // 相邻两个小刻度之间的角度
    51.  
       
    52.  
      private String[] mGraduations; // 每个大刻度的刻度值
    53.  
      private float initAngle;//指针实时角度
    54.  
       
    55.  
      private SweepGradient mSweepGradient ;//设置渐变
    56.  
      private int[] color = new int[7];//渐变颜色组
    57.  
       
    58.  
      public NoiseboardView(Context context) {
    59.  
      this(context, null);
    60.  
      }
    61.  
       
    62.  
      public NoiseboardView(Context context, AttributeSet attrs) {
    63.  
      this(context, attrs, 0);
    64.  
      }
    65.  
       
    66.  
      public NoiseboardView(Context context, AttributeSet attrs, int defStyleAttr) {
    67.  
      super(context, attrs, defStyleAttr);
    68.  
      //自定义属性
    69.  
      TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.NoiseboardView, defStyleAttr, 0);
    70.  
       
    71.  
      mRadius = a.getDimensionPixelSize(R.styleable.NoiseboardView_radius, dpToPx(80));
    72.  
      mBigSliceCount = a.getInteger(R.styleable.NoiseboardView_bigSliceCount, 5);
    73.  
      mScaleCountInOneBigScale = a.getInteger(R.styleable.NoiseboardView_sliceCountInOneBigSlice, 5);
    74.  
      mScaleColor = a.getColor(R.styleable.NoiseboardView_scaleColor, Color.WHITE);
    75.  
      mScaleTextSize = a.getDimensionPixelSize(R.styleable.NoiseboardView_scaleTextSize, spToPx(12));
    76.  
      mUnitText = a.getString(R.styleable.NoiseboardView_unitText);
    77.  
      mUnitTextSize = a.getDimensionPixelSize(R.styleable.NoiseboardView_unitTextSize, spToPx(14));
    78.  
      mMinValue = a.getInteger(R.styleable.NoiseboardView_minValue, 0);
    79.  
      mMaxValue = a.getInteger(R.styleable.NoiseboardView_maxValue, 150);
    80.  
      mRibbonWidth = a.getDimensionPixelSize(R.styleable.NoiseboardView_ribbonWidth, 0);
    81.  
       
    82.  
      a.recycle();
    83.  
      init();
    84.  
      }
    85.  
       
    86.  
      private void init() {
    87.  
       
    88.  
      //起始角度是从水平正方向即(钟表3点钟方向)开始从0算的,扫过的角度是按顺时针方向算
    89.  
      mStartAngle = 175;
    90.  
      mSweepAngle = 190;
    91.  
       
    92.  
      mPointerRadius = mRadius / 3 * 2;
    93.  
      mCircleRadius = mRadius / 17;
    94.  
       
    95.  
      mSmallScaleRadius = mRadius - dpToPx(10);
    96.  
      mBigScaleRadius = mRadius - dpToPx(18);
    97.  
      mNumScaleRadius = mRadius - dpToPx(20);
    98.  
       
    99.  
      mSmallScaleCount = mBigSliceCount * 5;
    100.  
      mBigScaleAngle = mSweepAngle / (float) mBigSliceCount;
    101.  
      mSmallScaleAngle = mBigScaleAngle / mScaleCountInOneBigScale;
    102.  
      mGraduations = getMeasureNumbers();
    103.  
       
    104.  
      //确定控件的宽度 padding值,在构造方法执行完就被赋值
    105.  
      mViewWidth = getPaddingLeft() + mRadius * 2 + getPaddingRight() + dpToPx(4);
    106.  
      mViewHeight = mViewWidth;
    107.  
      mCenterX = mViewWidth / 2.0f;
    108.  
      mCenterY = mViewHeight / 2.0f;
    109.  
       
    110.  
      mPaintScale = new Paint();
    111.  
      mPaintScale.setAntiAlias(true);
    112.  
      mPaintScale.setColor(mScaleColor);
    113.  
      mPaintScale.setStyle(Paint.Style.STROKE);
    114.  
      mPaintScale.setStrokeCap(Paint.Cap.ROUND);
    115.  
       
    116.  
      mPaintScaleText = new Paint();
    117.  
      mPaintScaleText.setAntiAlias(true);
    118.  
      mPaintScaleText.setColor(mScaleColor);
    119.  
      mPaintScaleText.setStyle(Paint.Style.STROKE);
    120.  
       
    121.  
      mPaintCirclePointer = new Paint();
    122.  
      mPaintCirclePointer.setAntiAlias(true);
    123.  
       
    124.  
      mRectScaleText = new Rect();
    125.  
      path = new Path();
    126.  
       
    127.  
      mPaintValue = new Paint();
    128.  
      mPaintValue.setAntiAlias(true);
    129.  
      mPaintValue.setStyle(Paint.Style.STROKE);
    130.  
      mPaintValue.setTextAlign(Paint.Align.CENTER);
    131.  
      mPaintValue.setTextSize(mUnitTextSize);
    132.  
       
    133.  
      initAngle = getAngleFromResult(mRealTimeValue);
    134.  
       
    135.  
      mViewColor_green = getResources().getColor(R.color.green_value);
    136.  
      mViewColor_yellow = getResources().getColor(R.color.yellow_value);
    137.  
      mViewColor_orange = getResources().getColor(R.color.orange_value);
    138.  
      mViewColor_red = getResources().getColor(R.color.red_value);
    139.  
      color[0] = mViewColor_red;
    140.  
      color[1] = mViewColor_red;
    141.  
      color[2] = mViewColor_green;
    142.  
      color[3] = mViewColor_green;
    143.  
      color[4] = mViewColor_yellow;
    144.  
      color[5] = mViewColor_orange;
    145.  
      color[6] = mViewColor_red;
    146.  
       
    147.  
      //色带画笔
    148.  
      mPaintRibbon = new Paint();
    149.  
      mPaintRibbon.setAntiAlias(true);
    150.  
      mPaintRibbon.setStyle(Paint.Style.STROKE);
    151.  
      mPaintRibbon.setStrokeWidth(mRibbonWidth);
    152.  
      mSweepGradient = new SweepGradient(mCenterX, mCenterY,color,null);
    153.  
      mPaintRibbon.setShader(mSweepGradient);//设置渐变 从X轴正方向取color数组颜色开始渐变
    154.  
       
    155.  
      if (mRibbonWidth > 0) {
    156.  
      int r = mRadius - mRibbonWidth / 2 + dpToPx(1) ;
    157.  
      mRectRibbon = new RectF(mCenterX - r, mCenterY - r, mCenterX + r, mCenterY + r);
    158.  
      }
    159.  
      }
    160.  
       
    161.  
      /**
    162.  
      * 确定每个大刻度的值
    163.  
      * @return
    164.  
      */
    165.  
      private String[] getMeasureNumbers() {
    166.  
      String[] strings = new String[mBigSliceCount + 1];
    167.  
      for (int i = 0; i <= mBigSliceCount; i++) {
    168.  
      if (i == 0) {
    169.  
      strings[i] = String.valueOf(mMinValue);
    170.  
      } else if (i == mBigSliceCount) {
    171.  
      strings[i] = String.valueOf(mMaxValue);
    172.  
      } else {
    173.  
      strings[i] = String.valueOf(((mMaxValue - mMinValue) / mBigSliceCount) * i);
    174.  
      }
    175.  
      }
    176.  
      return strings;
    177.  
      }
    178.  
       
    179.  
      /**
    180.  
      * <dt>UNSPECIFIED : 0 << 30 = 0</dt>
    181.  
      * <dd>
    182.  
      * 父控件没有对子控件做限制,子控件可以是自己想要的尺寸
    183.  
      * 其实就是子空间在布局里没有设置宽高,但布局里添加控件都要设置宽高,所以这种情况暂时没碰到
    184.  
      * </dd>
    185.  
      *
    186.  
      * <dt>EXACTLY : 1 << 30 = 1073741824</dt>
    187.  
      * <dd>
    188.  
      * 父控件给子控件决定了确切大小,子控件将被限定在给定的边界里。
    189.  
      * 如果是填充父窗体(match_parent),说明父控件已经明确知道子控件想要多大的尺寸了,也是这种模式
    190.  
      * </dd>
    191.  
      *
    192.  
      * <dt>AT_MOST : 2 << 30 = -2147483648</dt>
    193.  
      * <dd>
    194.  
      * 在布局设置wrap_content,父控件并不知道子控件到底需要多大尺寸(具体值),
    195.  
      * 需要子控件在onMeasure测量之后再让父控件给他一个尽可能大的尺寸以便让内容全部显示
    196.  
      * 如果在onMeasure没有指定控件大小,默认会填充父窗体,因为在view的onMeasure源码中,
    197.  
      * AT_MOST(相当于wrap_content )和EXACTLY (相当于match_parent )两种情况返回的测量宽高都是specSize,
    198.  
      * 而这个specSize正是父控件剩余的宽高,所以默认onMeasure方法中wrap_content 和match_parent 的效果是一样的,都是填充剩余的空间。
    199.  
      * </dd>
    200.  
      *
    201.  
      * @param widthMeasureSpec
    202.  
      * @param heightMeasureSpec
    203.  
      */
    204.  
      @Override
    205.  
      protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    206.  
       
    207.  
      int widthMode = MeasureSpec.getMode(widthMeasureSpec);//从约束规范中获取模式
    208.  
      int widthSize = MeasureSpec.getSize(widthMeasureSpec);//从约束规范中获取尺寸
    209.  
      int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    210.  
      int heightSize = MeasureSpec.getSize(heightMeasureSpec);
    211.  
       
    212.  
      //在布局中设置了具体值
    213.  
      if (widthMode == MeasureSpec.EXACTLY)
    214.  
      mViewWidth = widthSize;
    215.  
       
    216.  
      //在布局中设置 wrap_content,控件就取能完全展示内容的宽度(同时需要考虑屏幕的宽度)
    217.  
      if (widthMode == MeasureSpec.AT_MOST)
    218.  
      mViewWidth = Math.min(mViewWidth, widthSize);
    219.  
       
    220.  
      if (heightMode == MeasureSpec.EXACTLY) {
    221.  
      mViewHeight = heightSize;
    222.  
      } else {
    223.  
       
    224.  
      float[] point1 = getCoordinatePoint(mRadius, mStartAngle);
    225.  
      float[] point2 = getCoordinatePoint(mRadius, mStartAngle + mSweepAngle);
    226.  
      float maxY = Math.max(Math.abs(point1[1]) - mCenterY, Math.abs(point2[1]) - mCenterY);
    227.  
      float f = mCircleRadius + dpToPx(2) + dpToPx(25) ;
    228.  
      float max = Math.max(maxY, f);
    229.  
      mViewHeight = (int) (max + mRadius + getPaddingTop() + getPaddingBottom() + dpToPx(2) * 2);
    230.  
       
    231.  
      if (heightMode == MeasureSpec.AT_MOST)
    232.  
      mViewHeight = Math.min(mViewHeight, heightSize);
    233.  
      }
    234.  
       
    235.  
      //保存测量宽度和测量高度
    236.  
      setMeasuredDimension(mViewWidth, mViewHeight);
    237.  
      }
    238.  
       
    239.  
       
    240.  
      @Override
    241.  
      protected void onDraw(Canvas canvas) {
    242.  
      // 绘制色带
    243.  
      canvas.drawArc(mRectRibbon, 170, 199, false, mPaintRibbon);
    244.  
       
    245.  
      mPaintScale.setStrokeWidth(dpToPx(2));
    246.  
      for (int i = 0; i <= mBigSliceCount; i++) {
    247.  
      //绘制大刻度
    248.  
      float angle = i * mBigScaleAngle + mStartAngle;
    249.  
      float[] point1 = getCoordinatePoint(mRadius, angle);
    250.  
      float[] point2 = getCoordinatePoint(mBigScaleRadius, angle);
    251.  
      canvas.drawLine(point1[0], point1[1], point2[0], point2[1], mPaintScale);
    252.  
       
    253.  
      //绘制圆盘上的数字
    254.  
      mPaintScaleText.setTextSize(mScaleTextSize);
    255.  
      String number = mGraduations[i];
    256.  
      mPaintScaleText.getTextBounds(number, 0, number.length(), mRectScaleText);
    257.  
      if (angle % 360 > 135 && angle % 360 < 215) {
    258.  
      mPaintScaleText.setTextAlign(Paint.Align.LEFT);
    259.  
      } else if ((angle % 360 >= 0 && angle % 360 < 45) || (angle % 360 > 325 && angle % 360 <= 360)) {
    260.  
      mPaintScaleText.setTextAlign(Paint.Align.RIGHT);
    261.  
      } else {
    262.  
      mPaintScaleText.setTextAlign(Paint.Align.CENTER);
    263.  
      }
    264.  
      float[] numberPoint = getCoordinatePoint(mNumScaleRadius, angle);
    265.  
      if (i == 0 || i == mBigSliceCount) {
    266.  
      canvas.drawText(number, numberPoint[0], numberPoint[1] + (mRectScaleText.height() / 2), mPaintScaleText);
    267.  
      } else {
    268.  
      canvas.drawText(number, numberPoint[0], numberPoint[1] + mRectScaleText.height(), mPaintScaleText);
    269.  
      }
    270.  
      }
    271.  
       
    272.  
      //绘制小的子刻度
    273.  
      mPaintScale.setStrokeWidth(dpToPx(1));
    274.  
      for (int i = 0; i < mSmallScaleCount; i++) {
    275.  
      if (i % mScaleCountInOneBigScale != 0) {
    276.  
      float angle = i * mSmallScaleAngle + mStartAngle;
    277.  
      float[] point1 = getCoordinatePoint(mRadius, angle);
    278.  
      float[] point2 = getCoordinatePoint(mSmallScaleRadius, angle);
    279.  
       
    280.  
      mPaintScale.setStrokeWidth(dpToPx(1));
    281.  
      canvas.drawLine(point1[0], point1[1], point2[0], point2[1], mPaintScale);
    282.  
      }
    283.  
      }
    284.  
       
    285.  
      if (mRealTimeValue <= 40) {
    286.  
      mPaintValue.setColor(mViewColor_green);
    287.  
      mPaintCirclePointer.setColor(mViewColor_green);
    288.  
      } else if (mRealTimeValue > 40 && mRealTimeValue <= 90) {
    289.  
      mPaintValue.setColor(mViewColor_yellow);
    290.  
      mPaintCirclePointer.setColor(mViewColor_yellow);
    291.  
      } else if (mRealTimeValue > 90 && mRealTimeValue <= 120) {
    292.  
      mPaintValue.setColor(mViewColor_orange);
    293.  
      mPaintCirclePointer.setColor(mViewColor_orange);
    294.  
      } else {
    295.  
      mPaintValue.setColor(mViewColor_red);
    296.  
      mPaintCirclePointer.setColor(mViewColor_red);
    297.  
      }
    298.  
       
    299.  
      //绘制中心点的圆
    300.  
      mPaintCirclePointer.setStyle(Paint.Style.STROKE);
    301.  
      mPaintCirclePointer.setStrokeWidth(dpToPx(4));
    302.  
      canvas.drawCircle(mCenterX, mCenterY, mCircleRadius + dpToPx(3), mPaintCirclePointer);
    303.  
       
    304.  
      //绘制三角形指针
    305.  
      path.reset();
    306.  
      mPaintCirclePointer.setStyle(Paint.Style.FILL);
    307.  
      float[] point1 = getCoordinatePoint(mCircleRadius / 2, initAngle + 90);
    308.  
      path.moveTo(point1[0], point1[1]);
    309.  
      float[] point2 = getCoordinatePoint(mCircleRadius / 2, initAngle - 90);
    310.  
      path.lineTo(point2[0], point2[1]);
    311.  
      float[] point3 = getCoordinatePoint(mPointerRadius, initAngle);
    312.  
      path.lineTo(point3[0], point3[1]);
    313.  
      path.close();
    314.  
      canvas.drawPath(path, mPaintCirclePointer);
    315.  
       
    316.  
      // 绘制三角形指针底部的圆弧效果
    317.  
      canvas.drawCircle((point1[0] + point2[0]) / 2, (point1[1] + point2[1]) / 2, mCircleRadius / 2, mPaintCirclePointer);
    318.  
       
    319.  
      //绘制实时值
    320.  
      canvas.drawText(trimFloat(mRealTimeValue)+" "+ mUnitText, mCenterX, mCenterY - mRadius / 3 , mPaintValue);
    321.  
      }
    322.  
       
    323.  
      private int dpToPx(int dp) {
    324.  
      return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics());
    325.  
      }
    326.  
       
    327.  
      private int spToPx(int sp) {
    328.  
      return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, getResources().getDisplayMetrics());
    329.  
      }
    330.  
       
    331.  
      /**
    332.  
      * 依圆心坐标,半径,扇形角度,计算出扇形终射线与圆弧交叉点的xy坐标
    333.  
      */
    334.  
      public float[] getCoordinatePoint(int radius, float cirAngle) {
    335.  
      float[] point = new float[2];
    336.  
       
    337.  
      double arcAngle = Math.toRadians(cirAngle); //将角度转换为弧度
    338.  
      if (cirAngle < 90) {
    339.  
      point[0] = (float) (mCenterX + Math.cos(arcAngle) * radius);
    340.  
      point[1] = (float) (mCenterY + Math.sin(arcAngle) * radius);
    341.  
      } else if (cirAngle == 90) {
    342.  
      point[0] = mCenterX;
    343.  
      point[1] = mCenterY + radius;
    344.  
      } else if (cirAngle > 90 && cirAngle < 180) {
    345.  
      arcAngle = Math.PI * (180 - cirAngle) / 180.0;
    346.  
      point[0] = (float) (mCenterX - Math.cos(arcAngle) * radius);
    347.  
      point[1] = (float) (mCenterY + Math.sin(arcAngle) * radius);
    348.  
      } else if (cirAngle == 180) {
    349.  
      point[0] = mCenterX - radius;
    350.  
      point[1] = mCenterY;
    351.  
      } else if (cirAngle > 180 && cirAngle < 270) {
    352.  
      arcAngle = Math.PI * (cirAngle - 180) / 180.0;
    353.  
      point[0] = (float) (mCenterX - Math.cos(arcAngle) * radius);
    354.  
      point[1] = (float) (mCenterY - Math.sin(arcAngle) * radius);
    355.  
      } else if (cirAngle == 270) {
    356.  
      point[0] = mCenterX;
    357.  
      point[1] = mCenterY - radius;
    358.  
      } else {
    359.  
      arcAngle = Math.PI * (360 - cirAngle) / 180.0;
    360.  
      point[0] = (float) (mCenterX + Math.cos(arcAngle) * radius);
    361.  
      point[1] = (float) (mCenterY - Math.sin(arcAngle) * radius);
    362.  
      }
    363.  
       
    364.  
      Log.e("getCoordinatePoint","radius="+radius+",cirAngle="+cirAngle+",point[0]="+point[0]+",point[1]="+point[1]);
    365.  
      return point;
    366.  
      }
    367.  
       
    368.  
      /**
    369.  
      * 通过实时数值得到指针角度
    370.  
      */
    371.  
      private float getAngleFromResult(float result) {
    372.  
      if (result > mMaxValue)
    373.  
      return 360.0f;
    374.  
      return mSweepAngle * (result - mMinValue) / (mMaxValue - mMinValue) + mStartAngle;
    375.  
      }
    376.  
       
    377.  
      /**
    378.  
      * float类型如果小数点后为零则显示整数否则保留
    379.  
      */
    380.  
      public static String trimFloat(float value) {
    381.  
      if (Math.round(value) - value == 0) {
    382.  
      return String.valueOf((long) value);
    383.  
      }
    384.  
      return String.valueOf(value);
    385.  
      }
    386.  
       
    387.  
       
    388.  
      public float getRealTimeValue() {
    389.  
      return mRealTimeValue;
    390.  
      }
    391.  
       
    392.  
      /**
    393.  
      * 实时设置读数值
    394.  
      * @param realTimeValue
    395.  
      */
    396.  
      public void setRealTimeValue(float realTimeValue) {
    397.  
      if (realTimeValue > mMaxValue) return;
    398.  
      mRealTimeValue = realTimeValue;
    399.  
      initAngle = getAngleFromResult(mRealTimeValue);
    400.  
      invalidate();
    401.  
      }
    402.  
       
    403.  
      }

    具体代码请看Github

    没有梯子请点击这里下载

  • 相关阅读:
    Swift和Objective-C混编注意
    【算法设计与数据结构】为何程序员喜欢将INF设置为0x3f3f3f3f?(转)
    baidu网盘下载神器 Pandownload
    为什么大学要学一堆纸上谈兵的课程?(转)
    数据结构实训——校园导游系统
    数据结构实训——哈希表设计
    数据结构实训——员工管理系统
    数据结构实训——成绩统计系统
    数据结构——链表实现一元多项式的表示和加法
    数据结构——顺序表的一些算法
  • 原文地址:https://www.cnblogs.com/it-tsz/p/10842270.html
Copyright © 2020-2023  润新知