Android中的Camera可以用来进行自定义相机、取景框实时预览、拍照等操作。在5.0中,这个类不推荐使用了,新出了一个Camera2,那个东西没怎么研究过,反正一时半会用不到。本篇讲解的是如果用这个对象进行拍照,最后在提及下如何进行后台的静默拍照。

API翻译:http://bbs.51cto.com/thread-1063856-1.html(挺简单易懂的)

一、CameraManager

这个类是我自己封装的,不是API提供的。我在这里封装了打开相机,获取相机ID,保存拍照图片的操作。

1.1 初始化

初始化时需要传入一个camera对象,还有一个SurfaceHolder对象。传入Camera对象的目的是用于之后的拍照等操作,surfaceHloder是用于操作取景框预览图片的。

    private Camera mCamera;
private SurfaceHolder mHolder; public CameraManager(Camera camera, SurfaceHolder holder) {
mCamera = camera;
mHolder = holder; } public Camera getCamera() {
return mCamera;
}

1.2 打开相机

这部分的步骤是,先找到Camera的id,然后通过Camera.open(id)打开这个id的摄像头(获取id的方法下面会说到),并且返回一个Camera对象,用于以后的操作。

Camera android.hardware.Camera.open(int cameraId)

如果开启失败,那么camera对象就是null,如果开启成功,那么就需要提供实时预览,让用户能看到摄像头中的东西。

重要代码:

mCamera.setPreviewDisplay(mHolder);
// 如果成功开始实时预览
mCamera.startPreview();

代码片段:

    // 将摄像头中的图像展示到holder中
try {
// 这里的myCamera为已经初始化的Camera对象
mCamera.setPreviewDisplay(mHolder);
} catch (IOException e) {
e.printStackTrace();
// 如果出错立刻进行处理,停止预览照片
mCamera.stopPreview();
mCamera.release();
mCamera = null;
}
// 如果成功开始实时预览
mCamera.startPreview();

全部代码:

    /**
* 打开相机
*
* @param camera
* 照相机对象
* @param holder
* 用于实时展示取景框内容的控件
* @param tagInfo
* 摄像头信息,分为前置/后置摄像头 Camera.CameraInfo.CAMERA_FACING_FRONT:前置
* Camera.CameraInfo.CAMERA_FACING_BACK:后置
* @return 是否成功打开某个摄像头
*/
public boolean openCamera(int tagInfo) {
// 尝试开启摄像头
try {
mCamera = Camera.open(getCameraId(tagInfo));
} catch (RuntimeException e) {
e.printStackTrace();
return false;
}
// 开启前置失败
if (mCamera == null) {
return false;
}
// 将摄像头中的图像展示到holder中
try {
// 这里的myCamera为已经初始化的Camera对象
mCamera.setPreviewDisplay(mHolder);
} catch (IOException e) {
e.printStackTrace();
// 如果出错立刻进行处理,停止预览照片
mCamera.stopPreview();
mCamera.release();
mCamera = null;
}
// 如果成功开始实时预览
mCamera.startPreview();
return true;
}

1.3 获取摄像头的id

获取id的步骤是:先得到本机摄像头的数目,开始一个个判断前置/后置摄像头的id

/**
* @param tagInfo
* @return 得到特定camera info的id
*/
private int getCameraId(int tagInfo) {
Camera.CameraInfo cameraInfo = new Camera.CameraInfo();
// 开始遍历摄像头,得到camera info
int cameraId, cameraCount;
for (cameraId = 0, cameraCount = Camera.getNumberOfCameras(); cameraId < cameraCount; cameraId++) {
Camera.getCameraInfo(cameraId, cameraInfo);
if (cameraInfo.facing == tagInfo) {
break;
}
}
return cameraId;
}

获取前置/后置摄像头的id

通过传入不同的参数,就可以得到前置/后置的id了

    /**
* @return 前置摄像头的ID
*/
public int getFrontCameraId() {
return getCameraId(Camera.CameraInfo.CAMERA_FACING_FRONT);
} /**
* @return 后置摄像头的ID
*/
public int getBackCameraId() {
return getCameraId(Camera.CameraInfo.CAMERA_FACING_BACK);
}

