Android 三种动画

Android 三种动画的使用 – Tween Animation
Android的动画种类,常用到的有3种:Tween Animation,Frame Animation, SurfaceView

其中,Tween Animation不适合游戏开发,只适合简单的页面动画,比如页面翻页;Frame Animation可用在游戏和一般的动画上;SurfaceView适合用在游戏上,部分游戏引擎就是基于SurfaceView的。

1:Tween Animation 适合简单的页面动画
2:Frame Animation 用在游戏(简单动画)和一般的动画上
3:SurfaceView 适合用在游戏上,部分游戏引擎就是基于SurfaceView的

 


一:Tween Animation(补间动画)四种类型的解释

Android中实现补间动画可通过XML或者直接Java代码实现,这种动画比较简单,Android SDK自带的例子里就有4种的用法举例。

(1)XML中

alpha 渐变透明度动画效果
scale 渐变尺寸伸缩动画效果
translate 画面转换位置移动动画效果
rotate 画面转移旋转动画效果

(2)Java代码中

AlphaAnimation 渐变透明度动画效果
ScaleAnimation 渐变尺寸伸缩动画效果
TranslateAnimation 画面转换位置移动动画效果
RotateAnimation 画面转移旋转动画效果

下面以XML的格式讲解每种的各自用法,Java代码格式的则对应这4种。

1.Alpha

Full Screen
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android" >
<alpha
android:fromAlpha="0.1"
android:toAlpha="1.0"
android:duration="3000"
/>
<!-- 透明度控制动画效果 alpha
        浮点型值:
            fromAlpha 属性为动画起始时透明度
            toAlpha   属性为动画结束时透明度
            说明:
                0.0表示完全透明
                1.0表示完全不透明
            以上值取0.0-1.0之间的float数据类型的数字

        长整型值:
            duration  属性为动画持续时间
            说明:
                时间以毫秒为单位
-->
</set>2.Scale

Full Screen
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
   <scale
          android:interpolator=
                     "@android:anim/accelerate_decelerate_interpolator"
          android:fromXScale="0.0"
          android:toXScale="1.4"
          android:fromYScale="0.0"
          android:toYScale="1.4"
          android:pivotX="50%"
          android:pivotY="50%"
          android:fillAfter="false"
          android:duration="700" />
</set>
<!-- 尺寸伸缩动画效果 scale
       属性:interpolator 指定一个动画的插入器
        在我试验过程中,使用android.res.anim中的资源时候发现
        有三种动画插入器:
            accelerate_decelerate_interpolator  加速-减速 动画插入器
            accelerate_interpolator        加速-动画插入器
            decelerate_interpolator        减速- 动画插入器
        其他的属于特定的动画效果
      浮点型值:

            fromXScale 属性为动画起始时 X坐标上的伸缩尺寸
            toXScale   属性为动画结束时 X坐标上的伸缩尺寸   

            fromYScale 属性为动画起始时Y坐标上的伸缩尺寸
            toYScale   属性为动画结束时Y坐标上的伸缩尺寸  

            说明:
                 以上四种属性值  

                    0.0表示收缩到没有
                    1.0表示正常无伸缩
                    值小于1.0表示收缩
                    值大于1.0表示放大

            pivotX     属性为动画相对于物件的X坐标的开始位置
            pivotY     属性为动画相对于物件的Y坐标的开始位置

            说明:
                    以上两个属性值 从0%-100%中取值
                    50%为物件的X或Y方向坐标上的中点位置

        长整型值:
            duration  属性为动画持续时间
            说明:   时间以毫秒为单位

        布尔型值:
            fillAfter 属性 当设置为true ,该动画转化在动画结束后被应用
-->3.Translate

Full Screen
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:fromXDelta="30"
android:toXDelta="-80"
android:fromYDelta="30"
android:toYDelta="300"
android:duration="2000"
/>
<!-- translate 位置转移动画效果
        整型值:
            fromXDelta 属性为动画起始时 X坐标上的位置
            toXDelta   属性为动画结束时 X坐标上的位置
            fromYDelta 属性为动画起始时 Y坐标上的位置
            toYDelta   属性为动画结束时 Y坐标上的位置
            注意:
                     没有指定fromXType toXType fromYType toYType 时候,
                     默认是以自己为相对参照物
        长整型值:
            duration  属性为动画持续时间
            说明:   时间以毫秒为单位
