Android從系統(tǒng)調(diào)用相冊(cè)O(shè)kHttp上傳到服務(wù)器(activity數(shù)據(jù)傳遞)
想寫技術(shù)博客已經(jīng)好久,今天終于下定決心堅(jiān)持一周一個(gè)小博客,我是一名安卓工程師,工作剛剛一個(gè)月,水平有限,此文章是結(jié)合郭霖大神的《第一行代碼》以及自己參考網(wǎng)絡(luò)大神上的一些參考代碼結(jié)合起來的,在自己項(xiàng)目中進(jìn)行調(diào)用,雖然有些雜亂,但是也算是基本的實(shí)現(xiàn)了功能。
下面先來展示一下功能截圖
功能流程相信各位也都能看懂,在第一個(gè)注冊(cè)信息完成后點(diǎn)擊下一步后繼續(xù)完成第二個(gè)頁面的注冊(cè)信息,然后將注冊(cè)信息上傳到服務(wù)器。
我也是第一次工作第一次自己進(jìn)行獨(dú)立開發(fā),所以可能會(huì)走不少?gòu)澛?,但是總算功能能夠?qū)崿F(xiàn)了。
下面來給大家展示一下代碼,我會(huì)著重將一些重要的代碼展示出來
牽扯到的知識(shí)點(diǎn)
- Android調(diào)用系統(tǒng)相冊(cè)及相機(jī)(隱性intent)
- ContentProvider
- okHttp
- popupWindow
- Bundle
代碼講解
為了能夠更好的點(diǎn)擊btn彈出相機(jī)或者相冊(cè),我自定義了一個(gè)popupWindow,這個(gè)如果你不想寫,可以百度得到。
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_margin="10dp"
android:orientation="vertical"
android:paddingBottom="10dp">
<Button
android:id="@+id/btn_pop_album"
android:layout_width="match_parent"
android:layout_height="45dp"
android:background="#ffff"
android:text="本地相冊(cè)"
android:textSize="18sp" />
<Button
android:id="@+id/btn_pop_camera"
android:layout_width="match_parent"
android:layout_height="45dp"
android:background="#ffff"
android:text="相機(jī)拍攝"
android:textSize="18sp" />
<Button
android:id="@+id/btn_pop_cancel"
android:layout_width="match_parent"
android:layout_height="45dp"
android:layout_marginTop="10dp"
android:background="#ffff"
android:text="取消"
android:textSize="18sp" />
</LinearLayout>
</RelativeLayout>
>
點(diǎn)擊popupWindow調(diào)用相機(jī)或相冊(cè)
View popView = View.inflate(this, R.layout.popup_choose_pic, null);
Button btnPopAlbum = (Button) popView.findViewById(R.id.btn_pop_album);
Button btnPopCamera = (Button) popView.findViewById(R.id.btn_pop_camera);
Button btnPopCancel = (Button) popView.findViewById(R.id.btn_pop_cancel);
//獲取屏幕寬高
int widthPixels = getResources().getDisplayMetrics().widthPixels;
int heightPixels = getResources().getDisplayMetrics().heightPixels * 1 / 3;
final PopupWindow popupWindow = new PopupWindow(popView, widthPixels, heightPixels);
popupWindow.setAnimationStyle(R.style.anim_popup_dir);
popupWindow.setFocusable(true);
//點(diǎn)擊popup外部消失
popupWindow.setOutsideTouchable(true);
//消失時(shí)屏幕變?yōu)榘胪该? popupWindow.setOnDismissListener(new PopupWindow.OnDismissListener() {
@Override
public void onDismiss() {
WindowManager.LayoutParams params = getWindow().getAttributes();
params.alpha = 1.0f;
getWindow().setAttributes(params);
}
});
//出現(xiàn)時(shí)屏幕變?yōu)橥该? WindowManager.LayoutParams params = getWindow().getAttributes();
params.alpha = 0.5f;
getWindow().setAttributes(params);
popupWindow.showAtLocation(popView, Gravity.BOTTOM, 0, 50);
btnPopAlbum.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
popupWindow.dismiss();
//調(diào)用相機(jī)
invokeAlbum();
}
});
btnPopCamera.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
popupWindow.dismiss();
//打開相機(jī)
openCarema();
}
});
btnPopCancel.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
popupWindow.dismiss();
}
});
可以直接拷貝到xml里面看預(yù)覽圖
下面我們就著重講解點(diǎn)擊頭像這個(gè)功能邏輯
其實(shí)對(duì)于調(diào)用相冊(cè)以及相機(jī)這幾個(gè)功能,許多的API我也不是特別了解,也不能詳細(xì)的跟大家解釋一下,在這里我將我拷貝這些代碼的時(shí)候遇到的坑跟大家說下-------功能摘自《第一行代碼第二版》相機(jī)相冊(cè)那個(gè)章節(jié)。
調(diào)用相冊(cè)
private void invokeAlbum() {
//動(dòng)態(tài)申請(qǐng)危險(xiǎn)時(shí)權(quán)限,運(yùn)行時(shí)權(quán)限
if (ContextCompat.checkSelfPermission(HgCompleteInfoActivity.this, android.Manifest.permission.WRITE_EXTERNAL_STORAGE) !=
PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(HgCompleteInfoActivity.this, new String[]{android.Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1);
} else {
openAlbum();
}
}
private void openAlbum() {
Intent intent = new Intent("android.intent.action.GET_CONTENT");
intent.setType("image/*");
startActivityForResult(intent, CHOOSE_PHOTO);
}
在這里先進(jìn)行了動(dòng)態(tài)時(shí)權(quán)限,這是6.0之后支持的,具體的可以看郭霖的blog,目前著重實(shí)現(xiàn)功能
/**
* 動(dòng)態(tài)獲取到的權(quán)限后的重寫
*
* @param requestCode
* @param permissions
* @param grantResults
*/
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
switch (requestCode) {
case 1:
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
openAlbum();
} else {
Toast.makeText(HgCompleteInfoActivity.this, "you denied the permission", Toast.LENGTH_SHORT).show();
}
break;
default:
break;
}
}
這里是動(dòng)態(tài)獲取到權(quán)限后的重寫方法,如果不夠理解,可以想一下
app安裝時(shí)軟件介紹會(huì)解釋獲取哪些權(quán)限,那些權(quán)限是屬于manifests里注冊(cè)的,而動(dòng)態(tài)獲取權(quán)限時(shí),當(dāng)你第一次打開app時(shí),比如需要獲取你的地理位置,你點(diǎn)了拒絕,會(huì)彈出上面方法里面case1-->else邏輯。
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode) {
case CHOOSE_PHOTO:
if (resultCode == RESULT_OK) {
//判斷手機(jī)系統(tǒng)版本號(hào)
if (Build.VERSION.SDK_INT >= 19) {
handleImageOnKitKat(data);
} else {
handleImageBeforeKitKat(data);
}
}
break;
}
}
private void handleImageBeforeKitKat(Intent data) {
Uri uri = data.getData();
String imagePath = getImagePath(uri, null);
displayImage(imagePath);
}
private void handleImageOnKitKat(Intent data) {
String imagePath = null;
Uri uri = data.getData();
//Log.i("uri", uri + "");
if (DocumentsContract.isDocumentUri(HgCompleteInfoActivity.this, uri)) {
//如果是document類型的uri,則通過document id 處理
String docId = DocumentsContract.getDocumentId(uri);
Log.i("type of document", docId);
if ("com.android.providers.media.documents".equals(uri.getAuthority())) {
String id = docId.split(":")[1];//解析出數(shù)字格式的id
Log.i("type of document id", id);
String selection = MediaStore.Images.Media._ID + "=" + id;
Log.i("selection", selection);
imagePath = getImagePath(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, selection);
} else if ("com.android.providers.downloads.documents".equals(uri.getAuthority())) {
Uri contentUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"), Long.valueOf(docId));
imagePath = getImagePath(contentUri, null);
}
} else if ("content".equalsIgnoreCase(uri.getScheme())) {
//如果是content類型的uri,就用普通方式處理
imagePath = getImagePath(uri, null);
} else if ("file".equalsIgnoreCase(uri.getScheme())) {
//如果是file類型的Uri,直接獲取圖片路徑
imagePath = uri.getPath();
}
displayImage(imagePath);
}
private String getImagePath(Uri uri, String selection) {
String path = null;
//通過uri和selection來獲取真實(shí)的圖片路徑
Cursor cursor = getContentResolver().query(uri, null, selection, null, null);
if (cursor != null) {
if (cursor.moveToNext()) {
path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA));
}
cursor.close();
}
return path;
}
private void displayImage(String imagePath) {
if (imagePath != null) {
bitmap = BitmapFactory.decodeFile(imagePath);
headFile = saveMyBitmap(bitmap, "head");
//保存file到sp
saveFile(headFile.getName());
Glide.with(this).asBitmap().load(headFile).thumbnail(0.1f).into(civHead);
} else {
Toast.makeText(HgCompleteInfoActivity.this, "failed to get image ", Toast.LENGTH_SHORT).show();
}
}
//將bitmap轉(zhuǎn)化為png格式
public File saveMyBitmap(Bitmap mBitmap, String prefix) {
File storageDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM);
File file = null;
try {
file = File.createTempFile(
prefix, /* prefix */
".jpg", /* suffix */
storageDir /* directory */
);
FileOutputStream fos = new FileOutputStream(file);
mBitmap.compress(Bitmap.CompressFormat.JPEG, 10, fos);
fos.flush();
fos.close();
} catch (Exception e) {
e.printStackTrace();
}
return file;
}
/**
* 保存file到sp
*
* @param fileName
*/
private void saveFile(String fileName) {
SharedPreferences sp = getSharedPreferences("image", MODE_PRIVATE);
SharedPreferences.Editor edit = sp.edit();
edit.putString(HEAD_KEY, fileName);
//提交edit
edit.commit();
Log.i(TAG, "saveFile: 保存成功" + sp.getString("head", null));
}
這是阻擾我時(shí)間最多的邏輯部分,首先解釋一下這些代碼的邏輯
1.點(diǎn)擊獲取到的相冊(cè)顯示到我上圖中的CircleImageView(記得一定要壓縮,不然拍完照返回照片到ImageView會(huì)特別的慢,在這里我做的也不夠好,所以不展開講)
2.然后自己寫了兩個(gè)方法,savaFile和savaMyBitmap
3.在savaMyBitmap方法中,首先獲取了系統(tǒng)相冊(cè)的地址,然后每次我拍下照片或者選擇照片時(shí),都進(jìn)行簡(jiǎn)單的壓縮,因?yàn)閳D片要上傳到服務(wù)器進(jìn)行審核管理,做成微信頭像之類的超級(jí)壓縮方法有損畫質(zhì),所以我只是進(jìn)行了簡(jiǎn)單的壓縮上傳最后寫成一個(gè)文件。(在第二個(gè)頁面的時(shí)候可能會(huì)有幾個(gè)問題,三個(gè)ImageView即是三個(gè)bitmap,三個(gè)File文件。如何判斷及正確的顯示,我們可以定義成全局變量,因?yàn)槎际屈c(diǎn)擊事件,所以bitmap會(huì)在點(diǎn)擊后修改,然后保存下來。
4.在savaFile中,我靈機(jī)一動(dòng)(哈哈),想到了SharedPreferences這個(gè)神奇功能,我們沒有必要用sp去存file,因?yàn)闆]有那個(gè)方法,我們可以將名字存起來啊,edit.putString()中的HEAD_KEY就是我自己定的方法。你可以把sp當(dāng)成一個(gè)map,key是不會(huì)變的,但是value會(huì)變,所以理論上不管我們點(diǎn)多少次都會(huì)改變。
5.這樣文件就保存在本地了(可是不知道為啥我找不到,但是上傳到服務(wù)器里可以顯示)。
調(diào)用相機(jī)
private void openCarema() {
//創(chuàng)建File對(duì)象,用于存儲(chǔ)拍照后的照片
File outputImage = new File(getExternalCacheDir(), "output_image.jpg");
if (outputImage.exists()) {
outputImage.delete();
}
try {
outputImage.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
if (Build.VERSION.SDK_INT >= 24) {
imageUri = FileProvider.getUriForFile(HgCompleteInfoActivity.this, "com.qryl.qrylyh.activity.login.complete.fileprovider", outputImage);
} else {
imageUri = Uri.fromFile(outputImage);
}
//啟動(dòng)相機(jī)程序
Intent intent = new Intent("android.media.action.IMAGE_CAPTURE");
intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
startActivityForResult(intent, TAKE_PHOTO);
}
這里不多贅述,調(diào)用Uri的邏輯功能代碼都是郭霖的,我是cv戰(zhàn)士。
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode) {
case TAKE_PHOTO:
if (resultCode == RESULT_OK) {
//將拍攝的圖片顯示出來
try {
bitmap = BitmapFactory.decodeStream(getContentResolver().openInputStream(imageUri));
headFile = saveMyBitmap(bitmap, "head");
//保存file到sp
saveFile(headFile.getName());
Glide.with(this).asBitmap().load(headFile).thumbnail(0.1f).into(civHead);
Log.i("wechat", "壓縮后圖片的大小" + ("字節(jié)碼:" + " 寬度為:" + bitmap.getWidth() + " 高度為:" + bitmap.getHeight()));
Log.i(TAG, "File:" + headFile.getName() + " 路徑:" + headFile.getAbsolutePath());
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
break;
}
}
和上面的方法基本都一樣。
使用Bundle以及SharedPreferences的理由
因?yàn)槲乙训谝豁摰臄?shù)據(jù)全部保存下來,全部在第二個(gè)頁面點(diǎn)擊注冊(cè)時(shí)一起上傳,這樣的話只是請(qǐng)求一次服務(wù)器,所以我想到了sp方法將文件名字存儲(chǔ)起來,在第二個(gè)頁面直接調(diào)用File方法把文件取出來即可
而Bundle只是為了區(qū)別分開,本來我是打開實(shí)現(xiàn)serializable的一個(gè)Map工具類集合進(jìn)行數(shù)據(jù)的傳遞,技術(shù)有限,老是異常,就想到了用File存儲(chǔ)的方法
其他注冊(cè)信息Bundle方法
/**
* 傳遞數(shù)據(jù)到下個(gè)頁面
*/
private void putExtra() {
Intent intent = new Intent(HgCompleteInfoActivity.this, CompletePicActivity.class);
//傳遞數(shù)據(jù)
Bundle bundle = new Bundle();
//bundle.putByteArray("head", bytes);
bundle.putString("name", ageDialogText);
bundle.putString("identity", identityDialogText);
bundle.putString("gender", genderDialogText);
bundle.putString("age", ageDialogText);
bundle.putString("workexperience", workExperienceDialogText);
bundle.putString("begoodat", beGoodAtWorkDialogText);
bundle.putString("localservice", null);
intent.putExtras(bundle);
startActivity(intent);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_complete_pic);
initView();
Bundle bundle = getIntent().getExtras();
String name = (String) bundle.get("name");
String indentity = (String) bundle.get("identity");
String gender = (String) bundle.get("gender");
String age = (String) bundle.get("age");
String workexperience = (String) bundle.get("workexperience");
String begoodat = (String) bundle.get("begoodat");
String localservice = (String) bundle.get("localservice");
//dataMap.put("head", head.toString());
dataMap.put("name", name);
dataMap.put("indentity", indentity);
dataMap.put("gender", gender);
dataMap.put("age", age);
dataMap.put("workexperience", workexperience);
dataMap.put("begoodat", begoodat);
dataMap.put("localservice", localservice);
}
這沒啥好說的,第二個(gè)頁面直接取出來,我為了以后后期方便維護(hù),定義了一個(gè)HashMap,如果沒有此需求可以直接取出來當(dāng)成全局變量進(jìn)行調(diào)用即可。
調(diào)用圖片上傳的拓展
不知道大家是不是還記得第二個(gè)頁面有三個(gè)上傳照片的圖片,在剛才我也提過,如何在正確的相框中實(shí)現(xiàn)正確的圖片,是一個(gè)不難但是挺啰嗦的事情
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode) {
case TAKE_PHOTO:
if (resultCode == RESULT_OK) {
//將拍攝的圖片顯示出來
try {
bitmap = BitmapFactory.decodeStream(getContentResolver().openInputStream(imageUri));
if (choosed_image == R.id.sfz_image) {//身份證
//保存file到sp
sfzFile = saveMyBitmap(bitmap, "sfz");
saveFile(SFZ_KEY, sfzFile.getName());
Glide.with(this).asBitmap().load(sfzFile).thumbnail(0.1f).into(sfzImage);
} else if (choosed_image == R.id.jkz_image) {//健康證
jkzFile = saveMyBitmap(bitmap, "jkz");
//保存file到sp
saveFile(JKZ_KEY, jkzFile.getName());
Glide.with(this).asBitmap().load(jkzFile).thumbnail(0.1f).into(jkzImage);
} else if (choosed_image == R.id.zgz_image) {//從業(yè)資格證
zgzFile = saveMyBitmap(bitmap, "zgz");
//保存file到sp
saveFile(ZGZ_KEY, zgzFile.getName());
Glide.with(this).asBitmap().load(zgzFile).thumbnail(0.1f).into(zgzImage);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
break;
case CHOOSE_PHOTO:
if (resultCode == RESULT_OK) {
//判斷手機(jī)系統(tǒng)版本號(hào)
if (Build.VERSION.SDK_INT >= 19) {
handleImageOnKitKat(data);
} else {
handleImageBeforeKitKat(data);
}
}
break;
}
}
這些代碼看著很繁瑣,其實(shí)東西沒多少,只是比上面的邏輯多了個(gè)if,在這里我定義了一個(gè)全局的int變量,將三個(gè)相冊(cè)區(qū)別開來,R.id.sfz_image就是一個(gè)int值不是嗎。這樣看起來更加具像化,最好不用數(shù)字區(qū)分開。savaFile一樣。
這樣基本的上傳照片基本完成,我們可以點(diǎn)擊注冊(cè)把數(shù)據(jù)整合起來直接上傳到服務(wù)器了,在這里我用了okHttp框架
在第一行代碼里,okHttp只是簡(jiǎn)單的展示了一下方法,大家可以百度一下各種okHttp方法,因?yàn)槲覀冞@個(gè)項(xiàng)目,定的統(tǒng)一是post請(qǐng)求,而且因?yàn)閿?shù)據(jù)類型可能不同(File,String,int),所以我經(jīng)過半天的不懈努力,看到了一個(gè)nb的API:addFormDataPart()
/**
* 向服務(wù)器發(fā)送請(qǐng)求
*/
private void postData() {
SharedPreferences pref = getSharedPreferences("image", Context.MODE_PRIVATE);
String headImage = pref.getString(HEAD_KEY, null);
String sfzImage = pref.getString(SFZ_KEY, null);
String jkzImage = pref.getString(JKZ_KEY, null);
String zgzName = pref.getString(ZGZ_KEY, null);
//Log.i(TAG, "postData: 頭像圖片名字" + imageName);
OkHttpClient client = new OkHttpClient();
MultipartBody.Builder builder = new MultipartBody.Builder().setType(MultipartBody.FORM);
File storageDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM);
File headFile = new File(storageDir, headImage);
File sfzFile = new File(storageDir, sfzImage);
File jkzFile = new File(storageDir, jkzImage);
File zgzFile = new File(storageDir, zgzName);
if (headFile != null) {
// MediaType.parse() 里面是上傳的文件類型。
RequestBody body = RequestBody.create(MediaType.parse("image/*"), headFile);
// 參數(shù)分別為, 請(qǐng)求key ,文件名稱 , RequestBody
builder.addFormDataPart("txImg", headFile.getName(), body);
}
if (sfzFile != null) {
// MediaType.parse() 里面是上傳的文件類型。
RequestBody body = RequestBody.create(MediaType.parse("image/*"), sfzFile);
// 參數(shù)分別為, 請(qǐng)求key ,文件名稱 , RequestBody
builder.addFormDataPart("sfzImg", sfzFile.getName(), body);
}
if (jkzFile != null) {
// MediaType.parse() 里面是上傳的文件類型。
RequestBody body = RequestBody.create(MediaType.parse("image/*"), jkzFile);
// 參數(shù)分別為, 請(qǐng)求key ,文件名稱 , RequestBody
builder.addFormDataPart("jkzImg", jkzFile.getName(), body);
}
if (zgzFile != null) {
// MediaType.parse() 里面是上傳的文件類型。
RequestBody body = RequestBody.create(MediaType.parse("image/*"), zgzFile);
// 參數(shù)分別為, 請(qǐng)求key ,文件名稱 , RequestBody
builder.addFormDataPart("zgzImg", zgzFile.getName(), body);
}
builder.addFormDataPart("loginId", "1");
//builder.add("sfzImg", "1");
//builder.add("zgzImg", "1");
//builder.add("jkzImg", "1");
builder.addFormDataPart("realName", "sdfdf");
builder.addFormDataPart("gender", "0");
builder.addFormDataPart("age", "10");
builder.addFormDataPart("workYears", "10");
builder.addFormDataPart("introduce", "sdfsdfsf");
MultipartBody requestBody = builder.build();
Request request = new Request.Builder().url("服務(wù)器地址").post(requestBody).build();
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
Log.i(TAG, "onFailure: 失敗");
e.printStackTrace();
}
@Override
public void onResponse(Call call, Response response) throws IOException {
Log.i(TAG, "onResponse: 成功 " + response.body().string());
}
});
}
可以忽略那些沒用的提取數(shù)據(jù)代碼
寫到后面也比較懶了,因?yàn)槭侵苣?,晚上還有極限挑戰(zhàn)哈哈,所以要抓緊
在這client的回調(diào)我覺得用過okHttp的人都能看懂,重要的newCall方法里面的參數(shù),在這里我是用MultipartBody類,大家可以看上面那一段代碼的第12行開始
MultipartBody.Builder builder = new MultipartBody.Builder().setType(MultipartBody.FORM);
這段代碼是定義的這是上傳的表單類型
RequestBody body = RequestBody.create(MediaType.parse("image/*"), headFile);
// 參數(shù)分別為, 請(qǐng)求key ,文件名稱 , RequestBody
builder.addFormDataPart("txImg", headFile.getName(), body);
image/*是因?yàn)?,我的圖片文件是.jpg格式。
在這里,我打了詳細(xì)的注釋,幫助大家也幫助自己理解。這樣,我們?cè)趦蓚€(gè)頁面進(jìn)行實(shí)現(xiàn)注冊(cè)頁面,用了一次請(qǐng)求就將數(shù)據(jù)上傳到了服務(wù)器。其實(shí)上傳的方式有很多種,用流的方式上傳圖片也是可以的。第一次博客,因?yàn)樽约罕旧硎且粋€(gè)剛工作一個(gè)月的菜鳥,自己一個(gè)人搞安卓端的開發(fā),也算是獨(dú)立開發(fā)了。所以要多走不少?gòu)澛罚瑢?shí)現(xiàn)一個(gè)功能可能會(huì)麻煩不少。希望以后好好做一個(gè)cv戰(zhàn)士。