基于camera2 untiy悬浮窗摄像头预览分析

前言

在某机系统上 unity 导出的android 工程 使用camerax 悬浮窗口 bindToLifecycle 加上预览界面就显示不出来(手机上没问题,原因没找到),不得以 改用camera2 ,一切正常
本章 讲述 camera2 的悬浮窗 摄像头 预览分析

1 环境
unity 2020.2
as 4.1.1

2 unity 代码
布局如下
在这里插入图片描述
代码 脚本拖到 Main Camera 上

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class SampleScene : MonoBehaviour
{
    Button btn1;
    Button btn2;

    AndroidJavaClass jc = null;
    AndroidJavaObject jo = null;


    // Start is called before the first frame update
    void Start()
    {
        //第一种
        /*
         {
            GameObject obj1 = GameObject.Find("Canvas/Button1");
            if(obj1 != null)
            {
                btn1 = obj1.GetComponent<Button>();
                btn1.onClick.AddListener(delegate() { OnClickBtn1(); });
            }
            GameObject obj12 = GameObject.Find("Canvas/Button2");
            if (obj12 != null)
            {
                btn2 = obj12.GetComponent<Button>();
                btn1.onClick.AddListener(delegate () { OnClickBtn2(); });
            }
        }
        */
        //第二种
        {
            GameObject obj11 = GameObject.Find("Canvas/Button1");
            if (obj11 != null)
            {
                btn1 = obj11.GetComponent<Button>();
                btn1.onClick.AddListener(delegate () { OnClickAllBtn(obj11); });
            }
            GameObject obj112 = GameObject.Find("Canvas/Button2");
            if (obj112 != null)
            {
                btn2 = obj112.GetComponent<Button>();
                btn2.onClick.AddListener(delegate () { OnClickAllBtn(obj112); });
            }

        }

       
        if (Application.platform == RuntimePlatform.Android)
        {
            //1: unity调用Android的非静态方法。继承UnityPlayerActivity
            // 注意:使用这种方法的时候,你在Android Studio 中写的类要继承UnityPlayerActivity才能调到,
            //但是只能有一个类继承它,写多个类都继承的时候,其他类也是调不到的
            //  AndroidJavaClass jc = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
            //  AndroidJavaObject jo = jc.GetStatic<AndroidJavaObject>("currentActivity");
            //  jo.Call("testfun", "");
            //  package com.example.test;
            //public class MainActivity extends UnityPlayerActivity {
            // public void testfun(String str) { }
            //  }
            {
                jc = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
                jo = jc.GetStatic<AndroidJavaObject>("currentActivity");

            }

            //2://unity调用Android的非静态方法 包名加类名
            //   AndroidJavaObject jo = new AndroidJavaObject("com.example.test.Test"); //包名加类名
            //   jo.Call("testfun", "");
            //package com.example.test;
            //public class Test
            //{
            //  public void testfun(String str)  { }
            //}
            /*
            string packname = "com.test.testfloatingwindow";
            string activityname = ".MainActivity";

            jo = new AndroidJavaObject(packname + activityname);
            if (jo == null)
            {
                OutputLogErr("AndroidJavaObject:" + packname + activityname);
            }
            */

            //3: unity调用Android的静态方法
            // 注意:这里的AndroidJavaClass()里面的是自己的包名 + 类名
            //这样写的可以不用继承UnityPlayerActivity也可以用
            // AndroidJavaClass jc = new AndroidJavaClass("com.example.test.Test");
            // jc.GetStatic<AndroidJavaObject>("testfun", "");
            // package com.example.test;
            //public class Test
            //{
            //    public static void testfun(String str){}
            //}
            /*
            jc = new AndroidJavaClass(packname + activityname);
            if(jc  == null)
            {
                OutputLogErr("AndroidJavaClass:" + packname + activityname);
            }
            */

        }

    }

    // Update is called once per frame
    void Update()
    {
        
    }


    void OnClickBtn1()
    {
        OutputLog("OnClickBtn1");
        if (jo != null)
        {
            jo.Call("OnClickBtn1");
        }
    }

    void OnClickBtn2()
    {
        OutputLog("OnClickBtn2");
        if (jo != null)
        {
            jo.Call("OnClickBtn2");
        }
    }


    void OnClickAllBtn(GameObject obj)
    {
        OutputLog(obj.name);
        switch (obj.name)
        {
            case "Button1":
                OnClickBtn1();
                break;
            case "Button2":
                OnClickBtn2();
                break;

        }

    }

    void OutputLog(string str)
    {
        string date = DateTime.Now.ToLocalTime().ToString(); // 2008-9-4 20:12:12
        Debug.Log(date+str);
    }
    void OutputLogErr(string str)
    {
        string date = DateTime.Now.ToLocalTime().ToString(); // 2008-9-4 20:12:12
        Debug.LogError(date + str);
    }

    //接受android 消息
    //android 代码里  UnityPlayer.UnitySendMessage("Main Camera","onmessage","adroid"+str);//这里参数是string
    把消息发送给Unity场景中Main Camera物体上的onmessage方法
    //这里要把 这个脚本拖到 Main Camera 上
    把消息发送给Unity场景中某物体上的某方法
    //UnityPlayer.UnitySendMessage("某物体名","某方法","参数");//这里参数是string
    void onmessage(string str)
    {
        try
        {
            if (!string.IsNullOrEmpty(str))
            {
                OutputLog("onmessage" + str);
            }
            else
            {

            }
        }
        catch(Exception e)
        {
            OutputLogErr("onmessage err"+e.Message );
        }

    }
}

3 导出android 工程
导出前线配置下
配置下包名 这里导出的是 IL2CPP minnum api level 23
在这里插入图片描述
在这里插入图片描述