-->
</set>4.Rotate

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<rotate
        android:interpolator="@android:anim/accelerate_decelerate_interpolator"
        android:fromDegrees="0"
        android:toDegrees="+350"
        android:pivotX="50%"
        android:pivotY="50%"
        android:duration="3000" />
<!-- rotate 旋转动画效果
       属性:interpolator 指定一个动画的插入器
             在我试验过程中,使用android.res.anim中的资源时候发现
             有三种动画插入器:
                accelerate_decelerate_interpolator   加速-减速 动画插入器
                accelerate_interpolator               加速-动画插入器
                decelerate_interpolator               减速- 动画插入器
             其他的属于特定的动画效果

       浮点数型值:
            fromDegrees 属性为动画起始时物件的角度
            toDegrees   属性为动画结束时物件旋转的角度 可以大于360度 

            说明:
                     当角度为负数——表示逆时针旋转
                     当角度为正数——表示顺时针旋转
                     (负数from——to正数:顺时针旋转)
                     (负数from——to负数:逆时针旋转)
                     (正数from——to正数:顺时针旋转)
                     (正数from——to负数:逆时针旋转)     

            pivotX     属性为动画相对于物件的X坐标的开始位置
            pivotY     属性为动画相对于物件的Y坐标的开始位置

            说明:        以上两个属性值 从0%-100%中取值
                         50%为物件的X或Y方向坐标上的中点位置

        长整型值:
            duration  属性为动画持续时间
            说明:       时间以毫秒为单位
-->
</set>二:Tween Animation(补间动画)用法举例

可参考Android SDK自带的例子,其中在res/anim文件夹下定义了zoom_enter.xml和zoom_exit.xml两个动画定义文件,用到了Scale和Alpha这两种动画形式。

这里以Alpha的用法举例,分别以XML格式和Java代码两种方式来实现。

1:XML格式实现

1)、在res目录下创建一个anim文件夹,并新增一个名为alpha的xml文件。

<?xml version="1.0" encoding="UTF-8"?>
<set
 xmlns:android="http://schemas.android.com/apk/res/android">
 <alpha
  android:fromAlpha="0.1"
  android:toAlpha="1.0"
  android:duration="5000" />
</set>2)、将  这个星星放到drawable文件下,我们将用alpha来实现闪烁。

3)、现在调用前面命名为alpha的xml格式动画

    private void startAnimationFromXML() {
        // 加载动画
        Animation animation = AnimationUtils.loadAnimation(this, R.anim.alpha);
        animation.setRepeatCount(Animation.INFINITE);//无限循环
        // 动画开始
        ImageView starImg = (ImageView)findViewById(R.id.imageView1);
        starImg.startAnimation(animation);
    }如果这样,其实是不会出现闪烁的,原因是setRepeatCount在java代码里设置不起作用,必须在xml中,不知道为什么会这样?现在xml修改为:<?xml version="1.0" encoding="UTF-8"?>
<set
 xmlns:android="http://schemas.android.com/apk/res/android">
 <alpha
  android:fromAlpha="0.1"
  android:toAlpha="1.0"
  android:duration="1000"
  android:repeatCount="infinite"
  android:repeatMode="restart" />
</set>就可以了。使用AnimationUtils类,可以非常方便的构造动画类;现在你就可以看到这颗星星不停的在夜空中闪烁了。 2:Java代码直接实现

XML来定义动画有其方便之处,不过,有些时候,我们也喜欢用Java code的方式来实现。


    private void startAnitionFromJavaCode(){
     AlphaAnimation animation=new AlphaAnimation(0.1f, 1.0f);
     //说明:
     // 0.0表示完全透明
     // 1.0表示完全不透明
     // 设置动画持续时间
     animation.setDuration(1000);
     animation.setRepeatCount(Animation.INFINITE);
     animation.setRepeatMode(Animation.RESTART);
     ImageView starImg = (ImageView)findViewById(R.id.imageView1);
        starImg.startAnimation(animation);
    }
