• (一)自定义ViewGroup绘制出菜单


          从网上学习了hyman大神的卫星菜单实现,自己特意亲自又写了一编代码,对自定义ViewGroup的理解又深入了一点。我坚信只有自己写出来的知识才会有更加好的的掌握。因此也在自己的博客中将这个卫星菜单的案例给实现了。废话不多说,直接进入正题吧。

          该案例的完整代码以及相关的图片素材下载链接如下:

    http://download.csdn.net/detail/fuyk12/9233573

    一、效果展示

          首先上一张效果图,如下:

           这是在模拟器上的运行效果,有点卡顿。在真机上,是会很流畅,而且动画效果显示的会更清晰。

          效果说明:红色按钮是菜单按钮,点击它,会弹出菜单选项。菜单选项会像一个四分之一圆一样围绕在菜单按钮周围。而菜单的弹出采用了动画效果。如果点击菜单,则同时菜单消失且会弹出一个提示框。更具体的细节效果,看上面的效果图应该很明白了。

          所用的知识:(1)自定义ViewGroup。整个菜单的绘制就是一个自定义的ViewGroup。所以这其中要清楚理解view的绘制流程,还要掌握住自定义属性等知识。

                            (2)android中的常用的补间动画,例如菜单的弹出就是平移动画和旋转动画的结合。而点击菜单时,是缩放动画和透明动画的实现。

         难点:我们android的补间动画实际上视觉上的一种错觉,比如一个按钮你看到它平移了,实际上它只是android在另外一个地方重新绘制了,并不是原来的按钮,原来的按钮还      在初始位置,因此你点击它是没有效果的。为了解决这个问题,android推出了属性动画。但是属性动画向下兼容性不是很好。为了解决这样子的难点,我们依旧采用补                 间 动画,但是在逻辑上做了一个巧妙的处理。具体的怎么处理,等到了这一步,再详细说明。

         实现这个案例的思路:我们打算分这么几步来实现这个卫星菜单。

                                     (1)首先将各个菜单以及按钮给完整的绘制出来。即完成自定义ViewGroup。

                                     (2)完成菜单的弹出动画。

                                     (3)完成菜单的点击动画。

                                     (4)完成菜单点击事件的逻辑,比如弹出提示框。

           因此这是一个实战的案例,不是基础知识的讲解。需要读者具有相关的知识后才能很好的阅读。那么本篇文章就来实现第一步,即将整个菜单给绘制出来。

    二、自定义ViewGruop完成菜单绘制

           从菜单按钮以及菜单选项,都是一个自定义的ViewGroup。从效果图上可以看到,菜单选项以一个四分之一圆的方式围绕菜单。因此这个自定义的ViewGroup需要具有圆的半径这个属性。效果图中只展示了按钮位于右下角的情况,其实那个加号按钮完全可以放在左上,左下,右上。因此,我们给这个自定义的ViewGroup再添加一个属性,为位置属性。这样子一来,用户就可以放在四个角的其他位置了。这些分析,其实就是自定义属性里面的内容。表现在代码中,如下所示。

            新建项目,然后再res下的values文件下新建attrs.xml文件,其中的代码如下:

     1 <?xml version="1.0" encoding="utf-8"?>
     2 <resources>
     3     
     4     <attr name="position">
     5         <enum name="left_top" value= "0"/>
     6         <enum name="left_bottom" value= "1"/>
     7         <enum name="right_top" value= "2"/>
     8         <enum name="right_bottom" value= "3"/>
     9     </attr>
    10     <attr name="radius" format="dimension"/>
    11     
    12     <declare-styleable name = "ArcMenu">
    13        <attr name="position"/>
    14         <attr name="radius"/>
    15     </declare-styleable>
    16     
    17 </resources>

          从代码中,可以看到我们定义了一个属性集合“ArcMenu",其中有两个属性"position"和"radius”,对应于自定义ViewGruop的位置和需要的半径。其实ArcMenu就是我们自定义ViewGroup的名称。我们先将其建立出来。

          新建类ArcMenu继承自ViewGroup,代码如下:

     1 package com.example.menu;
     2 
     3 import android.content.Context;
     4 import android.content.res.TypedArray;
     5 import android.util.AttributeSet;
     6 import android.util.TypedValue;
     7 import android.view.View;
     8 import android.view.View.OnClickListener;
     9 import android.view.animation.Animation;
    10 import android.view.animation.RotateAnimation;
    11 import android.view.ViewGroup;
    12 
    13 public class ArcMenu extends ViewGroup {
    14     
    15     
    16     public ArcMenu(Context context) {
    17         this(context,null);
    18     }
    19     public ArcMenu(Context context, AttributeSet attrs) {
    20         this(context,attrs,0);
    21     }
    22     public ArcMenu(Context context, AttributeSet attrs, int defStyle) {
    23         super(context, attrs, defStyle);
    24         
    25         }
    26     
    27 }

          代码很简单,我们没有做任何操作。下面要做的就是把这个自定义的ViewGroup添加到布局中,并向里面添加控件,即我们的菜单。

          修改activity_main.xml中的代码,如下:

     1 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
     2     xmlns:tools="http://schemas.android.com/tools"
     3     xmlns:kun="http://schemas.android.com/apk/res/com.example.menu"
     4     android:layout_width="match_parent"
     5     android:layout_height="match_parent"
     6     android:orientation="vertical" >
     7     
     8     <com.example.menu.ArcMenu
     9         android:layout_width="match_parent"
    10         android:layout_height="match_parent"
    11         kun:position="right_bottom"
    12         kun:radius="200dp">
    13         <RelativeLayout 
    14                android:layout_width="wrap_content"
    15             android:layout_height="wrap_content"
    16             android:background="@drawable/composer_button" >
    17             <ImageView 
    18                 android:id="@+id/id_button"
    19                 android:layout_width="wrap_content"
    20                 android:layout_height="wrap_content"
    21                  android:layout_centerInParent="true"
    22                 android:src="@drawable/composer_icn_plus"/>
    23         </RelativeLayout>
    24          <ImageView 
    25                android:layout_width="wrap_content"
    26                 android:layout_height="wrap_content"
    27                 android:src="@drawable/composer_camera"
    28                 android:tag="Camera"/>
    29          <ImageView 
    30                android:layout_width="wrap_content"
    31                   android:layout_height="wrap_content"
    32                 android:src="@drawable/composer_music"
    33                 android:tag="Music"/>
    34          <ImageView 
    35                android:layout_width="wrap_content"
    36                   android:layout_height="wrap_content"
    37                 android:src="@drawable/composer_place"
    38                 android:tag="Place"/>
    39          <ImageView 
    40                android:layout_width="wrap_content"
    41                   android:layout_height="wrap_content"
    42                 android:src="@drawable/composer_sleep"
    43                 android:tag="Sleep"/>
    44           <ImageView 
    45                android:layout_width="wrap_content"
    46                   android:layout_height="wrap_content"
    47                 android:src="@drawable/composer_thought"
    48                 android:tag="Thought"/>
    49           <ImageView 
    50                 android:layout_width="wrap_content"
    51                   android:layout_height="wrap_content"
    52                 android:src="@drawable/composer_with"
    53                 android:tag="People"/>
    54         
    55     </com.example.menu.ArcMenu>
    56 
    57   
    58 </LinearLayout>

             大体是一个线性布局。然后在里面放入了自定义的ViewGroup,即ArcMenu。注意在第11行和第12行我们为ArcMenu指定了位置为右下,半径为200dp。你可以完全指定不同的位置和大小。而且我们在ArcMenu里面放入了好几个ImageView,这些ImageView就是我们的菜单,至于图片资源可以单击文章开头的链接下载。ImageView的个数,你完全可以自己来定,在这里我们放入了6个,,也就是有6个菜单选项。并且为每一个ImageView都指定了tag。这有什么用呢,先不用管它。你或许会有疑问,这些ImageView是怎么摆放的啊?所以接下来,赶快修改ArcMenu中的代码,将这个控件都摆放好。

             ArcMenu中的代码先贴出来吧,然后再解释,如下:

      1 package com.example.menu;
      2 
      3 import android.content.Context;
      4 import android.content.res.TypedArray;
      5 import android.util.AttributeSet;
      6 import android.util.TypedValue;
      7 import android.view.View;
      8 import android.view.View.OnClickListener;
      9 import android.view.animation.Animation;
     10 import android.view.animation.RotateAnimation;
     11 import android.view.ViewGroup;
     12 
     13 public class ArcMenu extends ViewGroup implements  OnClickListener {
     14     /**
     15      * 菜单按钮
     16      */
     17     private View mCBMenu;
     18     /**
     19      * 菜单的位置,为枚举类型
     20      * @author fuly1314
     21      *
     22      */
     23     private enum Position
     24     {
     25         LEFT_TOP,LEFT_BOTTOM,RIGHT_TOP,RIGHT_BOTTOM
     26     }
     27     /**
     28      * 菜单的状态
     29      * @author fuly1314
     30      *
     31      */
     32     private enum Status
     33     {
     34         OPEN,CLOSE
     35     }
     36     /**
     37      * 菜单为当前位置,默认为RIGHT_BOTTOM,在后面我们可以获取到
     38      */
     39     private Position mPosition = Position.RIGHT_BOTTOM;
     40     /**
     41      * 菜单的当前状态,默认为开启
     42      */
     43     private Status mCurStatus = Status.OPEN;
     44     
     45     /**
     46      * 菜单的半径,默认为120dp
     47      */
     48     private int mRadius = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 150,
     49             getResources().getDisplayMetrics());
     50 
     51     
     52     
     53     public ArcMenu(Context context) {
     54         this(context,null);
     55     }
     56     public ArcMenu(Context context, AttributeSet attrs) {
     57         this(context,attrs,0);
     58     }
     59     public ArcMenu(Context context, AttributeSet attrs, int defStyle) {
     60         super(context, attrs, defStyle);
     61         
     62         TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ArcMenu, defStyle, 0);
     63         //获取到菜单设置的位置
     64         int position = ta.getInt(R.styleable.ArcMenu_position, 3);
     65         
     66         switch(position){
     67         case 0:
     68             mPosition = Position.LEFT_TOP;
     69             break;
     70         case 1:
     71             mPosition = Position.LEFT_BOTTOM;
     72             break;
     73         case 2:
     74             mPosition = Position.RIGHT_TOP;
     75             break;
     76         case 3:
     77             mPosition = Position.RIGHT_BOTTOM;
     78             break;
     79         }
     80         
     81         //获取到菜单的半径
     82         mRadius = (int) ta.getDimension(R.styleable.ArcMenu_radius,
     83                 TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 120,
     84                         getResources().getDisplayMetrics()));            
     85         ta.recycle();
     86         
     87     }
     88     
     89     
     90     
     91     /**
     92      * 测量各个子View的大小
     93      */
     94     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) 
     95     {
     96         int count = getChildCount();//获取子view的数量
     97         
     98         for(int i=0;i<count;i++)
     99         {
    100             //测量子view的大小
    101             measureChild(getChildAt(i), widthMeasureSpec, heightMeasureSpec);
    102         }
    103         
    104         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    105     }
    106 
    107     /**
    108      * 摆放各个子view的位置
    109      */
    110     protected void onLayout(boolean changed, int l, int t, int r, int b) {
    111         
    112         if(changed)//如果发生了改变,就重新布局
    113         {
    114             layoutMainMenu();//菜单按钮的布局
    115             
    116         }
    117 
    118         
    119     }
    120     /**
    121      * 菜单按钮的布局
    122      */
    123     private void layoutMainMenu() {
    124         
    125          mCBMenu = getChildAt(0);//获得主菜单按钮
    126          
    127          mCBMenu.setOnClickListener(this);
    128         
    129         int left=0;
    130         int top=0;
    131         
    132         switch(mPosition)
    133         {
    134         case LEFT_TOP:
    135             left = 0;
    136             top = 0;
    137             break;
    138         case LEFT_BOTTOM:
    139             left = 0;
    140             top = getMeasuredHeight() - mCBMenu.getMeasuredHeight();
    141             break;
    142         case RIGHT_TOP:
    143             left = getMeasuredWidth() - mCBMenu.getMeasuredWidth();
    144             top = 0;
    145             break;
    146         case RIGHT_BOTTOM:
    147             left = getMeasuredWidth() - mCBMenu.getMeasuredWidth();
    148             top = getMeasuredHeight() - mCBMenu.getMeasuredHeight();
    149             break;
    150         }
    151         
    152         mCBMenu.layout(left, top, left+mCBMenu.getMeasuredWidth(), top+mCBMenu.getMeasuredHeight());
    153     }
    154     /**
    155      * 菜单按钮的点击事件
    156      * @param v
    157      */
    158     public void onClick(View v) {
    159         //为菜单按钮设置点击动画
    160         RotateAnimation rAnimation = new RotateAnimation(0f, 720f, Animation.RELATIVE_TO_SELF, 0.5f, 
    161                 Animation.RELATIVE_TO_SELF, 0.5f);
    162         
    163         rAnimation.setDuration(300);
    164         
    165         rAnimation.setFillAfter(true);
    166         
    167         v.startAnimation(rAnimation);
    168         
    169     }
    170 
    171 }

           代码有点长,但是很简单。首先是成员变量,我们需要知道当前菜单按钮的位置,是放在右下了还是左上,还需要当前菜单的状态,菜单已经折叠了还是已经打开了,更需要知道半径,这样才好设定菜单的位置啊,因此才有了这些成员变量。再然后就是在构造方法中获取我们在布局中的给ArcMenu设定的位置属性和半径属性,从而将成员变量初始化。

    接着在onMeasure方法,对放进ArcMenu中的每个子view进行测量,这样子就知道了每一个子view的大小,代码很简单,measureChild一下即可。然后就可以执行onLayout方法来摆放每一个子view的位置了。这里我们先摆放菜单按钮的位置,因为它比较简单。菜单,即布局中的ImageView先不管。然后又给菜单按钮添加了点击事件,即点击的时候,要它自身旋转一下。好了,代码很简单,看注释很容易理解。我们运行下,看看效果。如下:

          右下角有个红色的按钮,点击还有旋转(由于贴出来的不是动态图,因此旋转效果小伙伴可以在自己的实验中看到)。好了,菜单按钮我们布局算是成功了。下面就开始布局那些菜单吧。

           关于这些菜单,即这些ImageView的布局,牵涉到简单的数学知识。下面有一张我绘制的图片,简单来说明一下。

           假设菜单按钮放在左上角,围绕它的菜单如上图,红色的圈圈为代表。其中线段AB就是我们所设定的半径的长度,用radius来表示。现在我们如果想求A点的坐标(其中A是对应菜单的顶点坐标),根据简单的数学知识,就会得到如下结果:

    x = radius * cos(a)
    y = radius * sin(a)

          那么同样的道理,对于第 i 个菜单,它的顶点坐标就如下可求:

    x = radius * cos(a*i)
    y = radius * sin(a*i)

         好了,现在回到我们的程序中来。要知道,在代码中,你要注意以下问题:

    (1)遍历的子view的坐标是从0开始的。
    (2)遍历的子view包括菜单按钮,因为我们要出去它。
    (3)a的值应该是90除以菜单个数

          注意到上面的问题,再结合我们前面分析到的数学知识,很容易可以得到每一个子view的顶点坐标了,代码如下:

     1 for(int i=0;i<count-1;i++)
     2             {
     3                 View childView = getChildAt(i+1);//注意这里过滤掉菜单按钮,只要菜单选项view
     4                 
     5                 int left = (int) (mRadius*Math.cos(Math.PI/2/(count-2)*i));
     6                 int top = (int) (mRadius*Math.sin(Math.PI/2/(count-2)*i));
     7                 
     8                 switch(mPosition)
     9                 {
    10                 
    11                 case LEFT_TOP:
    12                     break;
    13                 case LEFT_BOTTOM:
    14                     top = getMeasuredHeight() - top-childView.getMeasuredHeight();
    15                     break;
    16                 case RIGHT_TOP:
    17                     left = getMeasuredWidth() - left-childView.getMeasuredWidth();
    18                     break;
    19                 case RIGHT_BOTTOM:
    20                     left = getMeasuredWidth() - left-childView.getMeasuredWidth();
    21                     top = getMeasuredHeight() - top-childView.getMeasuredHeight();
    22                     break;
    23                 }
    24 }

           怎么会多出switch语句呢?这是因为比如菜单按钮位于右下,则顶点坐标的高度应该是整体高度去掉控件本身的高度以及顶点高度。如下图所示:

          swtich的其他语句相信小伙伴仔细想想应该很快就会明白为什么这么计算了吧。道理是一样的。拿个本子,用笔画画就知道了

          讲了那么多,我们就将上面我们所讲的整合到代码中。那么修改ArcMenu,摆放我们的菜单吧!代码如下:

      1 package com.example.menu;
      2 
      3 import android.content.Context;
      4 import android.content.res.TypedArray;
      5 import android.util.AttributeSet;
      6 import android.util.TypedValue;
      7 import android.view.View;
      8 import android.view.View.OnClickListener;
      9 import android.view.animation.Animation;
     10 import android.view.animation.RotateAnimation;
     11 import android.view.ViewGroup;
     12 
     13 public class ArcMenu extends ViewGroup implements OnClickListener{
     14     /**
     15      * 菜单按钮
     16      */
     17     private View mCBMenu;
     18     /**
     19      * 菜单的位置,为枚举类型
     20      * @author fuly1314
     21      *
     22      */
     23     private enum Position
     24     {
     25         LEFT_TOP,LEFT_BOTTOM,RIGHT_TOP,RIGHT_BOTTOM
     26     }
     27     /**
     28      * 菜单的状态
     29      * @author fuly1314
     30      *
     31      */
     32     private enum Status
     33     {
     34         OPEN,CLOSE
     35     }
     36     /**
     37      * 菜单为当前位置,默认为RIGHT_BOTTOM,在后面我们可以获取到
     38      */
     39     private Position mPosition = Position.RIGHT_BOTTOM;
     40     /**
     41      * 菜单的当前状态,默认为开启
     42      */
     43     private Status mCurStatus = Status.OPEN;
     44     
     45     /**
     46      * 菜单的半径,默认为120dp
     47      */
     48     private int mRadius = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 150,
     49             getResources().getDisplayMetrics());
     50 
     51     
     52     
     53     public ArcMenu(Context context) {
     54         this(context,null);
     55     }
     56     public ArcMenu(Context context, AttributeSet attrs) {
     57         this(context,attrs,0);
     58     }
     59     public ArcMenu(Context context, AttributeSet attrs, int defStyle) {
     60         super(context, attrs, defStyle);
     61         
     62         TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ArcMenu, defStyle, 0);
     63         //获取到菜单设置的位置
     64         int position = ta.getInt(R.styleable.ArcMenu_position, 3);
     65         
     66         switch(position){
     67         case 0:
     68             mPosition = Position.LEFT_TOP;
     69             break;
     70         case 1:
     71             mPosition = Position.LEFT_BOTTOM;
     72             break;
     73         case 2:
     74             mPosition = Position.RIGHT_TOP;
     75             break;
     76         case 3:
     77             mPosition = Position.RIGHT_BOTTOM;
     78             break;
     79         }
     80         
     81         //获取到菜单的半径
     82         mRadius = (int) ta.getDimension(R.styleable.ArcMenu_radius,
     83                 TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 120,
     84                         getResources().getDisplayMetrics()));            
     85         ta.recycle();
     86         
     87     }
     88     
     89     
     90     
     91     /**
     92      * 测量各个子View的大小
     93      */
     94     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) 
     95     {
     96         int count = getChildCount();//获取子view的数量
     97         
     98         for(int i=0;i<count;i++)
     99         {
    100             //测量子view的大小
    101             measureChild(getChildAt(i), widthMeasureSpec, heightMeasureSpec);
    102         }
    103         
    104         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    105     }
    106 
    107     /**
    108      * 摆放各个子view的位置
    109      */
    110     protected void onLayout(boolean changed, int l, int t, int r, int b) {
    111         
    112         if(changed)//如果发生了改变,就重新布局
    113         {
    114             layoutMainMenu();//菜单按钮的布局
    115             /**
    116              * 下面的代码为菜单的布局
    117              */
    118             int count = getChildCount();
    119             
    120             for(int i=0;i<count-1;i++)
    121             {
    122                 View childView = getChildAt(i+1);//注意这里过滤掉菜单按钮,只要菜单选项view
    123                 
    124                 int left = (int) (mRadius*Math.cos(Math.PI/2/(count-2)*i));
    125                 int top = (int) (mRadius*Math.sin(Math.PI/2/(count-2)*i));
    126                 
    127                 switch(mPosition)
    128                 {
    129                 
    130                 case LEFT_TOP:
    131                     break;
    132                 case LEFT_BOTTOM:
    133                     top = getMeasuredHeight() - top-childView.getMeasuredHeight();
    134                     break;
    135                 case RIGHT_TOP:
    136                     left = getMeasuredWidth() - left-childView.getMeasuredWidth();
    137                     break;
    138                 case RIGHT_BOTTOM:
    139                     left = getMeasuredWidth() - left-childView.getMeasuredWidth();
    140                     top = getMeasuredHeight() - top-childView.getMeasuredHeight();
    141                     break;
    142                 }
    143                 
    144                 childView.layout(left, top, left+childView.getMeasuredWidth(),
    145                         top+childView.getMeasuredHeight());
    146             }
    147         }
    148 
    149         
    150     }
    151     /**
    152      * 菜单按钮的布局
    153      */
    154     private void layoutMainMenu() {
    155         
    156          mCBMenu = getChildAt(0);//获得主菜单按钮
    157          
    158          mCBMenu.setOnClickListener(this);
    159         
    160         int left=0;
    161         int top=0;
    162         
    163         switch(mPosition)
    164         {
    165         case LEFT_TOP:
    166             left = 0;
    167             top = 0;
    168             break;
    169         case LEFT_BOTTOM:
    170             left = 0;
    171             top = getMeasuredHeight() - mCBMenu.getMeasuredHeight();
    172             break;
    173         case RIGHT_TOP:
    174             left = getMeasuredWidth() - mCBMenu.getMeasuredWidth();
    175             top = 0;
    176             break;
    177         case RIGHT_BOTTOM:
    178             left = getMeasuredWidth() - mCBMenu.getMeasuredWidth();
    179             top = getMeasuredHeight() - mCBMenu.getMeasuredHeight();
    180             break;
    181         }
    182         
    183         mCBMenu.layout(left, top, left+mCBMenu.getMeasuredWidth(), top+mCBMenu.getMeasuredHeight());
    184     }
    185     /**
    186      * 菜单按钮的点击事件
    187      * @param v
    188      */
    189     public void onClick(View v) {
    190         //为菜单按钮设置点击动画
    191         RotateAnimation rAnimation = new RotateAnimation(0f, 720f, Animation.RELATIVE_TO_SELF, 0.5f, 
    192                 Animation.RELATIVE_TO_SELF, 0.5f);
    193         
    194         rAnimation.setDuration(300);
    195         
    196         rAnimation.setFillAfter(true);
    197         
    198         v.startAnimation(rAnimation);
    199         
    200     }
    201 
    202 }

          注意红色部分的代码,这样子我们就把每一个菜单摆放到位了。那么是不是呢?快运行一下程序,看看效果。如下:

           效果还不错哈。至此,ArcMenu每一个子view都绘制出来了。我们下面要出来的就是菜单的动画效果了。保存好现在的代码,快快进入下一节中吧。《实现菜单弹出动画》

  • 相关阅读:
    单片机第11课:串口通信查询方式---从计算机接受数据
    Android(Lollipop/5.0) Material Design(四) 创建列表和卡片
    智能指针
    [ACM] ural 1057 Amount of degrees (数位统计)
    vs2008,2010,2012安装包下载
    《智能主义》:360未来5年的方向是物联网安全与安全硬件。3星。
    《近视怎么办》:三甲医院眼科医生的科普。4星
    《当黑天鹅坐上无人车》。耐药性的蔓延是“公地悲剧”的例子之一;负责的人看不到造成的损失。3星
    《特朗普经济学》:考虑到全球供应链的新世界,征收关税就像在工厂中央竖起一堵墙。3星。
    《互联网大败局》:3星。10多年来中国互联网失败案例的公开资料汇编。
  • 原文地址:https://www.cnblogs.com/fuly550871915/p/4930470.html
Copyright © 2020-2023  润新知