4 导出Android 工程
在这里插入图片描述

5 增加悬浮窗 摄像头预览
调用过程
//
//启动过程
//1: 申请权限,得到相机与屏幕 信息 多出相依处理
//applyForPermission() //放到 onCreate 里就OK了
//有权限 dosomething
//2:dosomething 里 初始化相机
//createFloatingWindowLayout() 初始化各种监听 等
//3: 点击事件 或其他地方调用
//addFloatingWindowView 触发
// TextureView.SurfaceTextureListener() 的
// public void onSurfaceTextureAvailable 从而 启动相机
//4: 启动相机
//openCamera 在 mCameraManager.openCamera 前 回调函数都设置好
//createCameraDeviceCallback() ; //相机状态监听回调
// createImageReader(); //图像解析
//触发 createCameraPreviewSession
//5 创建预览会话
// 预览 解析 加入会话申请中

1> UnityPlayerActivity.java

// GENERATED BY UNITY. REMOVE THIS COMMENT TO PREVENT OVERWRITING WHEN EXPORTING AGAIN
package com.unity3d.player;

import android.Manifest;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.graphics.ImageFormat;
import android.graphics.PixelFormat;
import android.graphics.SurfaceTexture;
import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraCaptureSession;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraDevice;
import android.hardware.camera2.CameraManager;
import android.hardware.camera2.CaptureRequest;
import android.hardware.camera2.params.StreamConfigurationMap;
import android.media.Image;
import android.media.ImageReader;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.util.Size;
import android.view.Display;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.Surface;
import android.view.TextureView;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.os.Process;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;

import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

public class UnityPlayerActivity extends Activity implements IUnityPlayerLifecycleEvents
{
    protected UnityPlayer mUnityPlayer; // don't change the name of this variable; referenced from native code

    // Override this in your custom UnityPlayerActivity to tweak the command line arguments passed to the Unity Android Player
    // The command line arguments are passed as a string, separated by spaces
    // UnityPlayerActivity calls this from 'onCreate'
    // Supported: -force-gles20, -force-gles30, -force-gles31, -force-gles31aep, -force-gles32, -force-gles, -force-vulkan
    // See https://docs.unity3d.com/Manual/CommandLineArguments.html
    // @param cmdLine the current command line arguments, may be null
    // @return the modified command line string or null
    protected String updateUnityCommandLineArguments(String cmdLine)
    {
        return cmdLine;
    }

    // Setup activity layout
    @Override protected void onCreate(Bundle savedInstanceState)
    {
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        super.onCreate(savedInstanceState);

        String cmdLine = updateUnityCommandLineArguments(getIntent().getStringExtra("unity"));
        getIntent().putExtra("unity", cmdLine);

        mUnityPlayer = new UnityPlayer(this, this);
        setContentView(mUnityPlayer);
        mUnityPlayer.requestFocus();

        applyForPermission(); // add  by eh
    }

    // When Unity player unloaded move task to background
    @Override public void onUnityPlayerUnloaded() {
        moveTaskToBack(true);
    }

    // Callback before Unity player process is killed
    @Override public void onUnityPlayerQuitted() {
    }

    @Override protected void onNewIntent(Intent intent)
    {
        // To support deep linking, we need to make sure that the client can get access to
        // the last sent intent. The clients access this through a JNI api that allows them
        // to get the intent set on launch. To update that after launch we have to manually
        // replace the intent with the one caught here.
        setIntent(intent);
        mUnityPlayer.newIntent(intent);
    }

    // Quit Unity
    @Override protected void onDestroy ()
    {
        mUnityPlayer.destroy();
        super.onDestroy();
    }

    // Pause Unity
    @Override protected void onPause()
    {
        super.onPause();
        mUnityPlayer.pause();
    }

    // Resume Unity
    @Override protected void onResume()
    {
        super.onResume();
        mUnityPlayer.resume();
    }

    // Low Memory Unity
    @Override public void onLowMemory()
    {
        super.onLowMemory();
        mUnityPlayer.lowMemory();
    }

    // Trim Memory Unity
    @Override public void onTrimMemory(int level)
    {
        super.onTrimMemory(level);
        if (level == TRIM_MEMORY_RUNNING_CRITICAL)
        {
            mUnityPlayer.lowMemory();
        }
    }

    // This ensures the layout will be correct.
    @Override public void onConfigurationChanged(Configuration newConfig)
    {
        super.onConfigurationChanged(newConfig);
        mUnityPlayer.configurationChanged(newConfig);
    }

    // Notify Unity of the focus change.
    @Override public void onWindowFocusChanged(boolean hasFocus)
    {
        super.onWindowFocusChanged(hasFocus);
        mUnityPlayer.windowFocusChanged(hasFocus);
    }

    // For some reason the multiple keyevent type is not supported by the ndk.
    // Force event injection by overriding dispatchKeyEvent().
    @Override public boolean dispatchKeyEvent(KeyEvent event)
    {
        if (event.getAction() == KeyEvent.ACTION_MULTIPLE)
            return mUnityPlayer.injectEvent(event);
        return super.dispatchKeyEvent(event);
    }

    // Pass any events not handled by (unfocused) views straight to UnityPlayer
    @Override public boolean onKeyUp(int keyCode, KeyEvent event)     { return mUnityPlayer.injectEvent(event); }
    @Override public boolean onKeyDown(int keyCode, KeyEvent event)   { return mUnityPlayer.injectEvent(event); }
    @Override public boolean onTouchEvent(MotionEvent event)          { return mUnityPlayer.injectEvent(event); }
    /*API12*/ public boolean onGenericMotionEvent(MotionEvent event)  { return mUnityPlayer.injectEvent(event); }

