Android多媒体之Camera的相关操作

零、前言

今天主要有两点

1).界面布局,视图仿一下我手机自带的相机 2).Camera的简单使用,虽然Camera已经过时了,但还是来看一下,由简入深 下一篇会介绍替代者:Camera2 温馨提示:本文多图预警,请Wifi观看~

权限申请自行解决

<uses-permission android:name="android.permission.CAMERA"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.RECORD_AUDIO"/>


一、SurfaceView与Camera

1.View绘制原理及普通View局限性
View通过刷新重绘视图,Android系统通过发出VSYNC信号进行屏幕的重绘,刷新的间隔时间为16ms。
如果16ms内View完成需要执行的所有操作,在视觉上,不会产生卡顿的感觉;反之卡顿。
特别的需要频繁刷新的界面上,如游戏(60FPS以上),就会不断阻塞主线程,从而导致界面卡顿。

比较

刷新

刷新时线程

双缓冲

普通View

主动

仅主线程

SurfaceView

被动

允许子线程

SurfaceView相当于是另一个绘图线程,它是不会阻碍主线程,并且它在底层实现机制中实现了双缓冲机制
一个View需要频繁的刷新,或者在刷新时数据处理量大(可能引起卡顿),可以考虑使用SurfaceView来替代。
很明显相机随时捕捉画面,需要频繁的刷新,使用SurfaceView比较好

2.布局-整个SurfaceView
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".CameraActivity">

    <SurfaceView
        android:id="@+id/id_sv_video"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</android.support.constraint.ConstraintLayout>

3.SurfaceView和Camera的使用
public class CameraActivity extends AppCompatActivity implements SurfaceHolder.Callback {

    @BindView(R.id.id_sv_video)
    SurfaceView mIdSvVideo;
    private Camera camera;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);

        mIdSvVideo.getHolder().addCallback(this);

        // 打开摄像头并将展示方向旋转90度
        camera = Camera.open();
        camera.setDisplayOrientation(90);
    }

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        try {
            camera.setPreviewDisplay(holder);//Camera+SurfaceHolder
            camera.startPreview();//开启预览
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        camera.release();//释放资源
    }
}

二、界面布局:

1.这是手机自带的Camera布局

2.下载图标:iconfont+

下载图标.png


3.仿制界面

这是我仿的布局,具体怎么布局的,不是本篇的要点,自己看源码吧。

仿制界面


三、数据的捕获

1.Camera类中的回调接口
1.1--PreviewCallback

经测试camera.startPreview();之后,PreviewCallback的onPreviewFrame方法会不断回调 也就是说监听这个方法就可以获得连续的帧,这也是视频数据的来源

public interface PreviewCallback{
    void onPreviewFrame(byte[] data, Camera camera);
};

1.2--ShutterCallback

拍照的那一刻回调

 @Deprecated
 public interface ShutterCallback{
    void onShutter();
 }

1.3--PictureCallback

拍照后回调--data便是图片数据

@Deprecated
public interface PictureCallback {
    void onPictureTaken(byte[] data, Camera camera);
};

1.4--AutoFocusCallback

自动聚焦监听

@Deprecated
public interface AutoFocusCallback{
    void onAutoFocus(boolean success, Camera camera);
}

2.常用方法
2.1.拍照方法:takePicture
Camera open() 打开一个Camera(生成对象)
void startPreview() 开启预览
void stopPreview() 关闭预览
void release() 释放资源
void autoFocus(AutoFocusCallback cb) 自动聚焦
2.1.拍照方法:takePicture
---->[四参的:takePicture]-------------

 * @param shutter   拍照瞬间回调
 * @param raw       回调未压缩的原始数据
 * @param postview  回调与postview图像数据
 * @param jpeg      回调JPEG图片数据
 */