1.4 设定拍照成功后照片保存的路径和照片名称

首先要保证拍摄时存放照片的根目录存在,因为有时候用户会自己闲的蛋疼的删除掉你建立好的目录,拍照时你的程序找不到目录就会报错,一报错用户就觉得你的程序差劲,不好用。为了避免这个问题,我们每次保存前都需要判断下图片保存的目录是否存在。

        // 创建并保存图片文件
File mFile = new File(PHOTO_PATH);
if (!mFile.exists()) {
mFile.mkdirs();
}

图片的名字一般是根据当前时间来设定的,如果有其他需要请自行修改。

    /**
* 定义图片保存的路径和图片的名字
*/
public final static String PHOTO_PATH = "mnt/sdcard/CAMERA_DEMO/Camera/"; public static String getPhotoFileName() {
Date date = new Date(System.currentTimeMillis());
SimpleDateFormat dateFormat = new SimpleDateFormat("'LOCK'_yyyyMMdd_HHmmss");
return dateFormat.format(date) + ".jpg";
}

1.5 处理拍照的结果

当拍照成功后会回调一个方法,在这个方法中我们就可以进行相片的处理了。由于android拍照后的照片和真实照片是垂直的,所以需要旋转处理。最后去保存到SD卡。在保存完毕后,请务必对camera进行收尾处理,释放资源。

    /**
* 拍照成功回调
*/
public class PicCallback implements PictureCallback {
private String TAG = getClass().getSimpleName();
private Camera mCamera; public PicCallback(Camera camera) {
// TODO 自动生成的构造函数存根
mCamera = camera;
} /*
* 将拍照得到的字节转为bitmap,然后旋转,接着写入SD卡
* @param data
* @param camera
*/
@Override
public void onPictureTaken(byte[] data, Camera camera) {
// 将得到的照片进行270°旋转,使其竖直
Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
Matrix matrix = new Matrix();
matrix.preRotate(270);
bitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
// 创建并保存图片文件
File mFile = new File(PHOTO_PATH);
if (!mFile.exists()) {
mFile.mkdirs();
}
File pictureFile = new File(PHOTO_PATH, getPhotoFileName());
try {
FileOutputStream fos = new FileOutputStream(pictureFile);
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fos);
bitmap.recycle();
fos.close();
Log.i(TAG, "拍摄成功!");
} catch (Exception error) {
Log.e(TAG, "拍摄失败");
error.printStackTrace();
} finally {
mCamera.stopPreview();
mCamera.release();
mCamera = null;
}
} }

1.6 本类的全部代码