本节关键词:Animation、AnimationUtils、startAnimation。
==================================================================
Android 三种动画的使用 – Frame Animation
=====================================================================
Frame帧动画通俗的说就是像放电影那样一帧一帧的连续播放出来。Frame帧动画主要是通过AnimationDrawable类来实现的,它有start()和stop()两个重要的方法来启动和停止动画。

一、一个动画序列图的实现,即Frame-by-Frame动画,有两种方法:

1、animation-list配置,预先将一个动画按照每帧分解成的多个图片所组成的序列。然后再在Android的配置文件中将这些图片配置到动画里面。不过这种方式通常是不推荐的,因为这会导致一大堆分割后的图片出现。

    <animation-list xmlns:android="http://schemas.android.com/apk/res/android"
        android:oneshot="false">
        <item android:drawable="@drawable/explode1" android:duration="50" />
        <item android:drawable="@drawable/explode2" android:duration="50" />
        <item android:drawable="@drawable/explode3" android:duration="50" />
        <item android:drawable="@drawable/explode4" android:duration="50" />
    </animation-list>  2、AnimationDrawable动画。将同一动画序列的每帧图片都合并到一个大的图片中去,然后读取图片的时候按照约定好的宽、高去读就能准确的将该帧图片精确的读出来了。下图是星星闪烁序列图。

 

现在以第二种举例说明Frame Animation的用法。

二、Demo演示

1、新建一个工程后,将上面的星星序列图和下面的星星爆炸图放入工程。

 

2、在布局文件中,这里是main.xml加入两个imageView

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
 xmlns:android="http://schemas.android.com/apk/res/android"
 android:orientation="vertical"
 android:layout_width="fill_parent"
 android:layout_height="fill_parent">
 <TextView
  android:layout_width="fill_parent"
  android:layout_height="wrap_content"
  android:text="@string/hello" />
 <ImageView
  android:layout_height="35dip"
  android:layout_width="35dip"
  android:id="@+id/testImage"></ImageView>
 <ImageView
  android:src="@drawable/icon"
  android:id="@+id/imageView12"
  android:layout_height="90dip"
  android:layout_width="90dip"></ImageView>
</LinearLayout>3、使用AnimationDrawable来实现动画

这里要用到图片切割,所以会用到BitmapDrawable来获取一个Bitmap,

Resources res=getResources(); 
BitmapDrawable bmpDraw=(BitmapDrawable)res.getDrawable(R.drawable.vanishing_magiccube);

然后通过getBitmap获取一个Bitmap。

得到这个星星序列图之后,如何分割呢:

     for (int frame = 0; frame < 5; frame++) { 

         Bitmap bitmap = Bitmap.createBitmap(bmp,
                 frame*90,
                 0,
                 90,
                 90);
         animationDrawable2.addFrame(new BitmapDrawable(bitmap),120);
     }Bitmap的createBitmap方法,可以实现图片切割功能,具体请参考文档,这里星星序列图由5个小图组成,所以循环截取5个图片,然后将截取后的图片一个一个的放入animationDrawable中,间隔时间为120毫秒。

private void startFrameAnimation1(){
     animationDrawable = new AnimationDrawable();
     Bitmap[] bitmaps = new Bitmap[3];
     //从资源文件中获取图片资源
     Resources res=getResources();
     BitmapDrawable bmpDraw=(BitmapDrawable)res.getDrawable(R.drawable.power_light_star2);
     Bitmap bmp=bmpDraw.getBitmap();
     for (int frame = 0; frame < bitmaps.length; frame++) { 

         Bitmap bitmap = Bitmap.createBitmap(bmp,
                 frame*35,
                 0,
                 35,
                 35);
         animationDrawable.addFrame(new BitmapDrawable(bitmap),80);
     }// for,每层有 PLAYER_XIAOXUE_WALK_FRAME 帧 
      //Sets whether the animation should play once or repeat.
     animationDrawable.setOneShot(false);
     ImageView starImg = (ImageView)findViewById(R.id.testImage);

     starImg.setImageDrawable(animationDrawable);

     // run the start() method later on the UI thread
     //注意这里,因为是在onCreate()这个方法里加载了setImageDrawable,还没延迟,
     //如果直接使用animationDrawable.start();是不会有任何效果的。
     //所以,如果要在这里调用,必须使用post致一个Runnable那里,或者
     //在onWindowFocusChanged实现,因为这个时候,已经获取了图片。
     starImg.post(new Starter());

    }这里需要注意的是,不能在onCreate中直接调用animationDrawable的start方法。