public final void takePicture(ShutterCallback shutter, PictureCallback raw,
        PictureCallback postview, PictureCallback jpeg) {
      
 ---->[三参的:takePicture,第三参null]-------------       
public final void takePicture(ShutterCallback shutter, PictureCallback raw,
        PictureCallback jpeg) {
    takePicture(shutter, raw, null, jpeg);
}

4.拍照的功能实现

拍完照要camera.startPreview();再开启预览, 否则界面就不动了 这里测试拍照的文件名写死了(避免拍太多测试照片...),你可以用当前时间当文件名

hello.jpg

mIdIvSnap.setOnClickListener(v->{
    camera.takePicture(new Camera.ShutterCallback() {
        @Override
        public void onShutter() {
        }
    }, new Camera.PictureCallback() {
        @Override
        public void onPictureTaken(byte[] data, Camera camera) {
        }
    }, new Camera.PictureCallback() {
        @Override
        public void onPictureTaken(byte[] data, Camera camera) {
             File pic = FileHelper.get().createFile("pic/hello.jpg");
             FileOutputStream fos = null;
             try {
                 fos = new FileOutputStream(pic);
                 fos.write(data);
                 fos.flush();
                 fos.close();
                 camera.startPreview();
             } catch (IOException e) {
                 e.printStackTrace();
                 try {
                     assert fos != null;
                     fos.close();
                 } catch (IOException e1) {
                     e1.printStackTrace();
                 }
             }
    });
});

5.延迟拍照

思路很简单,就是用Handler发送延迟消息,将一个TextView先居中隐藏

延迟拍照.gif


5.1:延迟按钮的点击效果

选中时拍照延迟3s(此处简单地写死,当然你也可以暴漏设置方法)

延迟按钮点击.gif

private boolean isDelay = false;//是否延迟

mIdIvDelay.setOnClickListener(v -> {
    if (!isDelay) {
        mIdIvDelay.setImageTintList(ColorStateList.valueOf(0xffEFB90F));
    } else {
        mIdIvDelay.setImageTintList(ColorStateList.valueOf(0xfffffffF));
    }
    isDelay = !isDelay;
});

5.2:Handler发送延迟消息
private static final int DEFAULT_DELAY_COUNT = 3 + 1;//默认延迟时间3s
private int mCurDelayCount = DEFAULT_DELAY_COUNT;//当前倒计时时间

private Handler mHandler = new Handler() {//Handler
    @Override
    public void handleMessage(Message msg) {
        super.handleMessage(msg);
        mIdTvCountDown.setText(mCurDelayCount + "");
    }
};


//点击拍照按钮
mIdIvSnap.setOnClickListener(v -> {
    if (!isDelay) {//如果无延迟直接拍
        takePicture("pic/hello.jpg");
        return;
    }
    mIdTvCountDown.setVisibility(View.VISIBLE);
    mCurDelayCount = DEFAULT_DELAY_COUNT;
    mHandler.post(new Runnable() {
        @Override
        public void run() {
            if (mCurDelayCount > 0) {
                mCurDelayCount--;
                L.d(mCurDelayCount + L.l());
                mHandler.postDelayed(this, 1000);//延迟1s
                mHandler.sendEmptyMessage(0);//发送消息
            } else {
                takePicture("pic/hello.jpg");
                mIdTvCountDown.setVisibility(View.GONE);
            }
        }
    });
});

/**
 * 拍照方法封装
 *
 * @param name 图片名称(加文件夹:形式如:pic/hello.jpg)
 */
private void takePicture(String name) {
    camera.takePicture(null, null, (data, camera) -> {
        File pic = FileHelper.get().createFile(name);
        FileOutputStream fos = null;
        try {
            fos = new FileOutputStream(pic);
            fos.write(data);
            fos.flush();
            fos.close();
            camera.startPreview();
        } catch (IOException e) {
            e.printStackTrace();
            try {
                assert fos != null;
                fos.close();
            } catch (IOException e1) {
                e1.printStackTrace();
            }
        }
    });
}

三、其他相关

1.自动聚焦

点击SurfaceView自动聚焦(也就是变清楚)

//自动聚焦
mIdSvVideo.setOnClickListener(v -> {
    camera.autoFocus(new Camera.AutoFocusCallback() {
        @Override
        public void onAutoFocus(boolean success, Camera camera) {
        }
    });
});

2.改变焦距(即放大缩小)

我默认给了10个等级,放到最大之后回到开始大小

变焦.gif

private int currZoom;//当前缩放数
mParameters = camera.getParameters();//相机参数

/**
 * 缩放封装
 */
public void setZoom() {
    if (mParameters.isZoomSupported()) {//是否支持缩放
        try {
            Camera.Parameters params = mParameters;
            final int maxZoom = params.getMaxZoom();
            if (maxZoom == 0) return;
            currZoom = params.getZoom();
            currZoom += maxZoom / 10;
            if (currZoom > maxZoom) {
                currZoom = 0;
            }
            params.setZoom(currZoom);
            camera.setParameters(params);
            String rate = new DecimalFormat("#.0").format(currZoom / (maxZoom / 10 * 2.f) + 1);
            mIdIvZoom.setText(rate + "x");
        } catch (Exception e) {
            e.printStackTrace();
        }
    } else {
        ToastUtil.show(this, "您的手机不支持变焦功能!");
    }
}

3.打灯

打灯.gif

private boolean isFlashLight;//是否开启闪光灯

//开闪光灯
mIdIvSplash.setOnClickListener(v -> {
    if (!isFlashLight) {
        mIdIvSplash.setImageTintList(ColorStateList.valueOf(0xffEFB90F));
    } else {
        mIdIvSplash.setImageTintList(ColorStateList.valueOf(0xfffffffF));
    }
    isFlashLight = !isFlashLight;
    mParameters.setFlashMode(
            isFlashLight?Camera.Parameters.FLASH_MODE_TORCH:Camera.Parameters.FLASH_MODE_OFF);
    camera.setParameters(mParameters);
});

3.切换镜头

好吧,哥露脸了...

切换镜头.gif

//切换镜头
mIdIvSwitch.setOnClickListener(v -> {
    if (!isBack) {
        mIdIvSwitch.setImageTintList(ColorStateList.valueOf(0xffEFB90F));
    } else {
        mIdIvSwitch.setImageTintList(ColorStateList.valueOf(0xfffffffF));
    }
    changeCamera(isBack ? BACK : FRONT);
    isBack = !isBack;
});

private void changeCamera(int type) {
    camera.stopPreview();
    camera.release();
    camera = openCamera(type);
    try {
        camera.setPreviewDisplay(mHolder);
        camera.setDisplayOrientation(90);//并将展示方向旋转90度--水平-->竖直
    } catch (IOException e) {
        e.printStackTrace();
    }
    camera.startPreview();
}

private Camera openCamera(int type) {
    int frontIndex = -1;
    int backIndex = -1;
    int cameraCount = Camera.getNumberOfCameras();
    Camera.CameraInfo info = new Camera.CameraInfo();
    for (int cameraIndex = 0; cameraIndex < cameraCount; cameraIndex++) {
        Camera.getCameraInfo(cameraIndex, info);
        if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
            frontIndex = cameraIndex;
        } else if (info.facing == Camera.CameraInfo.CAMERA_FACING_BACK) {
            backIndex = cameraIndex;
        }
    }
    if (type == FRONT && frontIndex != -1) {
        return Camera.open(frontIndex);
    } else if (type == BACK && backIndex != -1) {
        return Camera.open(backIndex);
    }
    return null;
}

四、视频数据的收集

Android 中Google支持的 PreviewCallback.onPreviewFrame的YUV常用格式有两种: 一个是NV21,一个是YV12。Android一般默认使用YCbCr_420_SP的格式(NV21)。

1.实现界面效果

拍照和录像的切换,视频下:变红(偶数次点击)时开始录像,变蓝(奇数次点击)停止

未命名项目.gif

private boolean isPhoto = true;//是否是拍照
private boolean isRecoding;//是否在录像
private int clickRecordCount = 0;//录屏时的点击录屏次数

//切换到录制
mIdTvVideo.setOnClickListener(v -> {
    mIdTvVideo.setTextColor(0xffEFB90F);
    mIdTvPic.setTextColor(0xfffffffF);
    mIdIvSnap.setImageTintList(ColorStateList.valueOf(0xff0FC2EF));
    isPhoto = false;
});

//切换到拍照
mIdTvPic.setOnClickListener(v -> {
    mIdTvVideo.setTextColor(0xfffffffF);
    mIdIvSnap.setImageTintList(ColorStateList.valueOf(0x88ffffff));
    mIdTvPic.setTextColor(0xffEFB90F);
    isPhoto = true;
});

//开始按钮
mIdIvSnap.setOnClickListener(v -> {
    if (isPhoto) {
        takePhoto();//照相
    } else {
        if (clickRecordCount % 2 == 0) {
            recodeVideo();//录制
        } else {
            stopRecodeVideo();//停止录制
        }
    }
    clickRecordCount++;
});

/**
 * 录像
 */
private void recodeVideo() {
    isRecoding = true;
    mIdIvSnap.setImageTintList(ColorStateList.valueOf(0xffff0000));
    camera.startPreview();
}
/**
 * 停止录像
 */
private void stopRecodeVideo() {
    isRecoding = false;
    mIdIvSnap.setImageTintList(ColorStateList.valueOf(0xff0FC2EF));
}

//视频录像
camera.setPreviewCallback((data, camera) -> {
            if (isRecoding) {
                L.d("onPreviewFrame--" + Thread.currentThread().getName() + L.l());
                //TODO 收集数据
            }
        }
);

2.关于数据的尺寸

拍张照都2M多,录像还得了?随便设了两个尺寸没效果... Camera支持的尺寸是固定的哪几种...

mParameters.setPictureSize(720, 480);//设置图片尺寸
mParameters.setPreviewSize(720, 480);//设置预览尺寸

//查看支持的尺寸
List<Camera.Size> pictureSizes = camera.getParameters().getSupportedPictureSizes();
List<Camera.Size> previewSizes = camera.getParameters().getSupportedPreviewSizes();
for (int i = 0; i < pictureSizes.size(); i++) {
    Camera.Size pSize = pictureSizes.get(i);
    L.d("PictureSize.width = " + pSize.width + "--------PictureSize.height = " + pSize.height);
}
for (int i = 0; i < previewSizes.size(); i++) {
    Camera.Size pSize = previewSizes.get(i);
    L.d("previewSize.width = " + pSize.width + "-------previewSize.height = " + pSize.height);
}


PictureSize.width = 5184--------PictureSize.height = 3880
PictureSize.width = 4608--------PictureSize.height = 3456
PictureSize.width = 4608--------PictureSize.height = 2592
PictureSize.width = 4608--------PictureSize.height = 2304
PictureSize.width = 4608--------PictureSize.height = 2176
PictureSize.width = 4608--------PictureSize.height = 2126
PictureSize.width = 4160--------PictureSize.height = 3120
PictureSize.width = 4160--------PictureSize.height = 2340
PictureSize.width = 4000--------PictureSize.height = 3000
PictureSize.width = 3840--------PictureSize.height = 2160
PictureSize.width = 3264--------PictureSize.height = 2448
PictureSize.width = 3264--------PictureSize.height = 1632
PictureSize.width = 3264--------PictureSize.height = 1552
PictureSize.width = 3264--------PictureSize.height = 1504
PictureSize.width = 3200--------PictureSize.height = 2400
PictureSize.width = 2592--------PictureSize.height = 1944
PictureSize.width = 2592--------PictureSize.height = 1940
PictureSize.width = 2592--------PictureSize.height = 1296
PictureSize.width = 2592--------PictureSize.height = 1232
PictureSize.width = 2592--------PictureSize.height = 1458
PictureSize.width = 2560--------PictureSize.height = 1920
PictureSize.width = 2688--------PictureSize.height = 1512
PictureSize.width = 2304--------PictureSize.height = 1728
PictureSize.width = 2304--------PictureSize.height = 1296
PictureSize.width = 2048--------PictureSize.height = 1536
PictureSize.width = 1920--------PictureSize.height = 1080
PictureSize.width = 1840--------PictureSize.height = 1380
PictureSize.width = 1600--------PictureSize.height = 1200
PictureSize.width = 1600--------PictureSize.height = 900
PictureSize.width = 1440--------PictureSize.height = 1080
PictureSize.width = 1280--------PictureSize.height = 960
PictureSize.width = 1280--------PictureSize.height = 768
PictureSize.width = 1280--------PictureSize.height = 720
PictureSize.width = 1024--------PictureSize.height = 768
PictureSize.width = 800--------PictureSize.height = 600
PictureSize.width = 800--------PictureSize.height = 480
PictureSize.width = 720--------PictureSize.height = 480
PictureSize.width = 640--------PictureSize.height = 480
PictureSize.width = 352--------PictureSize.height = 288
PictureSize.width = 320--------PictureSize.height = 240
PictureSize.width = 176--------PictureSize.height = 144


previewSize.width = 2160-------previewSize.height = 1080
previewSize.width = 1920-------previewSize.height = 1080
previewSize.width = 1600-------previewSize.height = 900
previewSize.width = 1520-------previewSize.height = 720
previewSize.width = 1440-------previewSize.height = 1080
previewSize.width = 1280-------previewSize.height = 960
previewSize.width = 1280-------previewSize.height = 720
previewSize.width = 960-------previewSize.height = 720
previewSize.width = 720-------previewSize.height = 480
previewSize.width = 640-------previewSize.height = 480
previewSize.width = 352-------previewSize.height = 288
previewSize.width = 320-------previewSize.height = 240
previewSize.width = 176-------previewSize.height = 144

3.视频数据的收集

获取的数据暂时还无法解析,先留着吧

录制.png

//视频录像
camera.setPreviewCallback((data, camera) -> {
            if (isRecoding) {
                collectData(data);
            }
        }
);

 /**
  * 收集数据
  *
  * @param data
  */
 private void collectData(byte[] data) {
     try {
         mFosVideo.write(data);
     } catch (IOException e) {
         e.printStackTrace();
     }
 }
 
/**
 * 录像时生成流mFosVideo
 */
private void recodeVideo() {
    isRecoding = true;
    File videoFile = FileHelper.get().createFile("video/hello");
    try {
        mFosVideo = new FileOutputStream(videoFile);
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    }
    
    mIdIvSnap.setImageTintList(ColorStateList.valueOf(0xffff0000));
    camera.startPreview();
}

/**
 * 停止录像关闭流
 */
private void stopRecodeVideo() {
    isRecoding = false;
    mIdIvSnap.setImageTintList(ColorStateList.valueOf(0xff0FC2EF));
    try {
        mFosVideo.flush();
        mFosVideo.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

五、视频数据的收集:Camera+MediaRecorder

MediaRecorder不止能录音频,结合Camera还能录视频

视频录制.png


1.支持的视频尺寸也是有限制的
videoSize.width = 2160-------videoSize.height = 1080
videoSize.width = 1920-------videoSize.height = 1080
videoSize.width = 1280-------videoSize.height = 960
videoSize.width = 1440-------videoSize.height = 720
videoSize.width = 1280-------videoSize.height = 720
videoSize.width = 864-------videoSize.height = 480
videoSize.width = 800-------videoSize.height = 480
videoSize.width = 720-------videoSize.height = 480
videoSize.width = 640-------videoSize.height = 480
videoSize.width = 352-------videoSize.height = 288
videoSize.width = 320-------videoSize.height = 240
videoSize.width = 176-------videoSize.height = 144

视频录制辅助类
/**
 * 作者:张风捷特烈<br/>
 * 时间:2019/1/8 0008:16:29<br/>
 * 邮箱:1981462002@qq.com<br/>
 * 说明:视频录制辅助类
 */
public class VideoRecorderUtils {
    private MediaRecorder mediaRecorder;
    private Camera camera;
    private SurfaceHolder.Callback callback;
    private SurfaceView surfaceView;
    private int height, width;
    public static Point WH_2160X1080 = new Point(2160, 1080);
    public static Point WH_1920X1080 = new Point(1920, 1080);
    public static Point WH_1280X960 = new Point(1280, 960);
    public static Point WH_1440X720 = new Point(1440, 720);
    public static Point WH_1280X720 = new Point(1280, 720);
    public static Point WH_864X480 = new Point(864, 480);
    public static Point WH_800X480 = new Point(800, 480);
    public static Point WH_720X480 = new Point(720, 480);
    public static Point WH_640X480 = new Point(640, 480);
    public static Point WH_352X288 = new Point(352, 288);
    public static Point WH_320X240 = new Point(320, 240);
    public static Point WH_176X144 = new Point(176, 144);
    public void create(SurfaceView surfaceView,Point point) {
        this.surfaceView = surfaceView;
        surfaceView.setKeepScreenOn(true);
        callback = new SurfaceHolder.Callback() {
            public void surfaceCreated(SurfaceHolder holder) {
                camera = Camera.open();
                width = point.x;
                height = point.y;
                mediaRecorder = new MediaRecorder();
            }
            public void surfaceChanged(SurfaceHolder holder, int format,
                                       int width, int height) {
                doChange(holder);
            }
            @Override
            public void surfaceDestroyed(SurfaceHolder holder) {
                if (camera != null) {
                    camera.release();
                    camera = null;
                }
            }
        };
        surfaceView.getHolder().addCallback(callback);
    }
    private void doChange(SurfaceHolder holder) {
        try {
            camera.setPreviewDisplay(holder);
            camera.setDisplayOrientation(90);
            camera.startPreview();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    public void stopRecord() {
        mediaRecorder.release();
        camera.release();
        mediaRecorder = null;
        camera = Camera.open();
        mediaRecorder = new MediaRecorder();
        doChange(surfaceView.getHolder());
    }
    public void stop() {
        if (mediaRecorder != null && camera != null) {
            mediaRecorder.release();
            camera.release();
        }
    }
    public void destroy() {
        if (mediaRecorder != null && camera != null) {
            mediaRecorder.release();
            camera.release();
            mediaRecorder = null;
            camera = null;
        }
    }
    /**
     * @param path 保存的路径
     * @param name 录像视频名称(不包含后缀)
     */
    public void startRecord(String path, String name) {
        camera.unlock();
        mediaRecorder.setCamera(camera);
        mediaRecorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER);
        mediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);
        mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
        mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT);
        mediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
        mediaRecorder.setVideoEncodingBitRate(700 * 1024);
        mediaRecorder.setVideoSize(width, height);
        mediaRecorder.setVideoFrameRate(24);
        File file = new File(path);
        if (!file.exists()) {
            file.mkdirs();
        }
        mediaRecorder.setOutputFile(path + File.separator + name + ".mp4");
        File file1 = new File(path + File.separator + name + ".mp4");
        if (file1.exists()) {
            file1.delete();
        }
        mediaRecorder.setPreviewDisplay(surfaceView.getHolder().getSurface()
        mediaRecorder.setOrientationHint(0);
        try {
            mediaRecorder.prepare();
            mediaRecorder.start();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

3.辅助类的使用

避免看起来杂乱,新建了一个Activity类 使用的核心方法:

private boolean isRecording;

mVideoRecorderUtils = new VideoRecorderUtils();
mVideoRecorderUtils.create(mIdSvVideo, VideoRecorderUtils.WH_720X480);
path = Environment.getExternalStorageDirectory().getAbsolutePath();
mIdIvSnap.setOnClickListener(view -> {
    if (!isRecording) {
        mVideoRecorderUtils.startRecord(path, "Video");
    } else {
        mVideoRecorderUtils.stopRecord();
    }
    isRecording = !isRecording;
});

OK,就这样,还有写Camera的特效,等以后把图片知识弄好,再说吧

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券