Android 开启手电筒功能(完美适配4.x, 5.x, 6.x )

    最近在逛GooglePlay应用市场的时候发现很多的手电筒功能居然都带有广告感觉非常的不可思议的。而且这些应用不仅仅有广告而且安装包还特别的大,我本来还想下载一个来玩玩的,后来看了他们的东西感觉也不过如此的。主要功能还是打开手电筒的功能,有没有其他的新颖的地方了,为了能让大家更好的有一个属于的自己的手电筒功能,这里我将整理一个比较完美适配各个机型的手电筒出来。
    在着手准备开发的时候我们更多的时候可能会想着去百度或者google上寻找相应的代码,但是后来我发现市面上的这些代码更多的时候只是一个Demo,如果要完美的适配更多的记性问题还是挺多的,而且很多的代码还是之前的了,比如说5.0,6.0的代码就没有去适配了。因为之前一直都会研究下Android的源代码,而系统也是有自带的手电筒的功能的,于是我在思考是不是可以将系统中手电筒的源代码拿过来然后进行整合就可以了

    因为之前已经下载了Android的源代码了,所以这里我就直接使用Everything(该软件是一款在Window上全局搜索文件,而且速度还非常的快) 软件通过搜索Flashlight关键字就可以找到所有该名的开头的.java文件的。通过搜索我们找到了FlashlightController.java 在 /android-25/com/android/systemui/statusbar/policy,其实我们在下载Android SDK的时候就已经会把该源代码下载下来了,并不需要下载所有的Android代码的。

    打开手电筒在Android5.0以上跟Android5.0以下的实现还有点不一样的。而且在Android5.0以下打开手电筒也还是有点不一样的。下面我们分别来介绍各个版本下的具体实现:

Android5.0以下的实现

//开启手电筒
private Camera start() throws Throwable {
    mCamera = Camera.open();
    if(mCamera == null) {
        //表示当前手机没有前置摄像头
        return null;
    }
    mParameters = mCamera.getParameters();
    mParameters.setFlashMode(Camera.Parameters.FLASH_MODE_TORCH);
    mCamera.setParameters(mParameters);
    mCamera.startPreview();
    return mCamera;
}

//关闭手电筒
private void close() throws Throwable {
    mParameters.setFlashMode(Camera.Parameters.FLASH_MODE_OFF);
    mCamera.setParameters(mParameters);
    mCamera.stopPreview();
    mCamera.release();
}

    从上面的代码中我们可以看出Android5.0以下的实现方式特别的简单,只是通过Camera.open()来获取一个Camera就可以打开手电筒了,这个大家都不必多说的。网上的大部分的实现也都是这样子的了。