有两种方法来解决这个animationDrawable不工作的问题。

第一个是使用post,让Runnable来启动start,第二个是使用onWindowFocusChange来启动start。Demo演示中使用了两种方法来分别启动frame 动画。

本节关键词:AnimationDrawable、BitmapDrawable、Bitmap、Frame Animation

============================================================================
Android 三种动画的使用 – SurfaceView Animation
============================================================================
对于做游戏或者视频相关开发的时候,将会用到SurfaceView,一般情况下,在使用View的时候,对于画面的更新都是在我们的xml文件定义好的View里进行的,而这个动作在主线程下完成的,这就会导致用户交互、快速绘制新UI,或者当前渲染代码阻塞主线程的时间过长的时候,比如游戏场景,就不能使用这种方式,这个时候SurfaceView就能很好解决这个问题。

SurfaceView继承自View,所以你可以像普通View那样使用,不过SurfaceView需要实现SurfaceHolder的CallBack。

一、使用场合:

被动更新画面的。比如棋类,这种用view就好了。因为画面的更新是依赖于 onTouch 来更新,可以直接使用 invalidate。 因为这种情况下,这一次Touch和下一次的Touch需要的时间比较长些,不会产生影响。

主动更新。比如一个人在一直跑动。这就需要一个单独的thread不停的重绘人的状态,避免阻塞main UI thread。所以显然view不合适,需要surfaceView来控制。

它的特性是:可以在主线程之外的线程中向屏幕绘图。这样可以避免画图任务繁重的时候造成主线程阻塞,从而提高了程序的反应速度。

surfaceCreated和surfaceDestroyed

使用SurfaceView 有一个原则,所有的绘图工作必须得在Surface 被创建之后才能开始。可以被直接复制到显存从而显示出来,这使得显示速度会非常快,而在Surface 被销毁之前必须结束。所以Callback 中的surfaceCreated 和surfaceDestroyed 就成了绘图处理代码的边界。

   1: (1)public void surfaceChanged(SurfaceHolder holder,int format,int width,int height){}
   2: 
   3:      //在surface的大小发生改变时激发
   4: 
   5:  (2)public void surfaceCreated(SurfaceHolder holder){}
   6: 
   7:      //在创建时激发,一般在这里调用画图的线程。
   8: 
   9:  (3)public void surfaceDestroyed(SurfaceHolder holder) {}
  10: 
  11:      //销毁时激发,一般在这里将画图的线程停止、释放。

二、使用方法:

1.继承SurfaceView并实现SurfaceHolder.Callback接口
2.SurfaceView.getHolder()获得SurfaceHolder对象
3.SurfaceHolder.addCallback(callback)添加回调函数
4.SurfaceHolder.lockCanvas()获得Canvas对象并锁定画布
5.Canvas绘画
6.SurfaceHolder.unlockCanvasAndPost(Canvas canvas)结束锁定画图,并提交改变,将图形显示。
 

以上6个步骤是通常需要做的几个步骤,当然你也可以根据自己的需要修改这些步骤.


三、用法举例

创建两个继承类mySurfaceView和myThread,然后在mainActivity的onCreate方法里加载mySurfaceView实例。

我们实现在一个黑色背景的视图上画一个正方形和写一句读秒的句子。

 

   1: (1)、abstract void addCallback(SurfaceHolder.Callback callback);
   2: // 给SurfaceView当前的持有者一个回调对象。
   4: // 锁定画布,一般在锁定后就可以通过其返回的画布对象Canvas,在其上面画图等操作了。
   5: (3)、abstract Canvas lockCanvas(Rect dirty);   3: (2)、abstract Canvas lockCanvas();
   6: // 锁定画布的某个区域进行画图等..因为画完图后,会调用下面的unlockCanvasAndPost来改变显示内容。
   7: // 相对部分内存要求比较高的游戏来说,可以不用重画dirty外的其它区域的像素,可以提高速度。
   8: (4)、abstract void unlockCanvasAndPost(Canvas canvas);
   9: // 结束锁定画图,并提交改变。
