主頁 > 移動端開發 > Camera2 完整相機demo(預覽拍照錄制視頻) 菜鳥教程

Camera2 完整相機demo(預覽拍照錄制視頻) 菜鳥教程

2021-09-29 08:17:30 移動端開發

一 .功能介紹(UI布局)

打開camera app

首先會進入拍照模式的預覽界面:

拍照的預覽界面

總共就四個控制元件

1.整體的預覽控制元件TextureView

2.左下角的縮略圖ImageView

3.中間的拍照按鈕ImageButton

4.切換攝像頭的ImageButton

主要是兩個Fragment 通過ViewPage 左右滑動切換拍照模式跟錄像模式具體實作在后面,在我的Demo中向左滑動切換到錄像模式

進入錄像的預覽模式:

錄像的預覽界面

總共就五個控制元件

1.整體的預覽控制元件TextureView

2.左下角的縮略圖ImageView

3.中間的拍照按鈕ImageButton

4.切換攝像頭的ImageButton

5.頂部的計時器Chronometer

二 .UI的代碼部分

1.首先是 activity_main.xml中

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

    <androidx.viewpager.widget.ViewPager
        android:layout_gravity="center"
        android:id="@+id/change_page"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content">

<!--標題 <androidx.viewpager.widget.PagerTitleStrip
            android:layout_gravity="top"
            android:id="@+id/page_title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            >-->


    </androidx.viewpager.widget.ViewPager>

</RelativeLayout>

主界面主要是注冊了一個ViewPager 注釋的部分是之前準備的標題欄,是想通過標題提示是錄像界面還是拍照界面,但是自己沒有找到把標題欄背景變成透明的方法于是就拋棄了,我方在這里如果后續找到的話及時更新.

2.其次是 Mainactivity.java中

package com.example.camera;

import android.graphics.Color;
import android.os.Build;
import android.os.Bundle;
import android.view.View;

import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;
import androidx.viewpager.widget.ViewPager;

import java.util.ArrayList;
import java.util.List;

public class MainActivity extends AppCompatActivity {
    private ViewPager change_page;    //拍照錄像切換
    private List<Fragment> layoutList;   //布局集合(拍照錄像切換)
    //private List<String> titleList;      //標題集合(拍照錄像切換)
    private MyPagerAdapter myPagerAdapter;


    //初始化視圖界面(拍照/攝像)
    private void initView(){
        change_page = findViewById(R.id.change_page);
        layoutList = new ArrayList<>();
        layoutList.add(new TakePictureFragment());
        layoutList.add(new RecorderVideoFragment());

//        titleList = new ArrayList<>();
//        titleList.add("拍照");
//        titleList.add("錄像");
        //設定配接器
        myPagerAdapter = new MyPagerAdapter(getSupportFragmentManager(), layoutList);
        change_page.setAdapter(myPagerAdapter);
    }

    //活動的創建
    @Override
    protected void onCreate(Bundle saveInstanceState) {
        super.onCreate(saveInstanceState);
        setContentView(R.layout.activity_main);
        initView();

        //隱藏通知欄狀態欄
        if (Build.VERSION.SDK_INT >= 21) {
            View decorView=getWindow().getDecorView();//獲取當前界面的decorView
            int option = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
                    |View.SYSTEM_UI_FLAG_FULLSCREEN//隱藏狀態欄
                    |View.SYSTEM_UI_FLAG_LAYOUT_STABLE//保持整個View的穩定,使其不會隨著SystemUI的變化而變化;
                    |View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION//讓導航欄懸浮在Activity上
                  //  |View.SYSTEM_UI_FLAG_HIDE_NAVIGATION//隱藏導航欄
                    |View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;//沉浸模式且狀態欄和導航欄出現片刻后會自動隱藏
            decorView.setSystemUiVisibility(option);
            getWindow().setStatusBarColor(Color.TRANSPARENT);//設定透明顏色
            getWindow().setNavigationBarColor(Color.TRANSPARENT);
        }
        ActionBar actionBar=getSupportActionBar();
        actionBar.hide();
    }
    
    protected  void onDestroy() {
        super.onDestroy();
        TakePictureFragment.closeCamera();
    }
}

主活動中:

1.做了初始化視圖界面添加兩個碎片TakePictureFragment和RecorderVideoFragment

2.因為ViewPage要用到PagerAdapter 將默認的界面設定成了拍照預覽界面

3.onCreate中做了個沉浸式通知欄(隱藏狀態欄和通知欄)

4.onDestroy中呼叫了TakePictureFrament中的closeCamera

5.注釋掉的部分同上是標題欄

3.MyPagerAdapter.java中

package com.example.camera;

import android.view.ViewGroup;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentPagerAdapter;

import java.util.List;

public  class MyPagerAdapter extends FragmentPagerAdapter {
    private List<Fragment> layoutList;
//    private List<String> titleList;
    public MyPagerAdapter(FragmentManager manager, List<Fragment> layoutList ){
        super(manager);
        this.layoutList = layoutList;
//        this.titleList = titleList;
    }
    @Override
    public int getCount() {
        // 頁面數
        return layoutList.size();
    }

    @NonNull
    @Override
    public Fragment getItem(int position) {
        return layoutList.get(position);
    }

    @Override
    public void setPrimaryItem(ViewGroup container, int position, Object object) {
        super.setPrimaryItem(container, position, object);
    }
}

這個類繼承FragmentPageAdapter跟ViewPage聯合使用,注:ViewPage跟activity也可以不一定是需要Fragment主要的功能就是通過左右滑動來切換fragment

3.fragment_take_picture.xml

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

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

    <ImageButton
        android:background="@drawable/shape_white_ring"
        android:id="@+id/takePicture"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/shape_take_photo"
        android:layout_alignParentBottom="true"
        android:layout_centerHorizontal="true"
        android:layout_marginBottom="50dp"
        />

    <ImageView
        android:id="@+id/image_show"
        android:layout_width="50dp"
        android:layout_height="50dp"
        android:layout_alignParentBottom="true"
        android:layout_marginLeft="50dp"
        android:layout_marginBottom="50dp" />
    <ImageButton
        android:id="@+id/change"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@drawable/ic_baseline_flip_camera_android_24"
        android:layout_alignParentBottom="true"
        android:layout_marginRight="50dp"
        android:layout_marginBottom="50dp"
        android:layout_alignParentRight="true"
        />
</RelativeLayout>

4 TakePpictureFragment.java中

package com.example.camera;

import android.Manifest;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.ImageFormat;
import android.graphics.Matrix;
import android.graphics.RectF;
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.TotalCaptureResult;
import android.hardware.camera2.params.MeteringRectangle;
import android.hardware.camera2.params.StreamConfigurationMap;
import android.media.Image;
import android.media.ImageReader;
import android.media.MediaMetadataRetriever;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.util.Log;
import android.util.Size;
import android.util.SparseIntArray;
import android.view.LayoutInflater;
import android.view.Surface;
import android.view.TextureView;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

public class TakePictureFragment extends Fragment implements View.OnClickListener {
    private static final String TAG = "TakePictureFragment";

    private static final SparseIntArray ORIENTATION = new SparseIntArray();

    static {
        //手機ROTATION逆時針旋轉
        ORIENTATION.append(Surface.ROTATION_0, 90);
        ORIENTATION.append(Surface.ROTATION_90, 0);
        ORIENTATION.append(Surface.ROTATION_180, 270);
        ORIENTATION.append(Surface.ROTATION_270, 180);
    }

    private TextureView textureView;  //預覽框控制元件
    private ImageButton takePicture;    //拍照按鈕
    private ImageButton change;      //前后攝像頭切換按鈕
    private ImageView mImageView;     // 縮略圖顯示
    private String mCameraId;         // 攝像頭Id
    private Size mPreviewSize;      //獲取解析度
    private ImageReader mImageReader;  //圖片閱讀器
    private static CameraDevice mCameraDevice;   //攝像頭設備
    private static CameraCaptureSession mCaptureSession;   //獲取會話
    private CaptureRequest mPreviewRequest;      //獲取預覽請求
    private CaptureRequest.Builder mPreviewRequestBuilder;   //獲取到預覽請求的Builder通過它創建預覽請求
    private Surface mPreviewSurface;  //預覽顯示圖

