zoukankan      html  css  js  c++  java
  • [Android] SurfaceView使用实例

    转自http://blog.csdn.net/sodino/article/details/7704084

    同样,先上效果图如下:


    效果图中,抛物线的动画即是由SurfaceView实现的。底部栏中的文字翻转详情相关帖子:
    [Android] 文字翻转动画的实现

    需求:
    1.实现抛物线动画
       1.1 设计物理模型,能够根据时间变量计算出某个时刻图片的X/Y坐标。
       1.2 将图片高频率(相比于UI线程的缓慢而言)刷新到界面中。这儿需要实现将脏界面清屏及刷新操作。
    2.文字翻转动画(已解决,见上面的帖子链接)

    下面来逐一解决所提出的问题。

    -----------------------------------------------------------------------------
    分隔线内容与Android无关,请慎读,勿拍砖。谢啦



    1.1 设计物理模型,如果大家还记得初中物理时,这并不难。自己写的草稿图见下:


    可以有:图片要从高度为H的位置下落,并且第一次与X轴碰撞时会出现能量损失,至原来的N%。并且我们需要图片的最终落点离起始位置在X轴上的位移为L,默认存在重力加速度g。
    详细的物理分析见上图啦,下面只说代码中如何实现,相关代码在PhysicalTool.java。
    第一次下落过程所耗时t1与高度height会有如下关系:

    [java] view plaincopy
    1. t1 = Math.sqrt(2 * height * 1.0d / GRAVITY);  

    第一次与X轴碰撞后上升至最高点的耗时t2与高度 N%*height会有:

    [java] view plaincopy
    1. t2 = Math.sqrt((1 - WASTAGE) * 2 * height * 1.0d / GRAVITY);  

    那么总的动画时间为(t1 + t2 + t2),则水平位移速度有(width为X轴总位移):

    [java] view plaincopy
    1. velocity = width * 1.0d / (t1 + 2 * t2);  

    则根据时间计算图片的实时坐标有:
    PhysicalTool.comput()

    [java] view plaincopy
    1. double used = (System.currentTimeMillis() - startTime) * 1.0d / 1000;  
    2. x = velocity * used;  
    3. if (0 <= used && used < t1) {  
    4.         y = height - 0.5d * GRAVITY * used * used;  
    5. else if (t1 <= used && used < (t1 + t2)) {  
    6.         double tmp = t1 + t2 - used;  
    7.         y = (1 - WASTAGE) * height - 0.5d * GRAVITY * tmp * tmp;  
    8. else if ((t1 + t2) <= used && used < (t1 + 2 * t2)) {  
    9.         double tmp = used - t1 - t2;  
    10.         y = (1 - WASTAGE) * height - 0.5d * GRAVITY * tmp * tmp;  
    11. }  
    Android无关内容结束了。
    ----------------------------------------------------------------------------------------

    1.2 SurfaceView刷新界面
            SurfaceView是一个特殊的UI组件,特殊在于它能够使用非UI线程刷新界面。至于为何具有此特殊性,将在另一个帖子"SurfaceView 相关知识笔记"中讨论,该帖子将讲述SurfaceView、Surface、ViewRoot、Window Manager/Window、Canvas等之间的关系。
            
            使用SurfaceView需要自定义组件继承该类,并实现SurfaceHolder.Callback,该回调提供了三个方法:

    [java] view plaincopy
    1. surfaceCreated()//通知Surface已被创建,可以在此处启动动画线程  
    2. surfaceChanged()//通知Surface已改变  
    3. surfaceDestroyed()//通知Surface已被销毁,可以在此处终止动画线程  

    SurfaceView使用有一个原则,即该界面操作必须在surfaceCreated之后及surfaceDestroyed之前。该回调的监听通过SurfaceHolder设置。代码如下:
    [java] view plaincopy
    1. //于SurfaceView类中,该类实现SurfaceHolder.Callback接口,如本例中的ParabolaView  
    2. SurfaceHolder holder = getHolder();  
    3. holder.addCallback(this);  

    示例代码中,通过启动DrawThread调用handleThread()实现对SurfaceView的刷新。
            刷新界面首先需要执行holder.lockCanvas()锁定Canvas并获得Canvas实例,然后进行界面更新操作,最后结束锁定Canvas,提交界面更改,至Surface最终显示在屏幕上。
            代码如下:

    [java] view plaincopy
    1. canvas = holder.lockCanvas();  
    2. … … … …   
    3. … … … …   
    4. canvas.drawBitmap(bitmap, x, y, paint);  
    5. holder.unlockCanvasAndPost(canvas);  


    本例中,需要清除屏幕脏区域,出于简便的做法,是将整个SurfaceView背景重复地设置为透明,代码为:

    [java] view plaincopy
    1. canvas.drawColor(Color.TRANSPARENT, android.graphics.PorterDuff.Mode.CLEAR);  

    对于SurfaceView的操作,下面这个链接讲述得更详细,更易理解,推荐去看下:
    Android开发之SurfaceView

    惯例,Java代码如下,XML请自行实现

    本文由Sodino所有,转载请注明出处:http://blog.csdn.net/sodino/article/details/7704084

    [java] view plaincopy
    1. ActSurfaceView.java  
    2.   
    3. package lab.sodino.surfaceview;  
    4.   
    5. import lab.sodino.surfaceview.RotateAnimation.InterpolatedTimeListener;  
    6. import android.app.Activity;  
    7. import android.graphics.BitmapFactory;  
    8. import android.os.Bundle;  
    9. import android.os.Handler;  
    10. import android.os.Handler.Callback;  
    11. import android.os.Message;  
    12. import android.view.View;  
    13. import android.view.View.OnClickListener;  
    14. import android.view.ViewGroup;  
    15. import android.widget.Button;  
    16. import android.widget.TextView;  
    17.   
    18. public class ActSurfaceView extends Activity implements OnClickListener, ParabolaView.ParabolaListener, Callback,  
    19.                 InterpolatedTimeListener {  
    20.         public static final int REFRESH_TEXTVIEW = 1;  
    21.         private Button btnStartAnimation;  
    22.         /** 动画界面。 */  
    23.         private ParabolaView parabolaView;  
    24.         /** 购物车处显示购物数量的TextView。 */  
    25.         private TextView txtNumber;  
    26.         /** 购物车中的数量。 */  
    27.         private int number;  
    28.         private Handler handler;  
    29.         /** TextNumber是否允许显示最新的数字。 */  
    30.         private boolean enableRefresh;  
    31.   
    32.         public void onCreate(Bundle savedInstanceState) {  
    33.                 super.onCreate(savedInstanceState);  
    34.                 setContentView(R.layout.main);  
    35.   
    36.                 handler = new Handler(this);  
    37.   
    38.                 number = 0;  
    39.   
    40.                 btnStartAnimation = (Button) findViewById(R.id.btnStartAnim);  
    41.                 btnStartAnimation.setOnClickListener(this);  
    42.   
    43.                 parabolaView = (ParabolaView) findViewById(R.id.surfaceView);  
    44.                 parabolaView.setParabolaListener(this);  
    45.   
    46.                 txtNumber = (TextView) findViewById(R.id.txtNumber);  
    47.         }  
    48.   
    49.         public void onClick(View v) {  
    50.                 if (v == btnStartAnimation) {  
    51.                         LogOut.out(this"isShowMovie:" + parabolaView.isShowMovie());  
    52.                         if (parabolaView.isShowMovie() == false) {  
    53.                                 number++;  
    54.                                 enableRefresh = true;  
    55.                                 parabolaView.setIcon(BitmapFactory.decodeResource(getResources(), R.drawable.icon));  
    56.                                 // 设置起始Y轴高度和终止X轴位移  
    57.                                 parabolaView.setParams(200, ((ViewGroup) txtNumber.getParent()).getLeft());  
    58.                                 parabolaView.showMovie();  
    59.                         }  
    60.                 }  
    61.         }  
    62.   
    63.         public void onParabolaStart(ParabolaView view) {  
    64.   
    65.         }  
    66.   
    67.         public void onParabolaEnd(ParabolaView view) {  
    68.                 handler.sendEmptyMessage(REFRESH_TEXTVIEW);  
    69.         }  
    70.   
    71.         public boolean handleMessage(Message msg) {  
    72.                 switch (msg.what) {  
    73.                 case REFRESH_TEXTVIEW:  
    74.   
    75.                         if (txtNumber.getVisibility() != View.VISIBLE) {  
    76.                                 txtNumber.setVisibility(View.VISIBLE);  
    77.                         }  
    78.                         RotateAnimation anim = new RotateAnimation(txtNumber.getWidth() >> 1, txtNumber.getHeight() >> 1,  
    79.                                         RotateAnimation.ROTATE_INCREASE);  
    80.                         anim.setInterpolatedTimeListener(this);  
    81.                         txtNumber.startAnimation(anim);  
    82.                         break;  
    83.                 }  
    84.                 return false;  
    85.         }  
    86.   
    87.         @Override  
    88.         public void interpolatedTime(float interpolatedTime) {  
    89.                 // 监听到翻转进度过半时,更新txtNumber显示内容。  
    90.                 if (enableRefresh && interpolatedTime > 0.5f) {  
    91.                         txtNumber.setText(Integer.toString(number));  
    92.                         // Log.d("ANDROID_LAB", "setNumber:" + number);  
    93.                         enableRefresh = false;  
    94.                 }  
    95.         }  
    96. }  

    [java] view plaincopy
    1. DrawThread.java  
    2.   
    3. package lab.sodino.surfaceview;  
    4.   
    5. import android.view.SurfaceView;  
    6.   
    7. /** 
    8.  * @author Sodino E-mail:sodinoopen@hotmail.com 
    9.  * @version Time:2012-6-18 上午03:14:31 
    10.  */  
    11. public class DrawThread extends Thread {  
    12.         private SurfaceView surfaceView;  
    13.         private boolean running;  
    14.   
    15.         public DrawThread(SurfaceView surfaceView) {  
    16.                 this.surfaceView = surfaceView;  
    17.         }  
    18.   
    19.         public void run() {  
    20.                 if (surfaceView == null) {  
    21.                         return;  
    22.                 }  
    23.                 if (surfaceView instanceof ParabolaView) {  
    24.                         ((ParabolaView) surfaceView).handleThread();  
    25.                 }  
    26.         }  
    27.   
    28.         public void setRunning(boolean b) {  
    29.                 running = b;  
    30.         }  
    31.   
    32.         public boolean isRunning() {  
    33.                 return running;  
    34.         }  
    35. }  

    [java] view plaincopy
    1. ParabolaView.java  
    2. package lab.sodino.surfaceview;  
    3.   
    4. import android.content.Context;  
    5. import android.graphics.Bitmap;  
    6. import android.graphics.Canvas;  
    7. import android.graphics.Color;  
    8. import android.graphics.Paint;  
    9. import android.graphics.PixelFormat;  
    10. import android.util.AttributeSet;  
    11. import android.view.SurfaceHolder;  
    12. import android.view.SurfaceView;  
    13.   
    14. /** 
    15.  * @author Sodino E-mail:sodinoopen@hotmail.com 
    16.  * @version Time:2012-6-18 上午02:52:33 
    17.  */  
    18. public class ParabolaView extends SurfaceView implements SurfaceHolder.Callback {  
    19.         /** 每30ms刷一帧。 */  
    20.         private static final long SLEEP_DURATION = 10l;  
    21.         private SurfaceHolder holder;  
    22.         /** 动画图标。 */  
    23.         private Bitmap bitmap;  
    24.         private DrawThread thread;  
    25.         private PhysicalTool physicalTool;  
    26.         private ParabolaView.ParabolaListener listener;  
    27.         /** 默认未创建,相当于Destory。 */  
    28.         private boolean surfaceDestoryed = true;  
    29.   
    30.         public ParabolaView(Context context, AttributeSet attrs, int defStyle) {  
    31.                 super(context, attrs, defStyle);  
    32.                 init();  
    33.         }  
    34.   
    35.         public ParabolaView(Context context, AttributeSet attrs) {  
    36.                 super(context, attrs);  
    37.                 init();  
    38.         }  
    39.   
    40.         public ParabolaView(Context context) {  
    41.                 super(context);  
    42.                 init();  
    43.         }  
    44.   
    45.         private void init() {  
    46.                 holder = getHolder();  
    47.                 holder.addCallback(this);  
    48.                 holder.setFormat(PixelFormat.TRANSPARENT);  
    49.   
    50.                 setZOrderOnTop(true);  
    51.                 // setZOrderOnTop(false);  
    52.   
    53.                 physicalTool = new PhysicalTool();  
    54.         }  
    55.   
    56.         @Override  
    57.         public void surfaceCreated(SurfaceHolder holder) {  
    58.                 surfaceDestoryed = false;  
    59.         }  
    60.   
    61.         @Override  
    62.         public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {  
    63.   
    64.         }  
    65.   
    66.         @Override  
    67.         public void surfaceDestroyed(SurfaceHolder holder) {  
    68.                 LogOut.out(this"surfaceDestroyed");  
    69.                 surfaceDestoryed = true;  
    70.                 physicalTool.cancel();  
    71.         }  
    72.   
    73.         public void handleThread() {  
    74.                 Canvas canvas = null;  
    75.   
    76.                 Paint pTmp = new Paint();  
    77.                 pTmp.setAntiAlias(true);  
    78.                 pTmp.setColor(Color.RED);  
    79.   
    80.                 Paint paint = new Paint();  
    81.                 // 设置抗锯齿  
    82.                 paint.setAntiAlias(true);  
    83.                 paint.setColor(Color.CYAN);  
    84.                 physicalTool.start();  
    85.                 LogOut.out(this"doing:" + physicalTool.doing());  
    86.                 if (listener != null) {  
    87.                         listener.onParabolaStart(this);  
    88.                 }  
    89.                 while (physicalTool.doing()) {  
    90.                         try {  
    91.                                 physicalTool.compute();  
    92.                                 canvas = holder.lockCanvas();  
    93.                                 // 设置画布的背景为透明。  
    94.                                 canvas.drawColor(Color.TRANSPARENT, android.graphics.PorterDuff.Mode.CLEAR);  
    95.                                 // 绘上新图区域  
    96.                                 float x = (float) physicalTool.getX();  
    97.                                 // float y = (float) physicalTool.getY();  
    98.                                 float y = (float) physicalTool.getMirrorY(getHeight(), bitmap.getHeight());  
    99.                                 // LogOut.out(this, "x:" + x + " y:" + y);  
    100.                                 canvas.drawRect(x, y, x + bitmap.getWidth(), y + bitmap.getHeight(), pTmp);  
    101.                                 canvas.drawBitmap(bitmap, x, y, paint);  
    102.                                 holder.unlockCanvasAndPost(canvas);  
    103.                                 Thread.sleep(SLEEP_DURATION);  
    104.                         } catch (Exception e) {  
    105.                                 e.printStackTrace();  
    106.                         }  
    107.                 }  
    108.                 // 清除屏幕内容  
    109.                 // 直接按"Home"回桌面,SurfaceView被销毁了,lockCanvas返回为null。  
    110.                 if (surfaceDestoryed == false) {  
    111.                         canvas = holder.lockCanvas();  
    112.                         canvas.drawColor(Color.TRANSPARENT, android.graphics.PorterDuff.Mode.CLEAR);  
    113.                         holder.unlockCanvasAndPost(canvas);  
    114.                 }  
    115.   
    116.                 thread.setRunning(false);  
    117.                 if (listener != null) {  
    118.                         listener.onParabolaEnd(this);  
    119.                 }  
    120.         }  
    121.   
    122.         public void showMovie() {  
    123.                 if (thread == null) {  
    124.                         thread = new DrawThread(this);  
    125.                 } else if (thread.getState() == Thread.State.TERMINATED) {  
    126.                         thread.setRunning(false);  
    127.                         thread = new DrawThread(this);  
    128.                 }  
    129.                 LogOut.out(this"thread.getState:" + thread.getState());  
    130.                 if (thread.getState() == Thread.State.NEW) {  
    131.                         thread.start();  
    132.                 }  
    133.         }  
    134.   
    135.         /** 正在播放动画时,返回true;否则返回false。 */  
    136.         public boolean isShowMovie() {  
    137.                 return physicalTool.doing();  
    138.         }  
    139.   
    140.         public void setIcon(Bitmap bit) {  
    141.                 bitmap = bit;  
    142.         }  
    143.   
    144.         public void setParams(int height, int width) {  
    145.                 physicalTool.setParams(height, width);  
    146.         }  
    147.   
    148.         /** 设置抛物线的动画监听器。 */  
    149.         public void setParabolaListener(ParabolaView.ParabolaListener listener) {  
    150.                 this.listener = listener;  
    151.         }  
    152.   
    153.         static interface ParabolaListener {  
    154.                 public void onParabolaStart(ParabolaView view);  
    155.   
    156.                 public void onParabolaEnd(ParabolaView view);  
    157.         }  
    158. }  


    [java] view plaincopy
    1. PhysicalTool.java  
    2. package lab.sodino.surfaceview;  
    3.   
    4. /** 
    5.  * @author Sodino E-mail:sodinoopen@hotmail.com 
    6.  * @version Time:2012-6-18 上午06:07:16 
    7.  */  
    8. public class PhysicalTool {  
    9.         /** 重力加速度值。 */  
    10.         private static final float GRAVITY = 400.78033f;  
    11.         /** 与X轴碰撞后,重力势能损失掉的百分比。 */  
    12.         private static final float WASTAGE = 0.3f;  
    13.         /** 起始下降高度。 */  
    14.         private int height;  
    15.         /** 起始点到终点的X轴位移。 */  
    16.         private int width;  
    17.         /** 水平位移速度。 */  
    18.         private double velocity;  
    19.         /** X Y坐标。 */  
    20.         private double x, y;  
    21.         /** 动画开始时间。 */  
    22.         private long startTime;  
    23.         /** 首阶段下载的时间。 单位:毫秒。 */  
    24.         private double t1;  
    25.         /** 第二阶段上升与下载的时间。 单位:毫秒。 */  
    26.         private double t2;  
    27.         /** 动画正在进行时值为true,反之为false。 */  
    28.         private boolean doing;  
    29.   
    30.         public void start() {  
    31.                 startTime = System.currentTimeMillis();  
    32.                 doing = true;  
    33.         }  
    34.   
    35.         /** 设置起始下落的高度及水平初速度;并以此计算小球下落的第一阶段及第二阶段上升耗时。 */  
    36.         public void setParams(int h, int w) {  
    37.                 height = h;  
    38.                 width = w;  
    39.   
    40.                 t1 = Math.sqrt(2 * height * 1.0d / GRAVITY);  
    41.                 t2 = Math.sqrt((1 - WASTAGE) * 2 * height * 1.0d / GRAVITY);  
    42.                 velocity = width * 1.0d / (t1 + 2 * t2);  
    43.                 LogOut.out(this"t1=" + t1 + " t2=" + t2);  
    44.         }  
    45.   
    46.         /** 根据当前时间计算小球的X/Y坐标。 */  
    47.         public void compute() {  
    48.                 double used = (System.currentTimeMillis() - startTime) * 1.0d / 1000;  
    49.                 x = velocity * used;  
    50.                 if (0 <= used && used < t1) {  
    51.                         y = height - 0.5d * GRAVITY * used * used;  
    52.                 } else if (t1 <= used && used < (t1 + t2)) {  
    53.                         double tmp = t1 + t2 - used;  
    54.                         y = (1 - WASTAGE) * height - 0.5d * GRAVITY * tmp * tmp;  
    55.                 } else if ((t1 + t2) <= used && used < (t1 + 2 * t2)) {  
    56.                         double tmp = used - t1 - t2;  
    57.                         y = (1 - WASTAGE) * height - 0.5d * GRAVITY * tmp * tmp;  
    58.                 } else {  
    59.                         LogOut.out(this"used:" + used + " set doing false");  
    60.                         x = velocity * (t1 + 2 * t2);  
    61.                         y = 0;  
    62.                         doing = false;  
    63.                 }  
    64.         }  
    65.   
    66.         public double getX() {  
    67.                 return x;  
    68.         }  
    69.   
    70.         public double getY() {  
    71.                 return y;  
    72.         }  
    73.   
    74.         /** 反转Y轴正方向。适应手机的真实坐标系。 */  
    75.         public double getMirrorY(int parentHeight, int bitHeight) {  
    76.                 int half = parentHeight >> 1;  
    77.                 double tmp = half + (half - y);  
    78.                 tmp -= bitHeight;  
    79.                 return tmp;  
    80.         }  
    81.   
    82.         public boolean doing() {  
    83.                 return doing;  
    84.         }  
    85.   
    86.         public void cancel() {  
    87.                 doing = false;  
    88.         }  
    89. }  

    [java] view plaincopy
    1. RotateAnimation.java  
    2. package lab.sodino.surfaceview;  
    3.   
    4. import android.graphics.Camera;  
    5. import android.graphics.Matrix;  
    6. import android.view.animation.Animation;  
    7. import android.view.animation.Transformation;  
    8.   
    9. /** 
    10.  * @author Sodino E-mail:sodinoopen@hotmail.com 
    11.  * @version Time:2012-6-27 上午07:32:00 
    12.  */  
    13. public class RotateAnimation extends Animation {  
    14.         /** 值为true时可明确查看动画的旋转方向。 */  
    15.         public static final boolean DEBUG = false;  
    16.         /** 沿Y轴正方向看,数值减1时动画逆时针旋转。 */  
    17.         public static final boolean ROTATE_DECREASE = true;  
    18.         /** 沿Y轴正方向看,数值减1时动画顺时针旋转。 */  
    19.         public static final boolean ROTATE_INCREASE = false;  
    20.         /** Z轴上最大深度。 */  
    21.         public static final float DEPTH_Z = 310.0f;  
    22.         /** 动画显示时长。 */  
    23.         public static final long DURATION = 800l;  
    24.         /** 图片翻转类型。 */  
    25.         private final boolean type;  
    26.         private final float centerX;  
    27.         private final float centerY;  
    28.         private Camera camera;  
    29.         /** 用于监听动画进度。当值过半时需更新txtNumber的内容。 */  
    30.         private InterpolatedTimeListener listener;  
    31.   
    32.         public RotateAnimation(float cX, float cY, boolean type) {  
    33.                 centerX = cX;  
    34.                 centerY = cY;  
    35.                 this.type = type;  
    36.                 setDuration(DURATION);  
    37.         }  
    38.   
    39.         public void initialize(int width, int height, int parentWidth, int parentHeight) {  
    40.                 // 在构造函数之后、getTransformation()之前调用本方法。  
    41.                 super.initialize(width, height, parentWidth, parentHeight);  
    42.                 camera = new Camera();  
    43.         }  
    44.   
    45.         public void setInterpolatedTimeListener(InterpolatedTimeListener listener) {  
    46.                 this.listener = listener;  
    47.         }  
    48.   
    49.         protected void applyTransformation(float interpolatedTime, Transformation transformation) {  
    50.                 // interpolatedTime:动画进度值,范围为[0.0f,10.f]  
    51.                 if (listener != null) {  
    52.                         listener.interpolatedTime(interpolatedTime);  
    53.                 }  
    54.                 float from = 0.0f, to = 0.0f;  
    55.                 if (type == ROTATE_DECREASE) {  
    56.                         from = 0.0f;  
    57.                         to = 180.0f;  
    58.                 } else if (type == ROTATE_INCREASE) {  
    59.                         from = 360.0f;  
    60.                         to = 180.0f;  
    61.                 }  
    62.                 float degree = from + (to - from) * interpolatedTime;  
    63.                 boolean overHalf = (interpolatedTime > 0.5f);  
    64.                 if (overHalf) {  
    65.                         // 翻转过半的情况下,为保证数字仍为可读的文字而非镜面效果的文字,需翻转180度。  
    66.                         degree = degree - 180;  
    67.                 }  
    68.                 // float depth = 0.0f;  
    69.                 float depth = (0.5f - Math.abs(interpolatedTime - 0.5f)) * DEPTH_Z;  
    70.                 final Matrix matrix = transformation.getMatrix();  
    71.                 camera.save();  
    72.                 camera.translate(0.0f, 0.0f, depth);  
    73.                 camera.rotateY(degree);  
    74.                 camera.getMatrix(matrix);  
    75.                 camera.restore();  
    76.                 if (DEBUG) {  
    77.                         if (overHalf) {  
    78.                                 matrix.preTranslate(-centerX * 2, -centerY);  
    79.                                 matrix.postTranslate(centerX * 2, centerY);  
    80.                         }  
    81.                 } else {  
    82.                         matrix.preTranslate(-centerX, -centerY);  
    83.                         matrix.postTranslate(centerX, centerY);  
    84.                 }  
    85.         }  
    86.   
    87.         /** 动画进度监听器。 */  
    88.         public static interface InterpolatedTimeListener {  
    89.                 public void interpolatedTime(float interpolatedTime);  
    90.         }  
    91. }  

    夜深了,晚安啰...
  • 相关阅读:
    如何使用 Python 创建一名可操控的角色玩家
    Unity查找物体的四大主流方法及区别
    JavaFX桌面应用开发-鼠标事件和键盘事件
    profiler-gpu分析记录
    JavaFX桌面应用开发-Button(按钮)与事件
    CodeCombat代码全记录(Python学习利器)--Kithgard地牢代码1
    spine骨骼动画组件使用详解
    微信小程序animation
    LeetCode--不同路径
    Learning opencv续不足(七)线图像的设计D
  • 原文地址:https://www.cnblogs.com/walccott/p/4957581.html
Copyright © 2011-2022 走看看