    //增加的代码开始处
    final   private static String TAG = UnityPlayerActivity.class.getName() ;
    //供unity 调用 接口
    public  void OnClickBtn1(){
        Log.d(TAG,"android OnClickBtn1");
        addFloatingWindowView(); //增加悬浮窗后 ,
    }

    //供unity 调用 接口
    public  void OnClickBtn2(){
        Log.d(TAG,"android OnClickBtn2");
      //  addFloatingWindowView(); //增加悬浮窗后 ,
    }

    //供unity 调用 接口 暂时无用
    public static void OnClickBtn3(){
        Log.d(TAG,"android OnClickBtn3");
        // myActivity.addFloatingWindowView();
    }

    @Override
    protected void onStart() {
        super.onStart();
    }

    /
    //增加camera2 摄像头
    private CameraManager mCameraManager;
    private CameraDevice.StateCallback mStateCallback;
    private CameraDevice mCameraDevice;
    private TextureView mTextureView;
    private CameraCaptureSession mCaptureSession;
    private CaptureRequest mPreviewRequest;

    private int mCameraSensorOrientation = 0    ;     //摄像头方向
    private int mCameraFacing = CameraCharacteristics.LENS_FACING_FRONT   ;      //默认使用前置摄像头
    private int mDisplayRotation =  0; //getDisplay().defaultDisplay.rotation  //手机方向
    private String  cameraIdFront =  "";  //相机ID
    private  Size[] cameraSizeFront  ;  //相机支持的尺寸
    private  CameraCharacteristics cameraCharacteristicsFront ; //
    private static final int RECV_MAX_IMAGES  =  2 ; //同时最多接收2张图片

    private ImageReader mImageReader;
    private TextureView.SurfaceTextureListener surfaceTextureListener ;
    private ImageReader.OnImageAvailableListener imageAvailableListener;


    private boolean mFlashSupported =false;  //判断是否支持闪关灯
    // private HandlerThread mBackgroundThread; //用于运行不应阻塞UI的任务的附加线程
    //  private Handler mBackgroundHandler; //用于在后台运行任务的{@link Handler}
    android.util.Range<Integer> mAverageFPS =null  ; //中间帧率
    