package com.kale.camerademo;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date; import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.hardware.Camera;
import android.hardware.Camera.PictureCallback;
import android.util.Log;
import android.view.SurfaceHolder; public class CameraManager { private Camera mCamera;
private SurfaceHolder mHolder; public CameraManager(Camera camera, SurfaceHolder holder) {
mCamera = camera;
mHolder = holder; } public Camera getCamera() {
return mCamera;
} /**
* 打开相机
*
* @param camera
* 照相机对象
* @param holder
* 用于实时展示取景框内容的控件
* @param tagInfo
* 摄像头信息,分为前置/后置摄像头 Camera.CameraInfo.CAMERA_FACING_FRONT:前置
* Camera.CameraInfo.CAMERA_FACING_BACK:后置
* @return 是否成功打开某个摄像头
*/
public boolean openCamera(int tagInfo) {
// 尝试开启摄像头
try {
mCamera = Camera.open(getCameraId(tagInfo));
} catch (RuntimeException e) {
e.printStackTrace();
return false;
}
// 开启前置失败
if (mCamera == null) {
return false;
}
// 将摄像头中的图像展示到holder中
try {
// 这里的myCamera为已经初始化的Camera对象
mCamera.setPreviewDisplay(mHolder);
} catch (IOException e) {
e.printStackTrace();
// 如果出错立刻进行处理,停止预览照片
mCamera.stopPreview();
mCamera.release();
mCamera = null;
}
// 如果成功开始实时预览
mCamera.startPreview();
return true;
} /**
* @return 前置摄像头的ID
*/
public int getFrontCameraId() {
return getCameraId(Camera.CameraInfo.CAMERA_FACING_FRONT);
} /**
* @return 后置摄像头的ID
*/
public int getBackCameraId() {
return getCameraId(Camera.CameraInfo.CAMERA_FACING_BACK);
} /**
* @param tagInfo
* @return 得到特定camera info的id
*/
private int getCameraId(int tagInfo) {
Camera.CameraInfo cameraInfo = new Camera.CameraInfo();
// 开始遍历摄像头,得到camera info
int cameraId, cameraCount;
for (cameraId = 0, cameraCount = Camera.getNumberOfCameras(); cameraId < cameraCount; cameraId++) {
Camera.getCameraInfo(cameraId, cameraInfo); if (cameraInfo.facing == tagInfo) {
break;
}
}
return cameraId;
} /**
* 定义图片保存的路径和图片的名字
*/
public final static String PHOTO_PATH = "mnt/sdcard/CAMERA_DEMO/Camera/"; public static String getPhotoFileName() {
Date date = new Date(System.currentTimeMillis());
SimpleDateFormat dateFormat = new SimpleDateFormat("'LOCK'_yyyyMMdd_HHmmss");
return dateFormat.format(date) + ".jpg";
} /**
* 拍照成功回调
*/
public class PicCallback implements PictureCallback {
private String TAG = getClass().getSimpleName();
private Camera mCamera; public PicCallback(Camera camera) {
// TODO 自动生成的构造函数存根
mCamera = camera;
} /*
* 将拍照得到的字节转为bitmap,然后旋转,接着写入SD卡
* @param data
* @param camera
*/
@Override
public void onPictureTaken(byte[] data, Camera camera) {
// 将得到的照片进行270°旋转,使其竖直
Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
Matrix matrix = new Matrix();
matrix.preRotate(270);
bitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
// 创建并保存图片文件
File mFile = new File(PHOTO_PATH);
if (!mFile.exists()) {
mFile.mkdirs();
}
File pictureFile = new File(PHOTO_PATH, getPhotoFileName());
try {
FileOutputStream fos = new FileOutputStream(pictureFile);
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fos);
bitmap.recycle();
fos.close();
Log.i(TAG, "拍摄成功!");
} catch (Exception error) {
Log.e(TAG, "拍摄失败");
error.printStackTrace();
} finally {
mCamera.stopPreview();
mCamera.release();
mCamera = null;
}
} } }

二、布局文件

在讲解java代码之前,先贴下布局文件:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" > <SurfaceView
android:id="@+id/front_surfaceview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1" /> <SurfaceView
android:id="@+id/back_surfaceview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1" /> <RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1" > <Button
android:id="@+id/openFront_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:onClick="buttonListener"
android:text="open front camera" /> <Button
android:id="@+id/openBack_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_alignParentTop="true"
android:onClick="buttonListener"
android:text="open back camera" /> </RelativeLayout> </LinearLayout>

这里主要是放了两个预览框,一个是预览前置的一个是预览后置的。最下面是两个按钮,用来照相。这里的行为是打开摄像头后自动照相,所以就用了open xxxx camera作名字。

三、MainActivity

为了便于讲解,这部分我只说下对前置的操作,后置的操作完全一致,就不赘述了。

3.1 对象

    private CameraManager frontCameraManager;

    /**
* 定义前置有关的参数
*/
private SurfaceView frontSurfaceView;
private SurfaceHolder frontHolder;
private boolean isFrontOpened = false;
private Camera mFrontCamera;

3.2 初始化对象

先找到surfaceView对象,然后产生一个holder对象,这个holder对象实际上可以做的事情有很多