    //權限申請
    private String[] permissions = {Manifest.permission.CAMERA, Manifest.permission.READ_EXTERNAL_STORAGE,
            Manifest.permission.WRITE_EXTERNAL_STORAGE,Manifest.permission.RECORD_AUDIO};
    private List<String> permissionList = new ArrayList();

    private ArrayList<String> imageList = new ArrayList<>();  //圖片集合
    protected boolean isCreated=false;   //Fragment是否創建成功
    private boolean isVisible;   //Fragment是否可見


    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
                             @Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.d(TAG, "onCreateView: success");
        View view = inflater.inflate(R.layout.fragment_take_picture, container, false);
        //注冊監聽控制元件
        initView(view);
        textureView.setSurfaceTextureListener(textureListener);   //surfaceView回呼里面配置相機打開相機
        takePicture.setOnClickListener(this);      //拍照監聽
        mImageView.setOnClickListener(this);   //縮略圖監聽
        change.setOnClickListener(this);     //攝像頭切換監聽
        getPermission();         //申請權限
        //顯示最后一張圖
        isCreated = true;     //Fragment View 創建成功
        return view;      //顯示當前View
    }

    // 第一步:獲取權限

    /**
     * 獲取拍照和讀寫權限
     */
    private void getPermission() {
        Log.d(TAG, "getPermission: success");
        //版本判斷 當手機系統大于23時,才有必要去判斷權限是否獲取
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            //權限是否已經 授權 GRANTED-授權  DINIED-拒絕
            for (String permission : permissions) {
                //檢查權限是否全部授予
                if (ContextCompat.checkSelfPermission(getContext(), permission) != PackageManager.PERMISSION_GRANTED) {
                    //如果沒有就添加到權限集合
                    permissionList.add(permission);
                }
            }
            //是慷訓傳ture
            if (!permissionList.isEmpty()) {
                requestPermissions(permissionList.toArray(new String[permissionList.size()]), 1);
            } else {
                //表示全都授權了
                textureView.setSurfaceTextureListener(textureListener);
                //顯示最后一張圖片  最新
                setLastImagePath();
            }
        }
    }
    //權限回呼
    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] mPermissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        Log.d(TAG, "onRequestPermissionsResult: success");
        if (requestCode == 1) {
            //權限請求失敗
            if (grantResults.length > 0) {
                //存放沒授權的權限
                List<String> deniedPermissions = new ArrayList<>();
                for (int i = 0; i < grantResults.length; i++) {
                    int grantResult = grantResults[i];
                    String permission = permissions[grantResult];
                    if (grantResult != PackageManager.PERMISSION_GRANTED) {
                        deniedPermissions.add(permission);
                    }
                }
                if (deniedPermissions.isEmpty()) {
                    //說明都授權了  打開相機
                    openCamera();
                    //顯示最新的一張圖片或者視頻的第一幀
                    setLastImagePath();
                } else {
                    // 繼續申請權限
                    getPermission();
                }
            }
        }
    }
    //判斷Fragment是否可見
    @Override
    public void setUserVisibleHint(boolean isVisibleToUser) {
        super.setUserVisibleHint(isVisibleToUser);
        //判斷是否是第一次創建
        if (!isCreated) {
            return;
        }
        //如果可見
        if (isVisibleToUser) {
            isVisible = true;
            //顯示第一張照片
            setLastImagePath();
            //如果textureView 可用
            if (textureView.isAvailable()) {
                //打開相機
                openCamera();
            } else {
                //先配置相機再打開相機
                textureView.setSurfaceTextureListener(textureListener);
            }
        } else {
            //當切換成錄像時,將當前fragment 置為不可見
            Log.d(TAG, TAG + " releaseCamera");
            isVisible = false;
            closeCamera();
        }
    }

    @Override
    public void onResume() {
        super.onResume();
        //如果fragment 可見
        if (isVisible) {
            //textureView 可用
            if (textureView.isAvailable()) {
                //打開相機
                openCamera();
            } else {
                //設定相機引數
                textureView.setSurfaceTextureListener(textureListener);
            }
        }
    }
    //注冊視圖上監聽控制元件ID
    private void initView(View view) {
        Log.d(TAG, "initView: success");
        //預覽框控制元件
        textureView = view.findViewById(R.id.textureView);
        //拍照控制元件
        takePicture = view.findViewById(R.id.takePicture);
        //縮略圖控制元件
        mImageView = view.findViewById(R.id.image_show);
        //前后攝像頭切換控制元件
        change = view.findViewById(R.id.change);
    }
    @Override
    public void onClick(View view) {
        Log.d(TAG, "onClick: success");
        switch (view.getId()) {
            case R.id.takePicture:
                //拍照
                takePhoto();
                break;
            case R.id.change:
                //切換攝像頭
                changeCamera();
                break;
            case R.id.image_show:
                //打開相冊
                openAlbum();
                break;
        }
    }


    //第二步  寫回呼函式
    // 1.  SurfaceView狀態回呼
    TextureView.SurfaceTextureListener textureListener = new TextureView.SurfaceTextureListener() {
        @Override
        //textureView.isAvailable()為ture
        public void onSurfaceTextureAvailable(@NonNull SurfaceTexture surfaceTexture, int width, int height) {
            Log.d(TAG, "onSurfaceTextureAvailable: success");
            setupCamera();
//            configureTransform(width, height);   //旋轉
            openCamera();             //打開相機
        }

        @Override
        public void onSurfaceTextureSizeChanged(@NonNull SurfaceTexture surfaceTexture, int width, int height) {
//            configureTransform(width, height);
        }

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

        @Override
        public void onSurfaceTextureUpdated(@NonNull SurfaceTexture surfaceTexture) {
        }
    };

    // 2.  攝像頭狀態回呼
    private final CameraDevice.StateCallback stateCallback = new CameraDevice.StateCallback() {
        //打開成功獲取到camera設備
        @Override
        public void onOpened(@NonNull CameraDevice cameraDevice) {
            Log.d(TAG, "onOpened: success");
            mCameraDevice = cameraDevice;
            //開啟預覽
            startPreview();
        }

        //打開失敗
        @Override
        public void onDisconnected(@NonNull CameraDevice cameraDevice) {
            Toast.makeText(getContext(), "攝像頭設備連接失敗", Toast.LENGTH_SHORT).show();
        }

        //打開錯誤
        @Override
        public void one rror(@NonNull CameraDevice cameraDevice, int i) {
            Toast.makeText(getContext(), "攝像頭設備連接出錯", Toast.LENGTH_SHORT).show();
        }
    };

    // 3.實作PreviewCallback  拍照時呼叫
    private CameraCaptureSession.CaptureCallback mPreviewCaptureCallback = new CameraCaptureSession.CaptureCallback() {
        @Override
        // 一旦捕獲完成
        public void onCaptureCompleted(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull TotalCaptureResult result) {
            super.onCaptureCompleted(session, request, result);
        }
    };

    //第三步  設定(配置)相機
    //設定攝像機 id  引數
    private void setupCamera() {
        Log.d(TAG, "setupCamera: success");
        //獲取攝像頭的管理者CameraManager
        CameraManager manager = (CameraManager) getActivity().getSystemService(Context.CAMERA_SERVICE);
        try {
            //遍歷所有攝像頭
            for (String cameraId : manager.getCameraIdList()) {
                CameraCharacteristics characteristics = manager.getCameraCharacteristics(cameraId); //獲取攝像機的特征
                //默認打開后置  - 忽略前置 LENS(鏡頭)
                if (characteristics.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT)
                {
                    continue;
                }
                //獲取StreamConfigurationMap,他是管理攝像頭支持的所有輸出格式
                StreamConfigurationMap map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
                mPreviewSize = getOptimalSize(map.getOutputSizes(SurfaceTexture.class),
                        720, 960); //獲取最佳的預覽大小
                //進入回呼設定相機然后打開
                textureView.setSurfaceTextureListener(textureListener);
                mCameraId = cameraId;
                break;
            }
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    }
    //旋轉螢屏
    private void configureTransform(int viewWidth, int viewHeight) {
        Log.d(TAG, "configureTransform: success");
        if (textureView == null || mPreviewSize == null) {
            return;
        }
        int rotation = getActivity().getWindowManager().getDefaultDisplay().getRotation();
        Log.i("TAGggg", "rotation: "+rotation);
        Matrix matrix = new Matrix();
        RectF viewRect = new RectF(0, 0, viewWidth, viewHeight);
        RectF bufferRect = new RectF(0, 0, mPreviewSize.getHeight(), mPreviewSize.getWidth());
        float centerX = viewRect.centerX();
        float centerY = viewRect.centerY();
        if (rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270) {
            bufferRect.offset(centerX - bufferRect.centerX(), centerY - bufferRect.centerY());
            matrix.setRectToRect(viewRect, bufferRect, Matrix.ScaleToFit.FILL);
            float scale = Math.max((float) viewHeight / mPreviewSize.getHeight(),
                    (float) viewWidth / mPreviewSize.getWidth());
            matrix.postScale(scale, scale, centerX, centerY);
            matrix.postRotate(90 * (rotation - 2), centerX, centerY);
        } else if (rotation == Surface.ROTATION_180) {
            Log.i("TAGggg", "rotation  --- : "+rotation);
            matrix.postRotate(180, centerX, centerY);
        }
        textureView.setTransform(matrix);
    }

    //選擇sizeMap中大于并且接近width和height的size
    private Size getOptimalSize(Size[] sizeMap, int width, int height) {
        Log.d(TAG, "getOptimalSize: success");
        List<Size> sizeList = new ArrayList<>();
        for (Size option : sizeMap) {
            //當width > height
            if (width > height) {
                //選取官渡大于surface的寬度并且選取的高度大于surface的高度
                if (option.getWidth() > width && option.getHeight() > height) {
                    //符合的添加到sizeList
                    sizeList.add(option);
                }
            } else {
                //如果選擇寬度大于surface的高度并且選擇的高度大于surface的寬度
                if (option.getWidth() > height && option.getHeight() > width) {
                    //符合的添加到sizeList
                    sizeList.add(option);
                }
            }
        }
        if (sizeList.size() > 0) {
            return Collections.min(sizeList, new Comparator<Size>() {
                @Override
                public int compare(Size size, Size t1) {
                    return Long.signum(size.getWidth() * size.getHeight() - t1.getWidth() * t1.getHeight());
                }
            });
        }
        return sizeMap[0];

    }

    //創建預覽請求的Builder (TEMPLATE_PREVIEW表示預覽請求)
    private void setPreviewRequestBuilder() {
        Log.d(TAG, "setPreviewRequestBuilder: success");
        try {
            //通過cameraDevice獲取到預覽請求的Builder
            mPreviewRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
        //設定預覽的顯示圖
        mPreviewRequestBuilder.addTarget(mPreviewSurface);
        MeteringRectangle[] meteringRectangles = mPreviewRequestBuilder.get(CaptureRequest.CONTROL_AE_REGIONS);
        if (meteringRectangles != null && meteringRectangles.length > 0) {
            Log.d(TAG,"PreviewRequestBuilder: AF_REGIONS");
        }
        mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_MODE_AUTO);
        mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_IDLE);
    }
    // 重復預覽
    private void repeatPreview() {
        Log.d(TAG, "repeatPreview: success");
        mPreviewRequestBuilder.setTag(TAG);
        //通過預覽請求的builder的.build獲取到預覽請求
        mPreviewRequest = mPreviewRequestBuilder.build();
        //設定反復捕獲會話的請求,這樣預覽界面就會一直有資料顯示
        try {
            //第一個引數就是預覽求情,第二個引數是PreviewCallback,第三個是處理的執行緒
            mCaptureSession.setRepeatingRequest(mPreviewRequest, mPreviewCaptureCallback, null);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    }
    //第四步    打開相機
    private void openCamera() {
        Log.d(TAG, "openCamera: success");
        //獲取攝像頭的管理者 CameraManager
        CameraManager manager = (CameraManager) getActivity().getSystemService(Context.CAMERA_SERVICE);
        //檢查權限
        try {
            if (ActivityCompat.checkSelfPermission(getContext(), Manifest.permission.CAMERA) !=
                    PackageManager.PERMISSION_GRANTED) {
                return;
            } else {
                //通過manager.openCamera(id,cameraStateCallback,處理的執行緒)
                manager.openCamera(mCameraId, stateCallback, null);
            }
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    }

    //  關閉相機
    public static void closeCamera() {
        Log.d(TAG, "closeCamera: success");
        if (mCaptureSession != null) {
            mCaptureSession.close();
            mCaptureSession = null;
        }
        if (mCameraDevice != null) {
            mCameraDevice.close();
            mCameraDevice = null;
        }
    }

    //第五步 開啟相機預覽
    private void startPreview() {
        Log.d(TAG, "startPreview: success");
        //設定圖片閱讀器
        setupImageReader();
        SurfaceTexture mSurfaceTexture = textureView.getSurfaceTexture();
        //設定TextureView的緩沖區大小
        mSurfaceTexture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());
        //獲取Surface顯示預覽資料
        mPreviewSurface = new Surface(mSurfaceTexture);
        try {
            //創建預覽請求的Builder
            setPreviewRequestBuilder();
            //通過CameraDevice創建相機捕捉會話,第一個引數是捕獲資料的輸出Surface串列,第二個引數是CameraCaptureSession的狀態回呼介面,
            // 當他創建好后會回呼onCconfigured方法
            //第三個引數用來確定Callback在那個執行緒執行,null表示在當前執行緒執行
            mCameraDevice.createCaptureSession(Arrays.asList(mPreviewSurface, mImageReader.getSurface()),
                    new CameraCaptureSession.StateCallback() {
                @Override
                public void onConfigured(@NonNull CameraCaptureSession cameraCaptureSession) {
                    mCaptureSession = cameraCaptureSession;
                    repeatPreview();
                }

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

    //第七步 設定圖片閱讀
    private void setupImageReader() {
        Log.d(TAG, "setupImageReader: success");
        //前三個引數分別是需要的尺寸和格式,最后一個引數代表每次最多獲取幾幀資料
        mImageReader = ImageReader.newInstance(mPreviewSize.getWidth(), mPreviewSize.getHeight(), ImageFormat.JPEG, 1);
        //監聽ImageReader的事件,當有影像流資料可用時會回呼onImageAvailable
        mImageReader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() {
            @Override
            public void onImageAvailable(ImageReader imageReader) {
                Toast.makeText(getContext(), "圖片已保存", Toast.LENGTH_SHORT).show();
                //獲得mage
                Image image = imageReader.acquireNextImage();

                //開啟執行緒一部保存圖片
                ImageSaver imageSaver = new ImageSaver(getContext(), image);
                new Thread(imageSaver).start();

            }
        }, null);
    }

    // 第八步 拍照
    private void takePhoto() {
        Log.d(TAG, "takePhoto: success");
        try {
            //首先創建拍照的請求 CaptureRequest
            final CaptureRequest.Builder mCaptureBuilder = mCameraDevice.createCaptureRequest
                    (CameraDevice.TEMPLATE_STILL_CAPTURE);
            //獲取螢屏方向
            int rotation = getActivity().getWindowManager().getDefaultDisplay().getRotation();
            //獲取到當前預覽視窗的圖
            mCaptureBuilder.addTarget(mImageReader.getSurface());
            //設定拍照方向
            if (mCameraId.equals("1")) {
                rotation = 2;
            }
            //設定圖片的方向    因為默認的是橫屏   我們使用手機一般是豎屏所以需要處理
            mCaptureBuilder.set(CaptureRequest.JPEG_ORIENTATION, ORIENTATION.get(rotation));
            //停止預覽
            mCaptureSession.stopRepeating();
            //開始拍照,然后回呼上面的介面重啟預覽,因為mCaptureBuilder設定ImageReader作為target,
            // 所以會自動回呼ImageReader的onImageAvailable()方法保存圖片
            CameraCaptureSession.CaptureCallback captureCallback = new CameraCaptureSession.CaptureCallback() {
                @Override
                public void onCaptureCompleted(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request,
                                               @NonNull TotalCaptureResult result) {
                    super.onCaptureCompleted(session, request, result);
                    repeatPreview();
                }
            };
            mCaptureSession.capture(mCaptureBuilder.build(), captureCallback, null);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    }
    // 第九步 創建子執行緒保存圖片
    public class ImageSaver implements Runnable {
        private Image mImage;//圖片
        private Context mContext;
        public ImageSaver(Context context, Image image) {
            Log.d(TAG, "ImageSaver: success");
            mImage = image;
            mContext = context;
        }
        @Override
        public void run() {
            //將照片轉位元組
            ByteBuffer buffer = mImage.getPlanes()[0].getBuffer();
            byte[] data = new byte[buffer.remaining()];
            buffer.get(data);
            String path = Environment.getExternalStorageDirectory() +
                    "/DCIM/camera/myPicture" + System.currentTimeMillis() + ".jpg";
            File imageFile = new File(path);
            FileOutputStream fos = null;
            try {
                fos = new FileOutputStream(imageFile);
                fos.write(data, 0, data.length);
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                if (fos != null) {
                    try {
                        fos.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
                broadcast();
                Message message = new Message();
                message.what = 0;
                Bundle mBundle = new Bundle();
                mBundle.putString("myPath",path);
                message.setData(mBundle);
                handler.sendMessage(message);
                mImage.close(); // 必須關閉 不然拍第二章會報錯
            }
        }
        // 異步訊息處理
        private Handler handler = new Handler(Looper.myLooper()) {

            @Override
            public void handleMessage(@NonNull Message message) {
                super.handleMessage(message);
                switch (message.what) {
                    case 0:
                        Bundle bundle = message.getData();
                        //通過指定的鍵值對獲取到剛剛發送過來的地址
                        String myPath = bundle.getString("myPath");
                        imageList.add(myPath);
                        //imageList.add(bundle.getString(myPath));   // 這樣不行必須分開寫 不然 arrList沒有資料
                        setLastImagePath();
                        break;
                    default:
                        throw new IllegalStateException("Unexpected value: " + message.what);
                }

            }
        };
        // 廣播通知相冊更新
        public void broadcast() {
            Log.d(TAG, "broadcast: success");
            String path = Environment.getExternalStorageDirectory().getAbsolutePath() + "/DCIM/";
            Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
            Uri uri = Uri.fromFile(new File(path));
            intent.setData(uri);
            mContext.sendBroadcast(intent);
        }
    }

    // 改變前后攝像頭
    private void changeCamera() {
        Log.d(TAG, "changeCamera: success");
        if (mCameraId.equals(String.valueOf(CameraCharacteristics.LENS_FACING_BACK))) {
            Toast.makeText(getContext(), "前置轉后置", Toast.LENGTH_SHORT).show();
            mCameraId = String.valueOf(CameraCharacteristics.LENS_FACING_FRONT);
        } else {
            Toast.makeText(getContext(), "后置轉前置", Toast.LENGTH_SHORT).show();
            mCameraId = String.valueOf(CameraCharacteristics.LENS_FACING_BACK);
        }
        mCameraDevice.close();
        openCamera();
    }

    //找到最后一張的路徑
    private void setLastImagePath() {
        Log.d(TAG, "setLastImagePath: success");
        imageList = GetImageFilePath.getFilePath();
        String string = imageList.get(imageList.size() - 1);
        if(string.contains(".jpg")){
            setImageBitmap(string);
        }else {
            MediaMetadataRetriever retriever = new MediaMetadataRetriever();
            retriever.setDataSource(string);
            Bitmap bitmap = retriever.getFrameAtTime(1);
            mImageView.setImageBitmap(bitmap);
        }
    }
    // 設定縮略圖
    private void setImageBitmap(String path){
        Log.d(TAG, "setImageBitmap: success");
        Bitmap bitmap = (Bitmap) BitmapFactory.decodeFile(path);
        mImageView.setImageBitmap(bitmap);
    }
    //打開相冊
    private void openAlbum() {
        Log.d(TAG, "openAlbum: success");
        Intent intent = new Intent();
        // 在ImageShowActivity中直接從相冊中遍歷 不需要傳遞過去
        //intent.putStringArrayListExtra("myList", imageList);
        intent.setClass(getContext(), ImageShowActivity.class);
        startActivity(intent);
    }
}

其中拍照流程 復制下面加粗函式在代碼中查看

1.權限申請,以及權限回呼,沒有權限回呼第一次安裝程式申請權限之后還是不會進入預覽界面,重啟程式后才能正常預覽拍照

2.因為使用的ViewPage加Fragment所以需要setUserVisibleHint函式來保證Fragment可見的時候在對其進行初始化,不然兩個Fragment同時執行初始化相機打開相機等會報錯.

3.初始化控制元件,在該類中用的是initView

4.監聽控制元件onClick

5.初始化 Surface 并在其回呼函式中執行配置相機打開相機等操作 因為我使用的是默認的TextureView所以直接寫回呼函式TextureView.SurfaceTextureListener textureListener = new TextureView.SurfaceTextureListener()....

6.通過CameraManager配置相機引數選擇相機id setupCamera()

7.選擇sizeMap中大于并且接近width和height的size getOptimalSize 如果是自己寫demo這可可以不要自己設定個指定的值也可以,包括旋轉螢屏處理也可以不要我在其中就注釋了

8.打開相機通過CameraManager getSystemService(Context.CAMERA_SERVICE)在我的demo中在openCamera()函式中實作

9.打開相機又需要用到攝像頭狀態回呼 CameraDevice.StateCallback 在onOpened中主要是把獲取到的cameraDevice拿到 以及執行開啟預覽

10.開啟預覽之前需要設定ImageReader在我的demo中是setupImageReader()中實作

11.創建預覽就需要先通過cameraDevice創建createCaptureSession 創建預覽申請中又用到CameraCaptureSession.StateCallback回呼在這個回呼函式的onConfigured獲取到mCaptureSession(捕捉會話)并在其中呼叫重復預覽

12.創建重復預覽請求通過captureRequest.Builder獲取到預覽請求的build再通過mPreviewRequestBuilder.build();獲取到預覽請求mPreviewRequest再通過

mCaptureSession.setRepeatingRequest(mPreviewRequest, mPreviewCaptureCallback, null);

實作反復進行預覽請求以及反復捕獲請求會話 在我的demo中repeatPreview()中實作

13.按照上面預覽就成功了接下來就是拍照在demo中takePhoto()實作拍照時又會呼叫

CameraCaptureSession.CaptureCallback 定義的mCaptureSession進行拍照處理

14.拍完照片就是保存照片創建子執行緒保存圖片 創建子執行緒保存照片就是為了提高效率做到拍完就保存而且保存照片又牽扯到廣播通知相冊重繪 在該demo中是通過內部類 ImageSaver 開啟子執行緒

15.我之前寫了把拍完照片的路徑通過Hander 傳message傳出來 但是后來沒用上 讀者可以拋棄

16.廣播通知相冊更新 主要是通過Itent 加uri實作 具體在demo中的 broadcast()函式中實作

17.通過縮略圖顯示最新的一張圖片也就是最后一張圖片demo中通過setLastImagePath()和setImageBitmap() 實作 主要是用到將String轉Bitmap 看看就懂了 中間用到的GetImageFilePath.getFilePath 是另外寫的一個類用來遍歷系統相冊的所有路徑我沒有對其進行排序如果有需要 可以根據修改時間排序 忘了補充在其中還判斷了是圖片還是視頻如果是視頻顯示第一幀

18.切換攝像頭 很簡單 demo中changCamera()實作

19.最后就是通過縮略圖進入到系統相冊openAlbum() 在這個函式中通過Itent啟動另一個活動

就是ImageShowActivity

5.GetImageFilePath.java中

package com.example.camera;

import android.os.Environment;
import android.util.Log;

import java.io.File;
import java.util.ArrayList;

public class GetImageFilePath {
    //獲取相冊camera 圖片路徑
    static ArrayList<String> imageList = new ArrayList<>() ;
    public static ArrayList<String> getFilePath() {
        File file= new File(Environment.getExternalStorageDirectory().toString() + "/DCIM/Camera");
        File[] dirEpub = file.listFiles();
        if (dirEpub.length != 0){
            for (int i = 0; i < dirEpub.length; i++){
                String fileName = dirEpub[i].toString();
                imageList.add(fileName);
                Log.i("File", "File name = " + fileName);
            }
        }
        return imageList;
    }
}

就是遍歷系統相冊下的所有檔案路徑

6.ImageShowActivity.java中

package com.example.camera;

import android.annotation.SuppressLint;
import android.content.ContentUris;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.provider.MediaStore;
import android.util.Log;

import androidx.appcompat.app.AppCompatActivity;

import java.util.ArrayList;


public class ImageShowActivity extends AppCompatActivity  {
    private final static String TAG = "ImageShowActivity";
    private String lastImagePath;

    //圖片集合
    private ArrayList<String> imageList = new ArrayList<>();


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.e(TAG, "onCreate success !!!!!");
        setContentView(R.layout.activity_image_show);
        // 呼叫獲取圖片路徑類中的靜態方法
        imageList = GetImageFilePath.getFilePath();
        lastImagePath = imageList.get(imageList.size() - 1);
        gotoGallery(lastImagePath);
    }

    // 轉到畫廊   就是圖庫
        public void gotoGallery(String path) {
            Log.e("TAG", "gotoGallery success !!!!!");
            Uri uri =getMediaUriFromPath(this, path);
            Log.i("asdddd", "uri: "+uri);
            Intent intent = new Intent("com.android.camera.action.REVIEW", uri);
            intent.setData(uri);
            startActivity(intent);
            finish();

        }

    @SuppressLint("Range")
    public  Uri getMediaUriFromPath(Context context, String path) {
        Log.e("TAG", "getMediaUriFromPath success !!!!!");
        Uri uri = null;
        if(path.contains("jpg")){
            Uri picUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
            Cursor cursor = context.getContentResolver().query(picUri,
                    null,
                    MediaStore.Images.Media.DISPLAY_NAME + "= ?",
                    new String[] {path.substring(path.lastIndexOf("/") + 1)},
                    null);
            if(cursor.moveToFirst()) {
                uri = ContentUris.withAppendedId(picUri,
                        cursor.getLong(cursor.getColumnIndex(MediaStore.Images.Media._ID)));
            }
            cursor.close();
        }else if(path.contains("mp4")){
            Uri mediaUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
            Cursor cursor = context.getContentResolver().query(mediaUri,
                    null,
                    MediaStore.Video.Media.DISPLAY_NAME + "= ?",
                    new String[] {path.substring(path.lastIndexOf("/") + 1)},
                    null);
            if(cursor.moveToFirst()) {
                uri = ContentUris.withAppendedId(mediaUri,
                        cursor.getLong(cursor.getColumnIndex(MediaStore.Video.Media._ID)));
            }
            cursor.close();
        }
        return uri;
    }



}

重點就是gotoGallery()中通過Itent

Intent intent = new Intent("com.android.camera.action.REVIEW", uri);

就可以實作呼叫相冊的功能

7.ImageShowActivity.xml

<?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="match_parent">
    <ImageView
        android:id="@+id/show_pictures"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        />

</RelativeLayout>

就一個ImageView

8.RecoderVideoFragment.xml中

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

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

    <ImageButton
        android:background="@drawable/shape_white_ring"
        android:id="@+id/recording"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/shape_recorder"
        android:layout_alignParentBottom="true"
        android:layout_centerHorizontal="true"
        android:layout_marginBottom="50dp"
        />

    <ImageView
        android:id="@+id/image_show"
        android:layout_width="50dp"
        android:layout_height="50dp"
        android:layout_alignParentBottom="true"
        android:layout_marginBottom="50dp"
        android:layout_marginLeft="50dp"
        />
    <ImageButton
        android:id="@+id/change"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@drawable/ic_baseline_flip_camera_android_24"
        android:layout_alignParentBottom="true"
        android:layout_marginRight="50dp"
        android:layout_marginBottom="50dp"
        android:layout_alignParentRight="true"
        />

    <Chronometer
        android:id="@+id/timer"
        android:textColor="#f00"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:format="%s"
        android:gravity="center"
        android:textSize="40sp" />



</RelativeLayout>

其實完全可以不用這樣重寫一次 完全可以包含上一個takepicture的xml包進行寫就行其實是一模一樣的就是加了個計時器控制元件而已

9.RecoderVideoFragment.java中

package com.example.camera;


import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.ImageFormat;
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.CameraMetadata;
import android.hardware.camera2.CaptureFailure;
import android.hardware.camera2.CaptureRequest;
import android.hardware.camera2.CaptureResult;
import android.hardware.camera2.TotalCaptureResult;
import android.hardware.camera2.params.StreamConfigurationMap;
import android.media.MediaMetadataRetriever;
import android.media.MediaRecorder;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.SystemClock;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.Size;
import android.util.SparseIntArray;
import android.view.LayoutInflater;
import android.view.Surface;
import android.view.TextureView;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Chronometer;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;

public class RecorderVideoFragment extends Fragment implements View.OnClickListener {
    private String TAG = "RecorderVideoFragment";
    private ImageButton videoButton; //用來重新設定錄像按鈕
    private TextureView mTextureView; //預覽框控制元件
    private CaptureRequest.Builder mPreviewCaptureRequest; //獲取請求創建者
    private CameraDevice mCameraDevice; //camera設備
    private MediaRecorder mMediaRecorder; //音視頻錄制
    //攝像頭ID 默認置為后置BACK FRONT值為0 == BACK
    private String mCameraId = String.valueOf(CameraCharacteristics.LENS_FACING_FRONT);
    private Chronometer timer; //計時器
    private ArrayList<String> imageList = new ArrayList<>();   // 路徑集合
    private static CameraCaptureSession mCameraCaptureSession;   //獲取會話
    private Handler mChildHandler;   //子執行緒
    private CameraManager mCameraManager; //攝像頭管理者
    private boolean isVisible = false;  //fragment是否可見
    private boolean isRecording = false;   //是否在錄制視頻
    private HandlerThread mHandlerThread;  //執行緒處理者
    private ImageView mImageView;     //縮略圖按鈕
    private ImageButton change;    //前后置切換按鈕

    private static final SparseIntArray ORIENTATION = new SparseIntArray();

    static {
        ORIENTATION.append(Surface.ROTATION_0, 90);
        ORIENTATION.append(Surface.ROTATION_90, 0);
        ORIENTATION.append(Surface.ROTATION_180, 270);
        ORIENTATION.append(Surface.ROTATION_270, 180);
    }

    //Fragment 中 onCreateView回傳的就是fragment要顯示的view.
    @Nullable
    @Override
    /**
     * 第一個引數LayoutInflater inflater第二個引數ViewGroup container第三個引數 Bundle savedInstanceState
     * LayoutInflater inflater:作用類似于findViewById()用來尋找xml布局下的具體的控制元件Button、TextView等,
     * LayoutInflater inflater()用來找res/layout/下的xml布局檔案
     * ViewGroup container:表示容器,View放在里面
     * Bundle savedInstanceState:保存當前的狀態,在活動的生命周期中,只要離開了可見階段,活動很可能就會被行程終止,
     * 這種機制能保存當時的狀態
     */
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
                             @Nullable Bundle savedInstanceState) {
        Log.d(TAG, "onCreateView: success");
        View view = inflater.inflate(R.layout.fragment_recorder, container, false);
        initView(view);
        //監聽視頻按鈕
        videoButton.setOnClickListener(this);
        //監聽縮略圖按鈕
        mImageView.setOnClickListener(this);
        //監聽前后置切換按鈕
        change.setOnClickListener(this);
        return view;
    }

    //判斷Fragment是否可見
    @Override
    public void setUserVisibleHint(boolean isVisibleToUser) {
        super.setUserVisibleHint(isVisibleToUser);
        if (isVisibleToUser) {
            //如果Fragment可見 把isVisible置為true
            isVisible = true;
            Log.d(TAG, "setUserVisibleHint: true");
            //設定顯示最后一張圖片(視頻第一幀)
            setLastImagePath();
            //初始化子執行緒
            initChildHandler();
            //如果textureView可用
            if (mTextureView.isAvailable()) {
                openCamera();
            } else {
                initTextureView();
            }
        } else {
            closeCamera();
            return;
        }
    }

    @Override
    public void onResume() {
        super.onResume();
        if (isVisible) {
            初始化子執行緒
            initChildHandler();
            if (mTextureView.isAvailable()) {
                openCamera();
            } else {
                initTextureView();
            }
        }
    }
    //初始化監聽控制元件
    private void initView(View view){
        mImageView = view.findViewById(R.id.image_show);
        mTextureView = view.findViewById(R.id.textureView);
        timer = view.findViewById(R.id.timer);
        videoButton = view.findViewById(R.id.recording);
        change = view.findViewById(R.id.change);
    }
    @Override
    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.recording:
                if (isRecording) {
                    //再次按下將停止錄制
                    stopRecorder();
                    isRecording = false;
                } else {
                    //第一次按下將isRecording置為ture
                    //配置并開始錄制
                    isRecording = true;
                    configSession();
                    startRecorder();
                }
                break;
            case R.id.image_show:
                openAlbum();
                break;
            case R.id.change:
                changeCamera();
                break;

        }
    }


    //1.攝像頭狀態回呼
    private CameraDevice.StateCallback mCameraDeviceStateCallback = new CameraDevice.StateCallback() {
        @Override
        public void onOpened(@NonNull CameraDevice camera) {
            //攝像頭被打開
            mCameraDevice = camera;
            startPreview();
        }

        @Override
        public void onDisconnected(@NonNull CameraDevice camera) {
            //攝像頭斷開
        }

        @Override
        public void one rror(@NonNull CameraDevice camera, int error) {
            //例外
        }
    };
    //2.錄像時訊息捕獲回呼
    private CameraCaptureSession.CaptureCallback mSessionCaptureCallback = new CameraCaptureSession.CaptureCallback() {
        @Override
        public void onCaptureStarted(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request,
                                     long timestamp, long frameNumber) {
            super.onCaptureStarted(session, request, timestamp, frameNumber);
        }

        @Override
        public void onCaptureProgressed(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request,
                                        @NonNull CaptureResult partialResult) {
            super.onCaptureProgressed(session, request, partialResult);
        }

        @Override
        public void onCaptureCompleted(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request,
                                       @NonNull TotalCaptureResult result) {
            super.onCaptureCompleted(session, request, result);
        }

        @Override
        public void onCaptureFailed(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request,
                                    @NonNull CaptureFailure failure) {
            super.onCaptureFailed(session, request, failure);
        }
    };
    //3.錄像時會話狀態回呼
    private CameraCaptureSession.StateCallback mSessionStateCallback = new CameraCaptureSession.StateCallback() {
        @Override
        public void onConfigured(@NonNull CameraCaptureSession session) {
            mCameraCaptureSession = session;
            updatePreview();
            try {
                //執行重復獲取資料請求,等于一直獲取資料呈現預覽畫面,mSessionCaptureCallback會回傳此次操作的資訊回呼
                mCameraCaptureSession.setRepeatingRequest(mPreviewCaptureRequest.build(),
                        mSessionCaptureCallback, mChildHandler);
            } catch (CameraAccessException e) {
                e.printStackTrace();
            }
        }

        @Override
        public void onConfigureFailed(@NonNull CameraCaptureSession session) {
        }
    };

    //設定模式  閃光燈用
    private void setUpCaptureRequestBuilder(CaptureRequest.Builder builder) {
        builder.set(CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO);
    }

    /**
     * 初始化TextureView的紋理生成監聽,只有紋理生成準備好了,才能去進行攝像頭的初始化作業讓TextureView接收攝像頭預覽畫面
     */
    private void initTextureView() {
        mTextureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() {
            @Override
            public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
                //可以使用紋理
                initCameraManager();
                selectCamera();
                openCamera();
            }

            @Override
            public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
                //紋理尺寸變化

            }

            @Override
            public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
                //紋理被銷毀
                return false;
            }

            @Override
            public void onSurfaceTextureUpdated(SurfaceTexture surface) {
                //紋理更新

            }
        });
    }

    /**
     * 計算需要的使用的攝像頭解析度
     *
     * @return
     */
    private Size getMatchingSize() {
        Size selectSize = null;
        try {
            CameraCharacteristics cameraCharacteristics = mCameraManager.getCameraCharacteristics(mCameraId);
            StreamConfigurationMap streamConfigurationMap = cameraCharacteristics.get
                    (CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
            Size[] sizes = streamConfigurationMap.getOutputSizes(ImageFormat.JPEG);
            //這里是將預覽鋪滿螢屏,所以直接獲取螢屏解析度
            DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
            //螢屏解析度寬
            int deviceWidth = displayMetrics.widthPixels;
            //螢屏解析度高
            int deviceHeight = displayMetrics.heightPixels;
            /**
             * 回圈40次,讓寬度范圍從最小逐步增加,找到最符合螢屏寬度的解析度,
             * 你要是不放心那就增加回圈,肯定會找到一個解析度,不會出現此方法回傳一個null的Size的情況
             * ,但是回圈越大后獲取的解析度就越不匹配
             */
            for (int j = 1; j < 41; j++) {
                for (int i = 0; i < sizes.length; i++) { //遍歷所有Size
                    Size itemSize = sizes[i];
                    //判斷當前Size高度小于螢屏寬度+j*5  &&  判斷當前Size高度大于螢屏寬度-j*5  &&  判斷當前Size寬度小于當前螢屏高度
                    if (itemSize.getHeight() < (deviceWidth + j * 5) && itemSize.getHeight() > (deviceWidth - j * 5)) {
                        if (selectSize != null) { //如果之前已經找到一個匹配的寬度
                            if (Math.abs(deviceHeight - itemSize.getWidth()) < Math.abs(deviceHeight - selectSize.getWidth())) { //求絕對值算出最接近設備高度的尺寸
                                selectSize = itemSize;
                                continue;
                            }
                        } else {
                            selectSize = itemSize;
                        }
                    }
                }
                if (selectSize != null) { //如果不等于null 說明已經找到了 跳出回圈
                    break;
                }
            }
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
        Log.e(TAG, "getMatchingSize: 選擇的解析度寬度=" + selectSize.getWidth());
        Log.e(TAG, "getMatchingSize: 選擇的解析度高度=" + selectSize.getHeight());
        return selectSize;
    }

    /**
     * 初始化Camera2的相機管理,CameraManager用于獲取攝像頭解析度,攝像頭方向,攝像頭id與打開攝像頭的作業
     */
    private void initCameraManager() {
        mCameraManager = (CameraManager) getActivity().getSystemService(Context.CAMERA_SERVICE);
    }

    /**
     * 選擇一顆我們需要使用的攝像頭,主要是選擇使用前攝還是后攝或者是外接攝像頭
     */
    private void selectCamera() {
        if (mCameraManager != null) {
            Log.e(TAG, "selectCamera: CameraManager is null");

        }
        try {
            String[] cameraIdList = mCameraManager.getCameraIdList();   //獲取當前設備的全部攝像頭id集合
            if (cameraIdList.length == 0) {
                Log.e(TAG, "selectCamera: cameraIdList length is 0");
            }
            for (String cameraId : cameraIdList) {
                //遍歷所有攝像頭
                //螢屏方向
                int rotation = getActivity().getWindowManager().getDefaultDisplay().getRotation();
                //設定拍照方向
//                if (mCameraId.equals("1")) {
//                    rotation = 2;
//                }
                mPreviewCaptureRequest.set(CaptureRequest.JPEG_ORIENTATION, ORIENTATION.get(rotation));
                if (rotation == CameraCharacteristics.LENS_FACING_BACK) {
                    //這里選擇了后攝像頭
                    mCameraId = cameraId;
                }
            }

        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    }

    @SuppressLint("MissingPermission")
    private void openCamera() {
        try {
            if (mCameraManager == null) {
                initCameraManager();
            }
            mCameraManager.openCamera(mCameraId, mCameraDeviceStateCallback, mChildHandler);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    }

    private void closeCamera() {
        // 關閉預覽就是關閉捕獲會話
        stopPreview();
        // 關閉當前相機
        if (null != mCameraDevice) {
            mCameraDevice.close();
            mCameraDevice = null;
        }
        if (null != mMediaRecorder) {
            mMediaRecorder.release();
            mMediaRecorder = null;
        }
        if (mHandlerThread != null) {
            stopBackgroundThread();
        }
    }

    /**
     * 開啟預覽
     * 使用TextureView顯示相機預覽資料,
     * 預覽和拍照資料都是使用CameraCaptureSession會話來請求
     */
    private void startPreview() {
        stopPreview();
        SurfaceTexture mSurfaceTexture = mTextureView.getSurfaceTexture();
        Size cameraSize = getMatchingSize();
        //設定TextureView的緩沖區大小
        mSurfaceTexture.setDefaultBufferSize(cameraSize.getWidth(),
                cameraSize.getHeight());
        //獲取Surface顯示預覽資料
        Surface previewSurface = new Surface(mSurfaceTexture);
        try {
            //創建CaptureRequestBuilder,TEMPLATE_PREVIEW比表示預覽請求
            mPreviewCaptureRequest = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
            //設定Surface作為預覽資料的顯示界面
            mPreviewCaptureRequest.addTarget(previewSurface);
            //創建相機捕獲會話,第一個引數是捕獲資料Surface串列,
            // 第二個引數是CameraCaptureSession的狀態回呼介面,
            //當他創建好后會回呼onConfigured方法,第三個引數用來確定Callback在哪個執行緒執行
            mCameraDevice.createCaptureSession(Collections.singletonList(previewSurface),
                    new CameraCaptureSession.StateCallback() {
                @Override
                public void onConfigured(@NonNull CameraCaptureSession session) {
                    mCameraCaptureSession = session;
                    updatePreview();
                }

                @Override
                public void onConfigureFailed(@NonNull CameraCaptureSession session) {
                    Toast.makeText(getActivity().getApplicationContext(), "Faileedsa ", Toast.LENGTH_SHORT).show();
                }
            }, mChildHandler);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    }

    /**
     * 更新預覽
     */
    private void updatePreview() {
        if (null == mCameraDevice) {
            return;
        }
        try {
            setUpCaptureRequestBuilder(mPreviewCaptureRequest);
            HandlerThread thread = new HandlerThread("CameraPreview");
            thread.start();
            mCameraCaptureSession.setRepeatingRequest(mPreviewCaptureRequest.build(), null, mChildHandler);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    }

    /**
     * 關閉預覽
     */
    private void stopPreview() {
        //關閉預覽就是關閉捕獲會話
        if (mCameraCaptureSession != null) {
            mCameraCaptureSession.close();
            mCameraCaptureSession = null;
        }
    }

    /**
     * 初始化子執行緒Handler,操作Camera2需要一個子執行緒的Handler
     */
    private void initChildHandler() {
        mHandlerThread = new HandlerThread("DangJunHaoDemo");
        mHandlerThread.start();
        mChildHandler = new Handler(mHandlerThread.getLooper());
    }

    /**
     * 關閉執行緒
     */
    public void stopBackgroundThread() {
        if (mHandlerThread != null) {
            //quitSafely 安全退出
            mHandlerThread.quitSafely();
            try {
                mHandlerThread.join();
                mHandlerThread = null;
                mHandlerThread = null;
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 配置錄制視頻相關資料
     */
    private void configMediaRecorder() {
        File file = new File(Environment.getExternalStorageDirectory() +
                "/DCIM/camera/myMp4" + System.currentTimeMillis() + ".mp4");
        if (file.exists()) {
            file.delete();
        }
        if (mMediaRecorder == null) {
            mMediaRecorder = new MediaRecorder();
        }
        //設定音頻來源
        mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
        //設定視頻來源
        mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);
        //設定輸出格式
        mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
        //設定音頻編碼格式,請注意這里使用默認,實際app專案需要考慮兼容問題,應該選擇AAC
        mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
        //設定視頻編碼格式,請注意這里使用默認,實際app專案需要考慮兼容問題,應該選擇H264
        mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
        //設定位元率 一般是 1*解析度 到 10*解析度 之間波動,位元率越大視頻越清晰但是視頻檔案也越大,
        mMediaRecorder.setVideoEncodingBitRate(8 * 1024 * 1920);
        //設定幀數 選擇 30即可, 過大幀數也會讓視頻檔案更大當然也會更流暢,但是沒有多少實際提升,人眼極限也就30幀了,
        mMediaRecorder.setVideoFrameRate(30);
        Size size = getMatchingSize();
        mMediaRecorder.setVideoSize(size.getWidth(), size.getHeight());
        mMediaRecorder.setOrientationHint(90);
        //如果是前置
        if(mCameraId.equals("1")){
            mMediaRecorder.setOrientationHint(270);
        }
        Surface surface = new Surface(mTextureView.getSurfaceTexture());
        mMediaRecorder.setPreviewDisplay(surface);
        mMediaRecorder.setOutputFile(file.getAbsolutePath());
        try {
            mMediaRecorder.prepare();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 配置錄制視頻時的CameraCaptureSession
     */
    private void configSession() {
        try {
            if (mCameraCaptureSession != null) {
                mCameraCaptureSession.stopRepeating();//停止預覽,準備切換到錄制視頻
                mCameraCaptureSession.close();//關閉預覽的會話,需要重新創建錄制視頻的會話
                mCameraCaptureSession = null;
            }
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
        configMediaRecorder();
        Size cameraSize = getMatchingSize();
        SurfaceTexture surfaceTexture = mTextureView.getSurfaceTexture();
        surfaceTexture.setDefaultBufferSize(cameraSize.getWidth(), cameraSize.getHeight());
        Surface previewSurface = new Surface(surfaceTexture);
        Surface recorderSurface = mMediaRecorder.getSurface();//從獲取錄制視頻需要的Surface
        try {
            mPreviewCaptureRequest = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
            mPreviewCaptureRequest.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
            mPreviewCaptureRequest.addTarget(previewSurface);
            mPreviewCaptureRequest.addTarget(recorderSurface);
            //請注意這里設定了Arrays.asList(previewSurface,recorderSurface) 2個Surface,很好理解錄制視頻也需要有畫面預覽,
            // 第一個是預覽的Surface,第二個是錄制視頻使用的Surface
            mCameraDevice.createCaptureSession(Arrays.asList(previewSurface, recorderSurface),
                    mSessionStateCallback, mChildHandler);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    }
    /**
     * 開始錄制視頻
     */
    private void startRecorder() {
        mMediaRecorder.start();
        //開始計時
        startTime();
    }
    /**
     * 暫停錄制視頻(暫停后視頻檔案會自動保存)
     */
    private void stopRecorder() {
        if (mMediaRecorder != null) {
            mMediaRecorder.stop();
            mMediaRecorder.reset();
            //停止計時
            endTime();
        }
        broadcast();
        setLastImagePath();
        startPreview();
    }
    // 廣播通知相冊更新
    public void broadcast() {
        Log.d(TAG, "broadcast: success");
        String path = Environment.getExternalStorageDirectory().getAbsolutePath() + "/DCIM/";
        Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
        Uri uri = Uri.fromFile(new File(path));
        intent.setData(uri);
        getActivity().sendBroadcast(intent);
    }
    // 改變前后攝像頭
    private void changeCamera() {
        Log.d(TAG, "changeCamera: success");
        if (mCameraId.equals(String.valueOf(CameraCharacteristics.LENS_FACING_BACK))) {
            Toast.makeText(getContext(), "前置轉后置", Toast.LENGTH_SHORT).show();
            mCameraId = String.valueOf(CameraCharacteristics.LENS_FACING_FRONT);
        } else {
            Toast.makeText(getContext(), "后置轉前置", Toast.LENGTH_SHORT).show();
            mCameraId = String.valueOf(CameraCharacteristics.LENS_FACING_BACK);
        }
        mCameraDevice.close();
        openCamera();
    }

    //找到最后一張的路徑
    private void setLastImagePath() {
        Log.d(TAG, "setLastImagePath: success");
        imageList = GetImageFilePath.getFilePath();
        String string = imageList.get(imageList.size() - 1);
        if (string.contains(".jpg")) {
            setImageBitmap(string);
        } else {
            MediaMetadataRetriever retriever = new MediaMetadataRetriever();
            retriever.setDataSource(string);
            //獲取第1幀
            Bitmap bitmap = retriever.getFrameAtTime(1);
            mImageView.setImageBitmap(bitmap);
        }
    }

    // 設定縮略圖顯示
    private void setImageBitmap(String path) {
        Log.d(TAG, "setImageBitmap: success");
        Bitmap bitmap = BitmapFactory.decodeFile(path);
        //通過ImageView顯示縮略圖
        mImageView.setImageBitmap(bitmap);
    }
    //打開相冊
    private void openAlbum() {
        Log.d(TAG, "openAlbum: success");
        Intent intent = new Intent();
        // 在ImageShowActivity中直接從相冊中遍歷 不需要傳遞過去
        //intent.putStringArrayListExtra("myList", imageList);
        intent.setClass(getContext(), ImageShowActivity.class);
        startActivity(intent);
    }
    private void startTime() {
        timer.setBase(SystemClock.elapsedRealtime());//計時器清零
        timer.start();
    }
    private void endTime(){
        timer.stop();
        timer.setBase(SystemClock.elapsedRealtime());//計時器清零
    }
}

視頻錄制的流程跟拍照很相似

創建的請求不一樣僅此而已

1.因為我在拍照的Fragment中獲取過權限所以不在需要申請權限

2.因為使用的ViewPage加Fragment所以需要setUserVisibleHint函式來保證Fragment可見的時候在對其進行初始化,不然兩個Fragment同時執行初始化相機打開相機等會報錯.

3.初始化控制元件,在該類中用的是initView

4.監聽控制元件onClick

5.初始化TextureView 監聽 在demo中 initTextureVIew()

6.計算使用的攝像頭解析度 getMatchingSize()

7.初始化相機管理 initCameraManager()

只是獲取 CameraManager

8.選擇攝像頭 主要是選擇前后 selectCamera()

9.openCamera() 還是一樣openCamera 的時候需要用到CameraDevice.StateCallback 在里面 開啟預覽

10.開啟預覽 這里是把創建預覽請求mPreviewCaptureRequest = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);

mPreviewCaptureRequest.addTarget(previewSurface);

寫到了startPreview()中捕獲請求的回呼CameraCaptureSession.StateCallback() 也寫到這個方法里在回呼中呼叫重復預覽 repeatPreview()

11.重復預覽 repeatPreview()

mCameraCaptureSession.setRepeatingRequest(mPreviewCaptureRequest.build(), null, mChildHandler);

mChildHandler這里是創建了一個子執行緒處理

12.因為錄制視頻需要停止預覽stopPreview()

13.初始化子執行緒 initChildHandler() 以及關閉執行緒stopBackgroundThread()

14.配置錄制視頻的相關資料configMediaRecorder()

mMediaRecorder.setOrientationHint(270); 通過這個來處理保存的視頻方向前置需要設為270 后置需要設定90

15.配置錄制視頻的CameraCaptureSession 這里跟拍照不一樣configSession()

16.開始錄制startRecorder()

停止錄制stopRecorder()

17.廣播重繪相冊broadcast()

18. 改變前后攝像頭changeCamera()

19.設定顯示最后一樣圖片設定最后一張路徑setLastImagePath() 把路徑轉圖片顯示setImageBitmap

20.打開相冊 openAlbum()

21.錄制視頻需要用的計時器控制元件startTime() endTime()

總結幾個回呼使用與區別:

1.CameraDevice.StateCallback openCamera會用到

2.CameraCaptureSession.CaptureCallback 拍斬訓者是錄像的時候用到

3.CameraCaptureSession.StateCallback 處理請求的回呼

4.onRequestPermissionResult 權限回呼

5.TextureView.SurfaceTextureListener() SurfaceView狀態回呼

這是我學習的程序不一定對 但是對小白肯定有幫助

轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/303907.html

標籤:其他

上一篇:《#小程式:iOS逆向》的歷史版本更新內容

下一篇:這個神器,把打包Python腳本為Exe的流程都封裝好了

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【從零開始擼一個App】Dagger2

    Dagger2是一個IOC框架,一般用于Android平臺,第一次接觸的朋友,一定會被搞得暈頭轉向。它延續了Java平臺Spring框架代碼碎片化,注解滿天飛的傳統。嘗試將各處代碼片段串聯起來,理清思緒,真不是件容易的事。更不用說還有各版本細微的差別。 與Spring不同的是,Spring是通過反射 ......

    uj5u.com 2020-09-10 06:57:59 more
  • Flutter Weekly Issue 66

    新聞 Flutter 季度調研結果分享 教程 Flutter+FaaS一體化任務編排的思考與設計 詳解Dart中如何通過注解生成代碼 GitHub 用對了嗎?Flutter 團隊分享如何管理大型開源專案 插件 flutter-bubble-tab-indicator A Flutter librar ......

    uj5u.com 2020-09-10 06:58:52 more
  • Proguard 常用規則

    介紹 Proguard 入口,如何查看輸出,如何使用 keep 設定入口以及使用實體,如何配置壓縮,混淆,校驗等規則。

    ......

    uj5u.com 2020-09-10 06:59:00 more
  • Android 開發技術周報 Issue#292

    新聞 Android即將獲得類AirDrop功能:可向附近設備快速分享檔案 谷歌為安卓檔案管理應用引入可安全隱藏資料的Safe Folder功能 Android TV新主界面將顯示電影、電視節目和應用推薦內容 泄露的Android檔案暗示了傳說中的谷歌Pixel 5a與折疊屏新機 谷歌發布Andro ......

    uj5u.com 2020-09-10 07:00:37 more
  • AutoFitTextureView Error inflating class

    報錯: Binary XML file line #0: Binary XML file line #0: Error inflating class xxx.AutoFitTextureView 解決: <com.example.testy2.AutoFitTextureView android: ......

    uj5u.com 2020-09-10 07:00:41 more
  • 根據Uri,Cursor沒有獲取到對應的屬性

    Android: 背景:呼叫攝像頭,拍攝視頻,指定保存的地址,但是回傳的Cursor檔案,只有名稱和大小的屬性,沒有其他諸如時長,連ID屬性都沒有 使用 cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATIO ......

    uj5u.com 2020-09-10 07:00:44 more
  • Android連載29-持久化技術

    一、持久化技術 我們平時所使用的APP產生的資料,在記憶體中都是瞬時的,會隨著斷電、關機等丟失資料,因此android系統采用了持久化技術,用于存盤這些“瞬時”資料 持久化技術包括:檔案存盤、SharedPreference存盤以及資料庫存盤,還有更復雜的SD卡記憶體儲。 二、檔案存盤 最基本存盤方式, ......

    uj5u.com 2020-09-10 07:00:47 more
  • Android Camera2Video整合到自己專案里

    背景: Android專案里呼叫攝像頭拍攝視頻,原本使用的 MediaStore.ACTION_VIDEO_CAPTURE, 后來因專案需要,改成了camera2 1.Camera2Video 官方demo有點問題,下載后,不能直接整合到專案 問題1.多次拍攝視頻崩潰 問題2.雙擊record按鈕, ......

    uj5u.com 2020-09-10 07:00:50 more
  • Android 開發技術周報 Issue#293

    新聞 谷歌為Android TV開發者提供多種新功能 Android 11將自動填表功能整合到鍵盤輸入建議中 谷歌宣布Android Auto即將支持更多的導航和數字停車應用 谷歌Pixel 5只有XL版本 搭載驍龍765G且將比Pixel 4更便宜 [圖]Wear OS將迎來重磅更新:應用啟動時間 ......

    uj5u.com 2020-09-10 07:01:38 more
  • 海豚星空掃碼投屏 Android 接收端 SDK 集成 六步驟

    掃碼投屏,開放網路,獨占設備,不需要額外下載軟體,微信掃碼,發現設備。支持標準DLNA協議,支持倍速播放。視頻,音頻,圖片投屏。好點意思。還支持自定義基于 DLNA 擴展的操作動作。好像要收費,沒體驗。 這里簡單記錄一下集成程序。 一 跟目錄的build.gradle添加私有mevan倉庫 mave ......

    uj5u.com 2020-09-10 07:01:43 more
最新发布
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:40:31 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:40:11 more
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:39:36 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:39:13 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:16:23 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:16:15 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:15:46 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:14:53 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:14:08 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:08:34 more