    //悬浮窗相关
    private WindowManager windowManager;
    private WindowManager.LayoutParams layoutParams;
    //  public static final int DMS_INPUT_IMG_W = 640;
    //  public static final int DMS_INPUT_IMG_H = 480;
    public static final int PREVIEW_WIDTH = 640  ;                                       //预览的宽度
    public static final int PREVIEW_HEIGHT = 480 ;                                       //预览的高度
    public static final int DEFAULT_SCREEN_X = 300 ;                                    //默认开始位置
    public static final int DEFAULT_SCREEN_Y = 300 ;                                    //默认开始位置
    private Size mPreviewSize = null;     //预览的尺寸 默认 PREVIEW_WIDTH  PREVIEW_HEIGHT
    private View mPreviewdisplayView = null;  //预览窗口
    private Surface mPreviewSurface;                 //预览窗口surface
  //  LayoutInflater floatingWindowlayoutInflater = null ;  //悬浮窗口布局填充器  //放这里也可以,用临时变量也可以的
    //
    //权限 相关
    private final int REQUEST_CODE_CONTACT = 1;
    final  String[] AppPermissions = {
            //   Manifest.permission.WRITE_EXTERNAL_STORAGE,
            //    Manifest.permission.READ_EXTERNAL_STORAGE,
            //    Manifest.permission.READ_PHONE_STATE,
            Manifest.permission.INTERNET,
            Manifest.permission.CAMERA,
            //Manifest.permission.RECORD_AUDIO,
    };
    ///
    //创建悬浮窗布局
    void createFloatingWindowLayout(){

        windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
        layoutParams = new WindowManager.LayoutParams();

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            layoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL; //TYPE_APPLICATION_SUB_PANEL ; //TYPE_APPLICATION_OVERLAY;
        } else {
            layoutParams.type = WindowManager.LayoutParams.TYPE_PHONE;
        }
        layoutParams.format = PixelFormat.RGBA_8888;
        layoutParams.gravity = Gravity.LEFT | Gravity.TOP;
        layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
        layoutParams.width =  getPreviewWidth(); // PREVIEW_WIDTH;  //窗口宽度
        layoutParams.height = getPreviewHeight();//PREVIEW_HEIGHT; //窗口高度
        layoutParams.x = DEFAULT_SCREEN_X;  //开始的位置
        layoutParams.y = DEFAULT_SCREEN_Y; //开始的位置


    }

    //增加悬浮窗口
    void addFloatingWindowView(){
        LayoutInflater floatingWindowlayoutInflater = LayoutInflater.from(this);
        mPreviewdisplayView = floatingWindowlayoutInflater.inflate(R.layout.camera_display, null);
        mPreviewdisplayView.setOnTouchListener(new FloatingOnTouchListener());
        mTextureView = mPreviewdisplayView.findViewById(R.id.preview);
        mTextureView.setSurfaceTextureListener(surfaceTextureListener);
        windowManager.addView(mPreviewdisplayView, layoutParams);
    }

    private  void doSomeThing(){
        if(initCameraInfo()){
            initSurfaceTextureListener(); //监听SurfaceTexture
            initImageAvailableListener();  //图像监听,也可以放到createImageReader前
            createFloatingWindowLayout();  //创建布局
        }
    }

    //申请权限
    private  void applyForPermission(){
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            for (String str : AppPermissions) {
                if (ContextCompat.checkSelfPermission(this, str) != PackageManager.PERMISSION_GRANTED) {
                    ActivityCompat.requestPermissions(this, AppPermissions, REQUEST_CODE_CONTACT);
                    return;
                }
            }
            //do some thing
            doSomeThing();

        }else{
            //使用的比Build.VERSION_CODES.M大  所以这里不可能走到
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        // super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (requestCode == REQUEST_CODE_CONTACT) {
            // If request is cancelled, the result arrays are empty.
            if (grantResults.length > 0
                    && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                //do some thing
                doSomeThing();
            } else if (grantResults.length == 0) {
                // 已知魅族手机会进这个结果
            } else {
                Toast.makeText(this, "您拒绝了相关权限,无法使用!", Toast.LENGTH_SHORT).show();
                finish();
            }
        }
    }
    ///相机相关//
    //初始化Surface 监听
    void initSurfaceTextureListener(){

        surfaceTextureListener= new TextureView.SurfaceTextureListener() {
            @Override
            public void onSurfaceTextureAvailable(@NonNull SurfaceTexture surfaceTexture, int width, int height) {
                SurfaceTexture texture = mTextureView.getSurfaceTexture();
                texture.setDefaultBufferSize(getPreviewWidth(), getPreviewHeight());
                mPreviewSurface = new Surface(texture);
                openCamera();

            }

            @Override
            public void onSurfaceTextureSizeChanged(@NonNull SurfaceTexture surface, int width, int height) {

            }

            @Override
            public boolean onSurfaceTextureDestroyed(@NonNull SurfaceTexture surface) {
                return false;
            }

            @Override
            public void onSurfaceTextureUpdated(@NonNull SurfaceTexture surface) {

            }
        };
    }


    private  boolean initWindowManager(Context context){
        if(windowManager == null){
            if (context instanceof Activity) {
                Activity activity = (Activity) context;
                windowManager = activity.getWindowManager();
            } else {
                windowManager = (WindowManager) context.getSystemService(UnityPlayerActivity.WINDOW_SERVICE);
            }
        }
        return  windowManager !=null ;
    }
    //得到屏幕旋转角度
    private int getDisplayRotation(Context context) {
        if(initWindowManager(context))
        {
            int rotation = windowManager.getDefaultDisplay().getRotation();
            switch (rotation) {
                case Surface.ROTATION_0:
                    return 0;
                case Surface.ROTATION_90:
                    return 90;
                case Surface.ROTATION_180:
                    return 180;
                case Surface.ROTATION_270:
                    return 270;
            }
        }
        return 0;
    }

    //得到屏幕信息
    private Display getDisplay(Context context) {
        if(initWindowManager(context)) return windowManager.getDefaultDisplay() ;
        return null;
    }

    private class CompareSizeByArea implements java.util.Comparator<Size> {
        @Override
        public int compare(Size lhs, Size rhs) {
            return Long.signum((long) lhs.getWidth() * lhs.getHeight()
                    - (long) rhs.getWidth() * rhs.getHeight());
        }
    }

    private  int getPreviewWidth(){
        return mPreviewSize!=null?mPreviewSize.getWidth():PREVIEW_WIDTH ;
    }

    private  int getPreviewHeight(){
        return mPreviewSize!=null?mPreviewSize.getHeight():PREVIEW_HEIGHT;
    }

    //是否支持闪光灯模式
    private  boolean isFlashSupported(){
        return  mFlashSupported ;
    }

    //获取设备的方向
    private static int sensorToDeviceRotation(CameraCharacteristics characteristics, int deviceOrientation) {

        int sensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);

        ///
//        ORIENTATIONS.append(Surface.ROTATION_0, 90);
//
//        ORIENTATIONS.append(Surface.ROTATION_90, 0);
//
//        ORIENTATIONS.append(Surface.ROTATION_180, 270);
//
//        ORIENTATIONS.append(Surface.ROTATION_270, 180);
        //
        //  deviceOrientation = ORIENTATIONS.get(deviceOrientation);
        //int deviceOrientation = 0 ;
        switch (deviceOrientation)
        {
            case  Surface.ROTATION_0:  deviceOrientation = 90 ; break;
            case  Surface.ROTATION_90:  deviceOrientation = 0 ; break;
            case  Surface.ROTATION_180:  deviceOrientation = 270 ; break;
            case  Surface.ROTATION_270:  deviceOrientation = 180 ; break;
        }

        return (sensorOrientation + deviceOrientation + 360) % 360;

    }

    //初始化相机
    private boolean initCameraInfo() {
        mCameraManager = (CameraManager) getSystemService(CAMERA_SERVICE);
        try {
            String []  carmeraids = mCameraManager.getCameraIdList() ;
            for(int i =0 ;i<carmeraids.length ;i++){
                CameraCharacteristics cameraCharacteristics =  mCameraManager.getCameraCharacteristics(carmeraids[i]);
                // cameraCharacteristics.get(CameraCharacteristics.LENS_FACING);
                if(cameraCharacteristics.get(CameraCharacteristics.LENS_FACING)== mCameraFacing) {  //前置
                    cameraIdFront = carmeraids[i] ;
                    cameraCharacteristicsFront = cameraCharacteristics ;
                    cameraSizeFront =  cameraCharacteristics .get( CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP) .getOutputSizes(ImageReader.class);
                    break;
                }
            }
        }catch (CameraAccessException e){
            Log.e(TAG,e.getMessage());
            return false;
        }

//        int  supportLevel = cameraCharacteristicsFront.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL)
//        if (supportLevel == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY) {
//           // mActivity.toast("相机硬件不支持新特性")
//        }

        int  supportLevel = cameraCharacteristicsFront.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL);
        if(!isHardwareLevelSupported(supportLevel,CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)){ //希望支持
            return  false ;
        }
        //判断是否支持闪光灯
        Boolean available = cameraCharacteristicsFront.get(CameraCharacteristics.FLASH_INFO_AVAILABLE);
        mFlashSupported = available == null ? false : available;

        StreamConfigurationMap map = cameraCharacteristicsFront.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
        Size [] outputSizes = map.getOutputSizes(ImageFormat.YUV_420_888);
        Size largest = Collections.max(Arrays.asList(outputSizes), new CompareSizeByArea());
        // 获取预览画面的尺寸
         if(!(outputSizes!= null  && outputSizes.length > 0)){
            return  false;
        }
        boolean bFound = false ;
        for(Size s :outputSizes){
            if(s.getWidth() == PREVIEW_WIDTH && s.getHeight() == PREVIEW_HEIGHT){
                bFound = true ; //
                break;
            }
        }

        if(bFound){
            mPreviewSize = new Size(PREVIEW_WIDTH,PREVIEW_HEIGHT);
        }else {
            int average = (int)(outputSizes.length/2); //平均值  偶数个 取大的
            mPreviewSize = outputSizes[average] ;//map.getOutputSizes(SurfaceTexture.class)[0]; //0 为最大尺寸, 感觉不合理,所以取平均值
        }

        //帧率
        //Key<android.util.Range<Integer>[]> CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES
        android.util.Range<Integer>[] ranges =  cameraCharacteristicsFront.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES);
        if(ranges != null  && ranges.length > 0){
            //取中间值 从低往高  0 帧率最低
            int average = (int)(ranges.length/2); //平均值  偶数个 取大的
            mAverageFPS = ranges[average];  //中间值
        }
        return  true ;
    }