Allows you to control the surface size and format, edit the pixels in the surface, and monitor changes to the surface.

  private void initView() {
/**
* 初始化前置相机参数
*/
// 初始化surface view
frontSurfaceView = (SurfaceView) findViewById(R.id.front_surfaceview);
// 初始化surface holder
frontHolder = frontSurfaceView.getHolder();
}
frontCameraManager = new CameraManager(mFrontCamera, frontHolder);

需要注意的是这时的Camera对象还没有初始化,只有当打开相机时才能初始化,这部分在CameraManager中的打开相机部分有解释。

3.3 开启摄像头并照相

先打开摄像头,如果打开成功就试图对焦,并且表示摄像头已经打开,最后进行照相。

  /**
* @return 开启前置摄像头照相
*/
private void takeFrontPhoto() {
if (isFrontOpened == false && frontCameraManager.openCamera(Camera.CameraInfo.CAMERA_FACING_FRONT)) {
mFrontCamera = frontCameraManager.getCamera();
//自动对焦
mFrontCamera.autoFocus(mAutoFocus);
isFrontOpened = true;
// 拍照
mFrontCamera.takePicture(null, null, frontCameraManager.new PicCallback(mFrontCamera));
}
}
    /**
* 自动对焦的回调方法,用来处理对焦成功/不成功后的事件
*/
private AutoFocusCallback mAutoFocus = new AutoFocusCallback() { @Override
public void onAutoFocus(boolean success, Camera camera) {
//TODO:空实现
}
};

3.4 本类的全部代码

package com.kale.camerademo;

import android.app.Activity;
import android.hardware.Camera;
import android.hardware.Camera.AutoFocusCallback;
import android.os.Bundle;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View; /**
* @author:Jack Tony
* @description :
* 参考自:http://blog.csdn.net/a740169405/article/details/12207229
*
* 还可以参考:http://mobile.51cto.com/amedia-376703.htm
* @date :2015年1月16日
*/
public class MainActivity extends Activity { private CameraManager frontCameraManager;
private CameraManager backCameraManager;
/**
* 定义前置有关的参数
*/
private SurfaceView frontSurfaceView;
private SurfaceHolder frontHolder;
private boolean isFrontOpened = false;
private Camera mFrontCamera;
/**
* 定义后置有关的参数
*/
private SurfaceView backSurfaceView;
private SurfaceHolder backHolder;
private boolean isBackOpened = false;
private Camera mBackCamera; /**
* 自动对焦的回调方法,用来处理对焦成功/不成功后的事件
*/
private AutoFocusCallback mAutoFocus = new AutoFocusCallback() { @Override
public void onAutoFocus(boolean success, Camera camera) {
//TODO:空实现
}
}; @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main); initView(); } public void buttonListener(View view) {
switch (view.getId()) {
case R.id.openFront_button:
takeFrontPhoto();
break;
case R.id.openBack_button:
takeBackPhoto();
break; default:
break;
}
} private void initView() {
/**
* 初始化前置相机参数
*/
// 初始化surface view
frontSurfaceView = (SurfaceView) findViewById(R.id.front_surfaceview);
// 初始化surface holder
frontHolder = frontSurfaceView.getHolder();
frontCameraManager = new CameraManager(mFrontCamera, frontHolder);
/**
* 初始化后置相机参数
*/
// 初始化surface view
backSurfaceView = (SurfaceView) findViewById(R.id.back_surfaceview);
// 初始化surface holder
backHolder = backSurfaceView.getHolder();
backCameraManager = new CameraManager(mBackCamera, backHolder);
} /**
* @return 开启前置摄像头照相
*/
private void takeFrontPhoto() {
if (isFrontOpened == false && frontCameraManager.openCamera(Camera.CameraInfo.CAMERA_FACING_FRONT)) {
mFrontCamera = frontCameraManager.getCamera();
//自动对焦
mFrontCamera.autoFocus(mAutoFocus);
isFrontOpened = true;
// 拍照
mFrontCamera.takePicture(null, null, frontCameraManager.new PicCallback(mFrontCamera));
}
} /**
* @return 开启后置摄像头照相
*/
private void takeBackPhoto() {
if (isBackOpened == false && backCameraManager.openCamera(Camera.CameraInfo.CAMERA_FACING_BACK)) {
mBackCamera = backCameraManager.getCamera();
//自动对焦
mBackCamera.autoFocus(mAutoFocus);
isBackOpened = true;
// 拍照
mBackCamera.takePicture(null, null, backCameraManager.new PicCallback(mBackCamera));
}
} }