Android6.0以下的实现
    Android5.0到Android6.0之间的实现跟其他的版本也还有点不同的,由于google废弃了Camera而采用了CameraService来实现打开相机,同时打开手电筒通过开启相机的闪光灯的方式来实现:

  1. 首先获取cameraId。

    /**
    * 获取相机的cameraId,如果cameraId为空表示该手机不支持闪光灯
    * 还有一种可能就是该Camera的权限被拒绝了,所以一直拿不到CameraService的
    * @return
    * @throws CameraAccessException
    */
    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public String getCameraId() throws CameraAccessException {
    //获取手机上所有的摄像头的信息
    String ids[] = mCameraManager.getCameraIdList();
    for (String id : ids) {
        CameraCharacteristics c = mCameraManager.getCameraCharacteristics(id);
        Boolean flashAvailable = c.get(CameraCharacteristics.FLASH_INFO_AVAILABLE);
        Integer lensFacing = c.get(CameraCharacteristics.LENS_FACING);
        //这里需要判断手机是否支持闪光灯,而且是否有后置摄像头
        if (flashAvailable != null && flashAvailable
                && lensFacing != null && lensFacing == CameraCharacteristics.LENS_FACING_BACK) {
            return id;
        }
    }
    return null;
    }
  2. 接着在CameraDevice.StateCallback接口的onOpened()回调方法中获取CameraDevice

    private final CameraDevice.StateCallback mCameraListener = new CameraDevice.StateCallback() {
        @Override
        public void onOpened(CameraDevice camera) {
            //这里表示获取CameraDevice成功了,然后再执行下一步
            mCameraDevice = camera;
            postUpdateFlashlight();
        }
    
        @Override
        public void onDisconnected(CameraDevice camera) {
            //该回调接口表示连接底层的CameraService失败,然后重新释放资源
            if (mCameraDevice == camera) {
                dispatchError(ERROR_CODE_CAMERA_IN_USE);
                teardown();
            }
        }
    
        @Override
        public void onError(CameraDevice camera, int error) {
            Log.e(TAG, "Camera error: camera = " + camera + " error=" + error);
            if (camera == mCameraDevice || mCameraDevice == null) {
                // 我们在这里处理获取CameraDevice的一些错误码
                // 因为在获取CameraDevice的时候会有些出错误的
                handleError(errorCode);
            }
        }
    };
    
    private void startDevice() {
        //表示用户未授权给手电筒
        if (ContextCompat.checkSelfPermission(mContext, "android.permission.CAMERA") != 0) {
            dispatchError(ERROR_CODE_NOT_PERMISSION);
            return;
        }
        try {
            mCameraManager.openCamera(mCameraId, mCameraListener, mHandler);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        } catch (SecurityException e1) {
            Log.e(TAG, "The System will reject the camera permission and not open camera");
        } catch (Throwable throwable) {
            Log.e(TAG, "Couldn't open the camera use the cameraId by the CameraManager");
        }
    }
    
  3. 接着通过CameraCaptureSession.StateCallback的onConfigured()回调方法获取CameraCaptureSession

    private final CameraCaptureSession.StateCallback mSessionListener =
                                                        new CameraCaptureSession.StateCallback() {
            @Override
            public void onConfigured(CameraCaptureSession session) {
                mSession = session;
                postUpdateFlashlight();
            }
    
            @Override
            public void onConfigureFailed(CameraCaptureSession session) {
                Log.e(TAG, "Configure failed.");
                if (mSession == null || mSession == session) {
                    handleError(ERROR_CODE_UNKNOW);
                }
            }
    };
    
    private void startSession() throws CameraAccessException {
        mSurfaceTexture = new SurfaceTexture(0, false);
        Size size = getSmallestSize(mCameraDevice.getId());
        mSurfaceTexture.setDefaultBufferSize(size.getWidth(), size.getHeight());
        mSurface = new Surface(mSurfaceTexture);
        ArrayList<Surface> outputs = new ArrayList<>(1);
        outputs.add(mSurface);
        mCameraDevice.createCaptureSession(outputs, mSessionListener, mHandler);
    }
    
    private Size getSmallestSize(String cameraId) throws CameraAccessException {
        Size[] outputSizes = mCameraManager.getCameraCharacteristics(cameraId)
                .get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
                .getOutputSizes(SurfaceTexture.class);
        if (outputSizes == null || outputSizes.length == 0) {
            throw new IllegalStateException("Camera " + cameraId + "doesn't support any outputSize.");
        }
        Size chosen = outputSizes[0];
        for (Size s : outputSizes) {
            if (chosen.getWidth() >= s.getWidth() && chosen.getHeight() >= s.getHeight()) {
                chosen = s;
            }
        }
        return chosen;
    }
  4. 当我们CameraDevice和CameraCaptureSession,这个时候我们就可以控制手电筒了

    //开启手电筒功能
    if (mFlashlightRequest == null || 
        (mFlashlightRequest.get(CaptureRequest.FLASH_MODE)).intValue() != 2) {
        CaptureRequest.Builder builder =
                                  mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
        builder.set(CaptureRequest.FLASH_MODE, CameraMetadata.FLASH_MODE_TORCH);
        builder.addTarget(mSurface);
        CaptureRequest request = builder.build();
        mSession.capture(request, null, mHandler);
        mFlashlightRequest = request;
        mFlashlightEnabled = true;
    }
    
    //关闭手电筒功能
    if (mFlashlightRequest != null) {
        CaptureRequest.Builder builder =     
                                  mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
        //在这里我们只要设置一下FLASH_MODE就可以关闭手电筒,这样子我们不去释放资源的话,再下次打开手电筒的时候就可以非常快速的打开了,但是这里也有一个特别需要注意的是这样子虽然可以加快打开手电筒的速度,但是如果我们不释放资源的话,则其他的应用是开不起手电筒功能的,这样子也是有利也有弊的。在开发的时候我们要特别的注意这个问题。
        builder.set(CaptureRequest.FLASH_MODE, CameraMetadata.FLASH_MODE_OFF);
        builder.addTarget(mSurface);
        CaptureRequest request = builder.build();
        mSession.capture(request, null, mHandler);
        mFlashlightRequest = request;
        mFlashlightEnabled = false;
    }
  5. 当我们不需要使用手电筒功能的时候需要对该资源进行释放,不然别的应用将不能开启手电筒功能

    private void teardown() {
        if (mCameraDevice != null) {
            mCameraDevice.close();
        }
        mCameraDevice = null;
        mSession = null;
        mFlashlightRequest = null;
        if (mSurface != null) {
            mSurface.release();
            mSurfaceTexture.release();
        }
        mSurface = null;
        mSurfaceTexture = null;
    }

        通过上面打开手电筒的代码我们发现android5.0上的实现是比较复杂了,因为google对camera进行了大规模的改动和重新架构了,废弃了之前的Camera而采用了CameraManager去实现。但是在有些变态的手机上则会遇到获取cameraId出现SecurityException的异常,比如说在有些魅蓝手机装有阿里的云Os系统上使用这种方式去打开手电筒则不行的,还是需要使用第一种方式去打开手电筒的。