//        对于Camera2采集系统来说,每个摄像头都有一个支持等级: 从高到底为 越高越好
//        INFO_SUPPORTED_HARDWARE_LEVEL_3 支持YUV再处理和原始数据采集功能,并且具备先进的功能。
//        INFO_SUPPORTED_HARDWARE_LEVEL_FULL支持先进的摄像头功能。
//        INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED向后兼容模式,底层等同于Camera1的实现。
//        INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY 随机赠送的功能支持,支持性不足。
//        总的来说如果摄像头等级是LEVEL_3和LEVEL_FULL才建议使用Camera2进行采集,否则推荐采用兼容性更好的Camera1进行视频采集。

    //判断是否支持要求的等级
    // Returns true if the device supports the required hardware level, or better.
    boolean isHardwareLevelSupported(int supportLevel , int requiredLevel) {
        final int[] sortedHwLevels = {
                CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY,
                CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_EXTERNAL,
                CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
                CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL,
                CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_3
        };
        int deviceLevel = supportLevel  ; CameraCharacteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL);
        if (requiredLevel == deviceLevel) {
            return true;
        }

        for (int sortedlevel : sortedHwLevels) {
            if (sortedlevel == requiredLevel) {
                return true;
            } else if (sortedlevel == deviceLevel) {
                return false;
            }
        }
        return false; // Should never reach here
    }

    //初始化
    private  void initImageAvailableListener(){
        imageAvailableListener = new ImageReader.OnImageAvailableListener(){
            @Override
            public  void onImageAvailable(ImageReader var1){
                Image image = var1.acquireLatestImage() ;
                if(image== null){
                    return;
                }
                //得到图片 转成nv21格式
                byte [] date  = YUV_420_888toNV21(image);

                //  ByteBuffer buffer = image.getPlanes()[0].getBuffer();
                //  int  length = buffer.remaining();
                //  var bytes = ByteArray(length);
                // 转成 Bitmap
                //  ByteBuffer buffer = image.getPlanes()[0].getBuffer();
                //  byte[] b = new byte[buffer.capacity()];
                //    buffer.get(b, 0 , b.length);
                //   Bitmap bitmap = BitmapFactory.decodeByteArray(b, 0, b.length);
                image.close();
            }
        };
    }

    private  void createCameraDeviceCallback(){

        mStateCallback = new CameraDevice.StateCallback() {
            @Override
            public void onOpened(@NonNull CameraDevice camera) {
                mCameraDevice = camera;
                createCameraPreviewSession();

            }

            @Override
            public void onDisconnected(@NonNull CameraDevice camera) {
                camera.close();
                mCameraDevice = null;
            }

            @Override
            public void onError(@NonNull CameraDevice camera, int error) {
                camera.close();
                mCameraDevice = null;

            }
        };
    }

    public void openCamera(){
        mCameraManager = (CameraManager) getSystemService(CAMERA_SERVICE);

        if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
            // TODO: Consider calling
            //    ActivityCompat#requestPermissions
            // here to request the missing permissions, and then overriding
            //   public void onRequestPermissionsResult(int requestCode, String[] permissions,
            //                                          int[] grantResults)
            // to handle the case where the user grants the permission. See the documentation
            // for ActivityCompat#requestPermissions for more details.
            return;
        }

        createCameraDeviceCallback() ; //相机状态监听
        createImageReader(); //图像解析

        try {
            mCameraManager.openCamera(cameraIdFront, mStateCallback, null); //"0" 前置  "1"  后置  // handler null 主线程处理  否 创建个独立线程区处理
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    }

    //创建分析
    private void  createImageReader() {
        if (mImageReader != null) {
            mImageReader.close();
            mImageReader = null;
        }
        //mPreviewSize.getWidth()
        mImageReader = ImageReader.newInstance(getPreviewWidth(), getPreviewHeight(),
                ImageFormat.YUV_420_888, RECV_MAX_IMAGES);

        // imageReader = ImageReader.newInstance(1280, 720, ImageFormat.JPEG, 2)
        mImageReader.setOnImageAvailableListener(imageAvailableListener, null);
    }

    //创建预览会话
    private void createCameraPreviewSession() {
        try {

            final CaptureRequest.Builder previewRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
            previewRequestBuilder.addTarget(mPreviewSurface);
            previewRequestBuilder.addTarget(mImageReader.getSurface());

            
            //这几句可以放到 // 放到设置预览时连续捕获图像数据前 也可以
            // 设置自动对焦模式
            previewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
            // 设置自动曝光模式
            if(isFlashSupported()){ //如果支持就开启
                previewRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
            }
            //设置帧率
            if(mAverageFPS!=null){
                previewRequestBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, mAverageFPS);
            }
            // 开始显示相机预览
            mPreviewRequest = previewRequestBuilder.build();
            //
            // width 和 height 与 surfaceview 的相同
            //ImageReader 用于接收处理(拍照、录像)的相机流
            //SurfaceView 用于接收预览的相机流
            // ImageReader mImageReader = ImageReader.newInstance(width, height, ImageFormat.JPEG, 1);
            List<Surface> surfaces = new ArrayList<Surface>();
            surfaces.add(mPreviewSurface);
            surfaces.add(mImageReader.getSurface());
            //  surfaces.add(surfaceView.getHolder().getSurface());
            //  surfaces.add(mImageReader.getSurface());
            // Arrays.asList(surface)
            
            mCameraDevice.createCaptureSession(surfaces, new CameraCaptureSession.StateCallback() {

                @Override
                public void onConfigured(CameraCaptureSession cameraCaptureSession) {
                    mCaptureSession = cameraCaptureSession;
                    try {
                        
                        //这几句可以放到 // 放到设置预览时连续捕获图像数据 也可以
                        // 设置自动对焦模式
//                        previewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
//                        // 设置自动曝光模式
//                        if(isFlashSupported()){ //如果支持就开启
//                            previewRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
//                        }
//                        //设置帧率
//                        if(mAverageFPS!=null){
//                            previewRequestBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, mAverageFPS);
//                        }
//                        // 开始显示相机预览
//                        mPreviewRequest = previewRequestBuilder.build();
                        //
                        // 设置预览时连续捕获图像数据
                        mCaptureSession.setRepeatingRequest(mPreviewRequest, null, null);
                    } catch (CameraAccessException e) {
                        e.printStackTrace();
                    }


                }

                @Override
                public void onConfigureFailed(CameraCaptureSession cameraCaptureSession) {
                    Log.e("MainActivity", "onConfigureFailed");
                }
            }, null);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    }

    数据格式处理//
    //数据转化 抄别人的 //
    //Planar格式(P)的处理
    private static ByteBuffer getuvBufferWithoutPaddingP(ByteBuffer uBuffer, ByteBuffer vBuffer, int width, int height, int rowStride, int pixelStride){
        int pos = 0;
        byte []byteArray = new byte[height*width/2];
        for (int row=0; row<height/2; row++) {
            for (int col=0; col<width/2; col++) {
                int vuPos = col*pixelStride + row*rowStride;
                byteArray[pos++] = vBuffer.get(vuPos);
                byteArray[pos++] = uBuffer.get(vuPos);
            }
        }
        ByteBuffer bufferWithoutPaddings=ByteBuffer.allocate(byteArray.length);
        // 数组放到buffer中
        bufferWithoutPaddings.put(byteArray);
        //重置 limit 和postion 值否则 buffer 读取数据不对
        bufferWithoutPaddings.flip();
        return bufferWithoutPaddings;
    }
    //Semi-Planar格式(SP)的处理和y通道的数据
    private static ByteBuffer getBufferWithoutPadding(ByteBuffer buffer, int width, int rowStride, int times,boolean isVbuffer){
        if(width == rowStride) return buffer;  //没有buffer,不用处理。
        int bufferPos = buffer.position();
        int cap = buffer.capacity();
        byte []byteArray = new byte[times*width];
        int pos = 0;
        //对于y平面,要逐行赋值的次数就是height次。对于uv交替的平面,赋值的次数是height/2次
        for (int i=0;i<times;i++) {
            buffer.position(bufferPos);
            //part 1.1 对于u,v通道,会缺失最后一个像u值或者v值,因此需要特殊处理,否则会crash
            if(isVbuffer && i==times-1){
                width = width -1;
            }
            buffer.get(byteArray, pos, width);
            bufferPos+= rowStride;
            pos = pos+width;
        }

        //nv21数组转成buffer并返回
        ByteBuffer bufferWithoutPaddings=ByteBuffer.allocate(byteArray.length);
        // 数组放到buffer中
        bufferWithoutPaddings.put(byteArray);
        //重置 limit 和postion 值否则 buffer 读取数据不对
        bufferWithoutPaddings.flip();
        return bufferWithoutPaddings;
    }

    private static byte[] YUV_420_888toNV21(Image image) {
        int width =  image.getWidth();
        int height = image.getHeight();
        ByteBuffer yBuffer = getBufferWithoutPadding(image.getPlanes()[0].getBuffer(), image.getWidth(), image.getPlanes()[0].getRowStride(),image.getHeight(),false);
        ByteBuffer vBuffer;
        //part1 获得真正的消除padding的ybuffer和ubuffer。需要对P格式和SP格式做不同的处理。如果是P格式的话只能逐像素去做,性能会降低。
        if(image.getPlanes()[2].getPixelStride()==1){ //如果为true,说明是P格式。
            vBuffer = getuvBufferWithoutPaddingP(image.getPlanes()[1].getBuffer(), image.getPlanes()[2].getBuffer(),
                    width,height,image.getPlanes()[1].getRowStride(),image.getPlanes()[1].getPixelStride());
        }else{
            vBuffer = getBufferWithoutPadding(image.getPlanes()[2].getBuffer(), image.getWidth(), image.getPlanes()[2].getRowStride(),image.getHeight()/2,true);
        }

        //part2 将y数据和uv的交替数据(除去最后一个v值)赋值给nv21
        int ySize = yBuffer.remaining();
        int vSize = vBuffer.remaining();
        byte[] nv21;
        int byteSize = width*height*3/2;
        nv21 = new byte[byteSize];
        yBuffer.get(nv21, 0, ySize);
        vBuffer.get(nv21, ySize, vSize);

        //part3 最后一个像素值的u值是缺失的,因此需要从u平面取一下。
        ByteBuffer uPlane = image.getPlanes()[1].getBuffer();
        byte lastValue = uPlane.get(uPlane.capacity() - 1);
        nv21[byteSize - 1] = lastValue;
        return nv21;
    }

    
    //悬浮窗口 移动 更新位置
    private class FloatingOnTouchListener implements View.OnTouchListener {
        private int x;
        private int y;

        @Override
        public boolean onTouch(View view, MotionEvent event) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    x = (int) event.getRawX();
                    y = (int) event.getRawY();
                    break;
                case MotionEvent.ACTION_MOVE:
                    int nowX = (int) event.getRawX();
                    int nowY = (int) event.getRawY();
                    int movedX = nowX - x;
                    int movedY = nowY - y;
                    x = nowX;
                    y = nowY;
                    layoutParams.x = layoutParams.x + movedX;
                    layoutParams.y = layoutParams.y + movedY;
                    windowManager.updateViewLayout(view, layoutParams);
                    break;
                default:
                    break;
            }
            return true;
        }
    }

    
    //释放 与切换
    private void  releaseCamera() {
        if(mCaptureSession!=null){
            mCaptureSession.close();
            mCaptureSession = null ;
        }
        if(mCameraDevice !=null){
            mCameraDevice.close();
            mCameraDevice = null;
        }
        if(mImageReader !=null){
            mImageReader.close();
            mImageReader = null ;
        }
//        canExchangeCamera = false
    }

    private void  releaseThread() {
        // handlerThread.quitSafely()
    }


    private void exchangeCamera() {
//        if (mCameraDevice == null || !canExchangeCamera || !mTextureView.isAvailable) return
//
//                mCameraFacing = if (mCameraFacing == CameraCharacteristics.LENS_FACING_FRONT)
//            CameraCharacteristics.LENS_FACING_BACK
//        else
//            CameraCharacteristics.LENS_FACING_FRONT
//
//                    mPreviewSize = Size(PREVIEW_WIDTH, PREVIEW_HEIGHT) //重置预览大小
//        releaseCamera()
//        initCameraInfo()
    }
    //
    //权限