3.5 权限

    <!-- 调用相机权限 -->
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" /> <!-- 读写SD卡权限 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

四、后台静默照相的思路

由于保证安全性,android要求camera对象必须提供一个实时的预览框给用户,然用户知道当前相机已经开启。如果我们想要用户在不知情的情况下被照相,那么将预览框做成1dp的大小就行了。

这里还需要注意,由于打开相机需要时间,照相必须要在打开相机之后进行。如果要进行全自动照相,就必须要等打开相机之后再触发照相的代码。我提供的思路是新开一个线程放开启相机的代码,等待两秒后开始照相。

protected void takePhoto(){
//这里得开线程进行拍照,因为Activity还未完全显示的时候,是无法进行拍照的,SurfaceView必须先显示
new Thread(new Runnable() {
@Override
public void run() {
takeFrontPhoto();
}
}).start();
} //开启前置摄像头照相
private boolean takeFrontPhoto() {
if(openFacingFrontCamera())
{
try {
//因为开启摄像头需要时间,这里让线程睡2秒
Thread.sleep(2000);
} catch (InterruptedException e) {}
//拍照
myCamera.takePicture(null, null, myPicCallback);
return true;
}
else{
return false;
}
}

如果你想要同时拍摄前后相机的照片,我目前没有找到很好的方法,理论上是完全可以的。想的办法是在前置拍摄成功后,触发拍摄后置的代码。这个思路挺蠢的,不是很推荐。

如果你不想要用户打开activity照相,你完全可以用service来做这个事情。做一个悬浮窗,然后给悬浮窗中放预览框,设置悬浮窗大小为1dp。开启服务后自动执行照相的代码。

源码下载:http://download.csdn.net/detail/shark0017/8370303

参考自:

http://bbs.51cto.com/thread-1063856-1.html

使用Camera进行拍照 & 后台静默拍照的思路的更多相关文章

  1. Camera实现预览、拍照

    1.利用Intent方法实现拍照并保存 在菜单或按钮的选择操作中调用如下代码,开启系统自带Camera APP,并传递一个拍照存储的路径给系统应用程序,具体如下: imgPath = "/s ...

  2. 摄像头(3)调用系统拍照activity来拍照

    import android.app.Activity; import android.content.Intent; import android.content.pm.PackageManager ...

  3. android + eclipse + 后台静默安装(一看就会)

      首先要说到三个类. import android.content.pm.IPackageInstallObserver; import android.content.pm.IPackageIns ...

  4. Android后台服务拍照

    原文:https://blog.csdn.net/wurensen/article/details/47024961 一.背景介绍最近在项目中遇到一个需求,实现一个后台拍照的功能.一开始在网上寻找解决 ...

  5. Android后台服务拍照的解决方式

    一.背景介绍 近期在项目中遇到一个需求.实现一个后台拍照的功能. 一開始在网上寻找解决方式.也尝试了非常多种实现方式,都没有惬意的方案.只是确定了难点:即拍照要先预览,然后再调用拍照方法.问题也随之而 ...

  6. Android Camera+SurfaceView实现自己定义拍照

    对Activity强制横屏,保证预览方向正确. 使用OrientationEventListener监听设备方向.推断竖拍时,旋转照片后再保存.保证竖拍时预览图片和保存后的图片方向一致. 执行效果: ...

  7. iOS拍照之系统拍照

    拍照在App中使用频次高,入门级别直接调用系统拍照 思路: 系统拍照使用UIImagePickerController 1.设置下plist,否则没权限,报错 2.判断摄像头,获取权限,否则弹出界面黑 ...

  8. selenium加载配置参数,让chrome浏览器不出现‘Chrome正在受到自动软件的控制’的提示语,以及后台静默模式启动自动化测试,不占用桌面的方法

    一:自动化测试的时候,启动浏览器出现‘Chrome正在受到自动软件的控制’,怎么样隐藏,今天学习分享: 在浏览器配置里加个参数,忽略掉这个警告提示语,disable_infobars option = ...

  9. 忽略‘Chrome正在受到自动软件的控制’的提示语,以及后台静默模式启动。

    一.使用Chrome做的时候,会看到浏览器上方出现‘Chrome正在受到自动软件的控制’的提示语, 若想忽略此提示信息,在浏览器配置里加个参数:disable_infobars 代码如下 : # co ...

