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+
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();
再開啟預覽, 否則介面就不動了
這裡測試拍照的檔名寫死了(避免拍太多測試照片...),你可以用當前時間當檔名
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先居中隱藏
5.1:延遲按鈕的點選效果
選中時拍照延遲3s(此處簡單地寫死,當然你也可以暴漏設定方法)
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個等級,放到最大之後回到開始大小
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.打燈
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.切換鏡頭
好吧,哥露臉了...
//切換鏡頭
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.實現介面效果
拍照和錄影的切換,視訊下:變紅(偶數次點選)時開始錄影,變藍(奇數次點選)停止
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.視訊資料的收集
獲取的資料暫時還無法解析,先留著吧
//視訊錄影
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還能錄視訊
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/>
* 郵箱:[email protected]<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的特效,等以後把圖片知識弄好,再說吧
後記:捷文規範
1.本文成長記錄及勘誤表
專案原始碼 | 日期 | 備註 |
---|---|---|
V0.1-github | 2018-1-8 | Android多媒體之Camera的相關操作](www.jianshu.com/p/6db677f9d…) |
2.更多關於我
筆名 | 微信 | 愛好 | |
---|---|---|---|
張風捷特烈 | 1981462002 | zdl1994328 | 語言 |
我的github | 我的簡書 | 我的掘金 | 個人網站 |
3.宣告
1----本文由張風捷特烈原創,轉載請註明
2----歡迎廣大程式設計愛好者共同交流
3----個人能力有限,如有不正之處歡迎大家批評指證,必定虛心改正
4----看到這裡,我在此感謝你的喜歡與支援