//  <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
//  <uses-permission android:name="android.permission.CAMERA" />
//  <uses-permission android:name="android.permission.INTERNET" />
    //硬件加速
     // android:hardwareAccelerated="true"
    //使用 useAndroidX
    //android.useAndroidX=true
    //增加依赖
    //implementation 'androidx.core:core:1.2.0'   跟下面3句有关系,不增加,下面的找不到
    // import androidx.annotation.NonNull;
    //import androidx.core.app.ActivityCompat;
    //import androidx.core.content.ContextCompat;
    ///
    //  unity 导出 Plugin Version(3.6.0)    Gradle Version(6.1.1)  这里用
    //   compileSdkVersion 33
    //   buildToolsVersion '33.0.0'
    //   不支持 所以需要提升下
    //  Android Gradle Plugin Version
//3.6.0   ---->   4.1.1
    //   Gradle Version
//6.1.1   ----> 6.9
    //
    //启动过程
    //1: 申请权限,得到相机与屏幕 信息 多出相依处理
    //applyForPermission()   //放到 onCreate 里就OK了
    //有权限 dosomething
    //2:dosomething 里  初始化相机
    //createFloatingWindowLayout()  初始化各种监听 等
    //3: 点击事件 或其他地方调用
    //addFloatingWindowView   触发
    // TextureView.SurfaceTextureListener() 的
    //    public void onSurfaceTextureAvailable  从而   启动相机
    //4: 启动相机
    //openCamera   在 mCameraManager.openCamera 前 回调函数都设置好
    //createCameraDeviceCallback() ; //相机状态监听回调
    // createImageReader(); //图像解析
    //触发 createCameraPreviewSession
    //5  创建预览会话
    // 预览 解析  加入会话申请中
}