随机推荐

  1. 记2013年度成都MVP社区巡讲

    上个周六在天府软件园A区的翼起来咖啡举行了MVP社区巡讲成都站的活动,这个巡讲活动去年也搞过一次. MVP社区巡讲是 @微软中国MVP项目组 支持的,由各地的MVP担任讲师,给本地技术社区提供的一种社 ...

  2. 什么是Less、typescript与webpack?

    前端常用技术概述--Less.typescript与webpack 前言:讲起前端,我们就不能不讲CSS与Javascript,在这两种技术广泛应用的今天,他们的扩展也是层出不穷,css的扩展有Les ...

  3. vuex使用modules namespaced 后,模块名不同,函数名相同时候在组件中分发Action

    你在组件中使用 this.$store.dispatch('xxx') 分发 action,或者使用 mapActions 辅助函数将组件的 methods 映射为 store.dispatch 调用 ...

  4. 002.SSH日常命令

    一 远程登陆 ssh 用户名@远程主机ip:首次登陆需要下载对方公钥. 实例:ssh 192.168.10.129 二 远程复制 scp root@远程主机ip:[远程主机文件绝对路径] [需要保存的 ...

  5. 002.DNS-BIND简介

    一 Linux-BIND服务器简介 Bind是Berkeley Internet Name Domain Service的简写,它是一款实现DNS服务器的开放源码软件.已经成为世界上使用最为广泛的DN ...

  6. AngularJS过滤器filter入门

    在开发中,经常会遇到这样的场景 如用户的性别分为“男”和“女”,在数据库中保存的值为1和0,用户在查看自己的性别时后端返回的值自然是1或0,前端要转换为“男”或“女”再显示出来: 如我要换个羽毛球拍, ...

  7. canvas入门级小游戏《开关灯》思路讲解

    游戏很简单,10行10列布局,每行每列各10盏灯,游戏初始化时随机点亮其中一些灯,点击某盏灯,其上下左右的灯及本身状态反转,如果点击前是灭着的,点击后即点亮,将所有灯全部点亮才算过关.游戏试玩: 下面 ...

  8. 基于jquery的水平滚轴组件,多参数可设置。

    闲来无事,继续封装.此次封装的为水平滚轴组件,可选择滚动的距离大小.闲话不多说,直接上图. 参数说明: vis:4                中间区域可显示的 li 个数 scroll:4     ...

  9. NLP文本相似度(TF-IDF)

    本篇博文是数据挖掘部分的首篇,思路主要是先聊聊相似度的理论部分,下一篇是代码实战.       我们在比较事物时,往往会用到“不同”,“一样”,“相似”等词语,这些词语背后都涉及到一个动作——双方的比 ...

  10. 优美的爆搜?KDtree学习

    如果给你平面内一些点,让你求距离某一个指定点最近的点,应该怎么办呢? O(n)遍历! 但是,在遍历的过程中,我们发现有一些点是永远无法更新答案的. 如果我们把这些点按照一定顺序整理起来,省略对不必要点 ...