1、首先创建一个mainActivity,这相当于程序的入口,不过,不要调用setContentView(R.layout.main);因为我们这里不会去调用main.xml页面,我们通过SurfaceView来创建页面。

2、创建一个继承自SurfaceView,并实现SurfaceHolder的Callback类mySurfaceView

3、创建一个画图的线程,这个线程负责在mySurfaceView上面画上面的图形。

以上三步的代码如下:

   1: package com.lang.surfaceview01;
   2: 
   3: import android.app.Activity;
   4: import android.content.Context;
   5: import android.graphics.Canvas;
   6: import android.graphics.Color;
   7: import android.graphics.Paint;
   8: import android.graphics.Rect;
   9: import android.os.Bundle;
  10: import android.util.Log;
  11: import android.view.SurfaceHolder;
  12: import android.view.SurfaceView;
  13: 
  14: public class mainActivity extends Activity {
  15:     /** Called when the activity is first created. */
  16:     @Override
  17:     public void onCreate(Bundle savedInstanceState) {
  18:         super.onCreate(savedInstanceState);
  19:         //这里不要调用main视图,我们要通过SurfaceView来动态创建视图。
  20:         //setContentView(R.layout.main);
  21:         Log.i("tmp", "onCreate 1");
  22:         setContentView(new mySurfaceView(this));
  23:         Log.i("tmp", "onCreate 2");
  24:     }
  25: 
  26:     //创建一个继承自SurfaceView的mySurfaceView,
  27:     class mySurfaceView extends SurfaceView implements SurfaceHolder.Callback {
  28: 
  29:         private SurfaceHolder _surfaceHolder;
  30:         private myThread _myThread;
  31: 
  32:         /**
  33:          * 创建一个新的实例 mySurfaceView.
  34:          *
  35:          * @param context
  36:          */
  37:         public mySurfaceView(Context context) {
  38: 
  39:             super(context);
  40:             // TODO Auto-generated constructor stub
  41:             this._surfaceHolder = this.getHolder();
  42:             _surfaceHolder.addCallback(this);
  43:             _myThread = new myThread(_surfaceHolder);
  44:         }
  45: 
  46:         /* (non-Javadoc)
  47:          * @see android.view.SurfaceHolder.Callback#surfaceChanged(android.view.SurfaceHolder, int, int, int)
  48:          */
  49:         @Override
  50:         public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
  51: 
  52:             // TODO Auto-generated method stub
  53:             Log.i("tmp", "surfaceChanged");
  54:         }
  55: 
  56:         /* (non-Javadoc)
  57:          * @see android.view.SurfaceHolder.Callback#surfaceCreated(android.view.SurfaceHolder)
  58:          */
  59:         @Override
  60:         public void surfaceCreated(SurfaceHolder holder) {
  61:             Log.i("tmp", "surfaceCreated");
  62:             // TODO Auto-generated method stub
  63:             _myThread.setRunning(true);
  64:            
  65:             _myThread.start();
  66: 
  67:         }
  68: 
  69:         /* (non-Javadoc)
  70:          * @see android.view.SurfaceHolder.Callback#surfaceDestroyed(android.view.SurfaceHolder)
  71:          */
  72:         @Override
  73:         public void surfaceDestroyed(SurfaceHolder holder) {
  74:             Log.i("tmp", "surfaceDestroyed");
  75:             // TODO Auto-generated method stub
  76:             _myThread.stopThread();// = false;
  77:             //_myThread = null;
  78:         }
  79: 
  80:     }
  81: 
  82:     //专门用于绘图的线程。
  83:     class myThread extends Thread {
  84:         private SurfaceHolder _surfaceHolder;
  85:         //该线程如果没有运行,则退出线程
  86:         private boolean isRun;
  87:         int count = 0;
  88: 
  89:         public myThread(SurfaceHolder holder) {
  90:             this._surfaceHolder = holder;
  91:             this.isRun = true;
  92:         }
  93:         public void setRunning(boolean running){
  94:             this.isRun = running;
  95:         }
  96: 
  97:         //停止并销毁线程,销毁是通过线程的run方法执行结束后,线程被系统自动销毁,不需要
  98:         //认为的显示调用销毁。
  99:         public void stopThread() {
 100:             synchronized(this) {
 101:                 isRun = false;
 102:                 this.notify();
 103:             }
 104:           }
 105: 
 106:         /* (non-Javadoc)
 107:          * @see java.lang.Thread#run()
 108:          * 这个方法必须实现,当该方法执行完之后,则该线程自动销毁。
 109:          */
 110:         @Override
 111:         public void run() {
 112: 
 113:             while (isRun) {
 114:                 //注意这里要对surfaceHolder进行同步处理
 115:                 Canvas canvas = null;
 116:                 synchronized (this._surfaceHolder) {
 117:                     //锁定画布,一般在锁定后就可以通过其返回的画布对象Canvas,在其上面画图等操作了。 
 118:                     canvas = _surfaceHolder.lockCanvas();
 119:                     canvas.drawColor(Color.BLACK);//设置画布背景颜色 
 120:                     Paint p = new Paint(); //创建画笔 
 121:                     p.setColor(Color.WHITE);
 122:                     Rect r = new Rect(100, 50, 300, 250);
 123:                     canvas.drawRect(r, p);
 124:                     //在线程里一直话,直到isRun为false
 125:                     canvas.drawText("这是第" + (count++) + "秒", 100, 310, p);
 126:                     Log.i("thd", "" + count);
 127:                     try {
 128:                         Thread.sleep(1000);
 129:                     } catch (Exception e) {
 130: 
 131:                         // TODO Auto-generated catch block
 132:                         e.printStackTrace();
 133: 
 134:                     }//睡眠时间为1秒
 135: 
 136:                 }
 137: 
 138:                 if (canvas != null) //同步完之后释放画布并提交
 139:                 {
 140:                     _surfaceHolder.unlockCanvasAndPost(canvas);//结束锁定画图,并提交改变。 
 141: 
 142:                 }
 143: 
 144:             }
 145: 
 146:         }
 147: 
 148:     }
 149: }