Android6.0及以上打开方式

    Google的开发人员觉得在5.0上打开手电筒的方式太过于复杂了,于是就将更多没必要的细节封装起来不需要让调用知道更多的。因此在Android6.0上就做了一些一定的规模的精简性,实现上没有这么负责了。

  1. 首先还是需要获取cameraId,这一步更上面的是一样的
  2. 其次注册CameraManager.TorchCallback

    
    private final CameraManager.TorchCallback mTorchCallback =
            new CameraManager.TorchCallback() {
    
        //该方法表示手电筒是否支持
        @Override
        public void onTorchModeUnavailable(String cameraId) {
            if (TextUtils.equals(cameraId, mCameraId)) {
                setCameraAvailable(false);
            }
        }
    
        //该方法表示手电筒状态发生了变化,比如说手电筒从开变成了关
        @Override
        public void onTorchModeChanged(String cameraId, boolean enabled) {
            if (TextUtils.equals(cameraId, mCameraId)) {
                setCameraAvailable(true);
                setTorchMode(enabled);
            }
        }
    
        private void setCameraAvailable(boolean available) {
            boolean changed;
            synchronized (HighVersionController.this) {
                changed = mCameraAvailable != available;
                mCameraAvailable = available;
            }
            if (changed) {
                Log.d(TAG, "dispatchAvailabilityChanged(" + available + ")");
            }
        }
    
        private void setTorchMode(boolean enabled) {
            boolean changed;
            synchronized (HighVersionController.this) {
                changed = mFlashlightEnabled != enabled;
                mFlashlightEnabled = enabled;
            }
            if (changed) {
                Log.d(TAG, "dispatchModeChanged(" + enabled + ")");
                dispatchModeChanged(enabled);
            }
        }
    };
    
     public synchronized void ensureHandler() {
         if (mHandler == null) {
             HandlerThread thread = new HandlerThread(TAG, 8);
             thread.start();
             mHandler = new Handler(thread.getLooper());
         }
    }
    
    if (mCameraId != null) {
         mCameraManager.registerTorchCallback(mTorchCallback, mHandler);
    }

        从上面的代码中我们可以很快的知道Android6.0上的实现方式跟5.0上进行了大大的精简了,我们不需要关心内部的一些东西,我们只要关心的是手电筒的状态是否发生了变化以及手电筒是否能用之类的就可以了,至于一些其他的细节的话我们可以封装起来通过回调的方式暴漏出来就可以了。下面我们就将几个平台不同差异化的实现然后抽象出来,然后统一根据不同的版本然后通过不同的方式来实现出来。

类图的设计
flash_light_class_relation.png-31.4kB

    首先我们抽象出一个FlashLightFactory的接口,该接口主要定义错误码,开启手电筒方法,添加手电筒状态监听,删除状态监听,以及获取手电筒开关状态等等一系列的东西。下面分别介绍不同的类的作用

  • FlashLightFactory:该接口主要是定义了一些对手电筒操作的基本方法,比如说开启手电筒,获取手电筒的开关状态,以及判断手机是否支持手电筒功能等等。
  • BaseController:该类继承FlashLightFactory接口,然后实现一些简单的常用的功能,比如说错误的分发,手电筒状态的分发之类的
  • CompatController:该类继承BaseController,主要是用在Android5.0以上的一些公共的方法上的,比如说获取CameraId,因为5.0之后废除了Camera而直接使用CameraManager了,如果我们将CameraManager之类的东西写在BaseController方法内的话在一些低版本的手机则会出现奔溃的。
  • LowVersionController:该类直接由于是用了低版本的方法去开启手电筒的,而且还是使用了Camera去实现的,所以我们需要在这个里面单独进行处理开启手电筒的功能的,主要是处理android5.0以下的手机上的。
  • HighVersionController:该类继承CompatController,用于处理Android6.0上开启手电筒的方式。
  • MiddleVersionController:该类主要是用于实现Android5.0-6.0之间的打开手电筒的,通过使用CameraManager以及CameraDevice等等去实现的。
  • FlashLightListener:该接口的主要是对外提供使用的,主要是用户手电筒打开错误的回调,以及手电筒开关状态的回调,供调用手电筒功能的人使用

    当我们有了一个清晰的逻辑之后,我们就根据这个逻辑分别对应不同的版本去实现的。其实上面的实现也是根据Android各个版本对手电筒不同的实现进行不同的处理的,这样子我们是为了更好的适配不同的手机类型,使我们的手电筒更加兼容不同的品牌。下面我们就可以弄一个专门搞一个控制类用于控制各个版本的处理的方式。

核心代码
    为了让大家更好的学习代码以及能更快的掌握知识,同时也尽可能的去踩坑,下面我会将代码放到github上,同时也希望大家更加踊跃的反馈问题,或者是大家有更好的实现方法也可以将你们的代码提交上去,有利于大家的一起学习和进步。本次的打开手电筒其实说难不难,但是要做起来而且要做好的话确实也不简单,特别是大家都对那些系统的API都不是很熟悉的情况下要写出一个比较好的兼容各个平台的手电筒功能还真的是不简单,本人本着不重复造轮子,合理的利用Android开源的代码所以就将各个平台的手电筒功能给搬过来,因为这样子的代码才是最稳定的,坑也是最少的。我们平时在学些的时候更多的还是要多利用别人的代码,别老是自己去重复的造轮子,你造的轮子肯定比不过别人经过千万测试的代码要好的。

github地址https://github.com/ACMNexus/flashlight

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值