camera_display.xml

<?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">

    <TextureView
        android:id="@+id/preview"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

    </TextureView>

</LinearLayout>

AndroidManifest.xml
增加权限



修改
android:hardwareAccelerated=“false” 为 “true”

<?xml version="1.0" encoding="utf-8"?>
<!-- GENERATED BY UNITY. REMOVE THIS COMMENT TO PREVENT OVERWRITING WHEN EXPORTING AGAIN-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.unity3d.player" xmlns:tools="http://schemas.android.com/tools">
  <application>
    <activity android:name="com.unity3d.player.UnityPlayerActivity" android:theme="@style/UnityThemeSelector" android:screenOrientation="landscape" android:launchMode="singleTask" android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|orientation|screenLayout|uiMode|screenSize|smallestScreenSize|fontScale|layoutDirection|density" android:hardwareAccelerated="true">
      <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
      </intent-filter>
      <meta-data android:name="unityplayer.UnityActivity" android:value="true" />
      <meta-data android:name="android.notch_support" android:value="true" />
    </activity>
    <meta-data android:name="unity.splash-mode" android:value="0" />
    <meta-data android:name="unity.splash-enable" android:value="True" />
    <meta-data android:name="notch.config" android:value="portrait|landscape" />
    <meta-data android:name="unity.build-id" android:value="df8d9e41-33b6-4006-847a-7fc0aeb2ca09" />
  </application>
  <uses-feature android:glEsVersion="0x00030000" />
  <uses-feature android:name="android.hardware.vulkan.version" android:required="false" />
  <uses-feature android:name="android.hardware.touchscreen" android:required="false" />
  <uses-feature android:name="android.hardware.touchscreen.multitouch" android:required="false" />
  <uses-feature android:name="android.hardware.touchscreen.multitouch.distinct" android:required="false" />

  <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
  <uses-permission android:name="android.permission.CAMERA" />
  <uses-permission android:name="android.permission.INTERNET" />