现在运行上面的代码,可以看到上图被画出来了,但是,这段代码其实是有问题的,如果你按Back键后退,再进入,没有问题,但是,如果你按Home键(模拟器上的小房子)再进入的话,就会出现线程异常的错误:java.lang.IllegalThreadStateException: Thread already started.

导致这个错误发生的原因在于,一个线程只能被start一次,如果同一个线程被你在surfaceCreated方法里调用两次start则会报上面的异常错误。

但是,有个奇怪的问题是,及时该线程被销毁了,其状态为terminated,这个时候,通过debug可以看到,该线程已经不存在,但令人困惑的是,这个时候如果你再使用start,则依然报“Thread already started”,那么,既然该线程已经不存在,为什么还报这个错误呢?

 

针对这种情况必须修改surfaceCreated,判断该Thread的状态,如果为terminated,则新建一个,否则启动线程,这样,就能保证该线程以一个实例在运行。

   1: @Override
   2: public void surfaceCreated(SurfaceHolder holder) {
   3:     Log.i("tmp", "surfaceCreated");
   4:     // TODO Auto-generated method stub
   5:     _myThread.setRunning(true);
   6:     Log.i("tmp", "state: " + _myThread.getState());
   7:     //如果其状态为terminated,则表示该线程已经被销毁掉了。需要重新创建一个线程
   8:     if(_myThread.getState() == Thread.State.TERMINATED){
   9:         Log.i("tmp", "_myThread not TERMINATED");
  10:         _myThread = new myThread(_surfaceHolder);
  11:     }else{
  12:         Log.i("tmp", "_myThread already started.");
  13:     }
  14:     _myThread.start();
  15: 
  16: }

再次运行,则刚才的错误形象消失。

注意,这里不能使用Singlton模式来创建myThread,虽然你的初衷是想以单实例的模式运行线程,但事实上,线程是由系统控制的,run函数运行结束,系统自动销毁该线程。切忌不要用以前的Thread.Stop()去终止线程,这种方法因为安全问题,已被放弃使用。

本节关键词:SurfaceView、SurfaceHolder、CallBack、Thread、Run、线程销毁


 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值