</manifest>

unityLibrary 下 gradle.properties
增加 android.useAndroidX=true
在这里插入图片描述
unityLibrary 下 build.gradle
增加依赖库

    //by eh  add   这句一定要增加 下面2句可以不加
    implementation 'androidx.core:core:1.2.0'
    1.1.0-beta01
    //下面2句没加一样跑,不知道在上面那里是否已经包含了
    def camera2_version = "1.2.0"
    implementation "androidx.camera:camera-core:${camera2_version}"
    implementation "androidx.camera:camera-camera2:${camera2_version}"

在这里插入图片描述
文件内容如下

// GENERATED BY UNITY. REMOVE THIS COMMENT TO PREVENT OVERWRITING WHEN EXPORTING AGAIN

apply plugin: 'com.android.library'

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])

    //by eh  add
    implementation 'androidx.core:core:1.2.0'
    1.1.0-beta01
    //下面2句没加一样跑,不知道在上面那里是否已经包含了
    def camera2_version = "1.2.0"
    implementation "androidx.camera:camera-core:${camera2_version}"
    implementation "androidx.camera:camera-camera2:${camera2_version}"
}

android {
    compileSdkVersion 33
    buildToolsVersion '33.0.0'

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

    defaultConfig {
        minSdkVersion 23
        targetSdkVersion 30
        ndk {
            abiFilters 'arm64-v8a'
        }
        versionCode 1
        versionName '1.0'
        consumerProguardFiles 'proguard-unity.txt'
    }

    lintOptions {
        abortOnError false
    }

    aaptOptions {
        noCompress = ['.ress', '.resource', '.obb'] + unityStreamingAssets.tokenize(', ')
        ignoreAssetsPattern = "!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~"
    }

    packagingOptions {
        doNotStrip '*/arm64-v8a/*.so'
    }
}

def getSdkDir() {
    Properties local = new Properties()
    local.load(new FileInputStream("${rootDir}/local.properties"))
    return local.getProperty('sdk.dir')
}

def BuildIl2Cpp(String workingDir, String targetDirectory, String architecture, String abi, String configuration) {
    exec {
        commandLine(workingDir + "/src/main/Il2CppOutputProject/IL2CPP/build/deploy/netcoreapp3.1/il2cpp.exe",
            "--compile-cpp",
            "--libil2cpp-static",
            "--platform=Android",
            "--architecture=" + architecture,
            "--configuration=" + configuration,
            "--outputpath=" + workingDir + targetDirectory + abi + "/libil2cpp.so",
            "--cachedirectory=" + workingDir + "/build/il2cpp_"+ abi + "_" + configuration + "/il2cpp_cache",
            "--additional-include-directories=" + workingDir + "/src/main/Il2CppOutputProject/IL2CPP/external/bdwgc/include",
            "--additional-include-directories=" + workingDir + "/src/main/Il2CppOutputProject/IL2CPP/libil2cpp/include",
            "--tool-chain-path=" + android.ndkDirectory,
            "--map-file-parser=" + workingDir + "/src/main/Il2CppOutputProject/IL2CPP/MapFileParser/MapFileParser.exe",
            "--generatedcppdir=" + workingDir + "/src/main/Il2CppOutputProject/Source/il2cppOutput",
            "--baselib-directory=" + workingDir + "/src/main/jniStaticLibs/" + abi,
            "--dotnetprofile=unityaot")
        environment "ANDROID_SDK_ROOT", getSdkDir()
    }
    delete workingDir + targetDirectory + abi + "/libil2cpp.sym.so"
    ant.move(file: workingDir + targetDirectory + abi + "/libil2cpp.dbg.so", tofile: workingDir + "/symbols/" + abi + "/libil2cpp.so")
}

android {
    task BuildIl2CppTask {
        doLast {
              BuildIl2Cpp(projectDir.toString().replaceAll('\\\\', '/'), '/src/main/jniLibs/', 'ARM64', 'arm64-v8a', 'Release');
        }
    }
    afterEvaluate {
        if (project(':unityLibrary').tasks.findByName('mergeDebugJniLibFolders'))
            project(':unityLibrary').mergeDebugJniLibFolders.dependsOn BuildIl2CppTask
        if (project(':unityLibrary').tasks.findByName('mergeReleaseJniLibFolders'))
            project(':unityLibrary').mergeReleaseJniLibFolders.dependsOn BuildIl2CppTask
    }
    sourceSets {
        main {
            jni.srcDirs = ["src/main/Il2CppOutputProject"]
        }
    }
}

launcher 下 build.gradle
只修改了目标版本号跟 unityLibrary 下的一致
在这里插入图片描述

6 运行结果
在这里插入图片描述
7 后续增加其他功能
unity 跟 AS DEMO工程 DEMO下载地址
觉得有用 ,点个赞,加个关注

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值