GLSurfaceView camera 製作自定義相機

1. SurfaceView,GlSurfaceView,SurfaceTexture,TextureView的優缺點及區別
SurfaceView
繼承自View,擁有View的大部分屬性,但是由於holder的存在,不能設置透明度。 
優點:可以在一個獨立的線程中進行繪製,不會影響主線程,使用雙緩衝機制,播放視頻時畫面更流暢 
缺點:surface的顯示不受View屬性的控制,不能將其放在ViewGroup中,SurfaceView不能嵌套使用。

GlSurfaceView
GlSurfaceView繼承自SurfaceView類,專門用來顯示OpenGL渲染的,簡單理解可以顯示視頻,圖像及3D場景這些的。

SurfaceTexture
和SurfaceView功能類似,區別是,SurfaceTexure可以不顯示在界面中。使用OpenGl對圖片流進行美化,添加水印,濾鏡這些操作的時候我們都是通過SurfaceTexre去處理,處理完之後再通過GlSurfaceView顯示。缺點,可能會導致個別幀的延遲。本身管理着BufferQueue,所以內存消耗會多一點。

TextureView
同樣繼承自View,必須在開啓硬件加速的設備中使用(保守估計目前百分之九十的Android設備都開啓了),TextureView通過setSurfaceTextureListener的回調在子線程中進行更新UI. 
優點:支持動畫效果。 
缺點:在5.0之前在主線程渲染,在5.0之後在單獨線程渲染。


2. 如何通過SurfaceView顯示Camera預覽。
基本步驟
在xml文件中設置SurfaceView 。
實現SurfaceHolder.Callback的回調。
打開攝像頭Camera.open(0);
設置攝像頭相關參數;
將攝像頭數據設置到SurfaceView中,並開啓預覽。
代碼部分
<android.support.constraint.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"  tools:context="com.aserbao.aserbaosandroid.opengl.openGlCamera.simpleCameraOpengl.simpleCamera.CameraSurfaceViewShowActivity">
    <FrameLayout
        android:id="@+id/frame_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <SurfaceView
                    android:layout_width="match_parent"
                     android:layout_height="match_parent"
                     android:id="@+id/mSurface" />
        <Button
            android:id="@+id/btn_change"
            android:text="給動畫,SurfaceView不支持設置透明度"
            android:textAllCaps="false"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>
    </FrameLayout>
</android.support.constraint.ConstraintLayout>

/**
 * 使用SurfaceView預覽Camera數據
 */
public class CameraSurfaceViewShowActivity extends AppCompatActivity implements SurfaceHolder.Callback {
    @BindView(R.id.mSurface)
    SurfaceView mSurfaceView;

    public SurfaceHolder mHolder;
    private Camera mCamera;
    private Camera.Parameters mParameters;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_base_camera);
        ButterKnife.bind(this);
        mHolder = mSurfaceView.getHolder();
        mHolder.addCallback(this);
        mHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
    }

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        try {
            // Open the Camera in preview mode
            mCamera = Camera.open(0);
            mCamera.setDisplayOrientation(90);
            mCamera.setPreviewDisplay(holder);
            mCamera.startPreview();
        } catch (IOException e) {
        }
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
        mCamera.autoFocus(new Camera.AutoFocusCallback() {
            @Override
            public void onAutoFocus(boolean success, Camera camera) {
                if (success) {
                    mParameters = mCamera.getParameters();
                    mParameters.setPictureFormat(PixelFormat.JPEG); //圖片輸出格式
//                    mParameters.setFlashMode(Camera.Parameters.FLASH_MODE_TORCH);//預覽持續發光
                    mParameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);//持續對焦模式
                    mCamera.setParameters(mParameters);
                    mCamera.startPreview();
                    mCamera.cancelAutoFocus();
                }
            }
        });
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        if (mCamera != null) {
            mCamera.stopPreview();
            mCamera.release();
            mCamera = null;
        }
    }

    @OnClick(R.id.btn_change)
    public void onViewClicked() {
//        PropertyValuesHolder valuesHolder2 = PropertyValuesHolder.ofFloat("rotationX", 0.0f, 360.0f, 0.0F);
        PropertyValuesHolder valuesHolder = PropertyValuesHolder.ofFloat("rotationY", 0.0f, 360.0f, 0.0F);
        PropertyValuesHolder valuesHolder1 = PropertyValuesHolder.ofFloat("scaleX", 1.0f, 0.5f,1.0f);
        PropertyValuesHolder valuesHolder3 = PropertyValuesHolder.ofFloat("scaleY", 1.0f, 0.5f,1.0f);
        ObjectAnimator objectAnimator = ObjectAnimator.ofPropertyValuesHolder(mSurfaceView,  valuesHolder,valuesHolder1,valuesHolder3);
        objectAnimator.setDuration(5000).start();
    }
}
效果展示


提別提醒
SurfaceView預覽相機視圖不支持透明度,可以設置縮放旋轉屬性。如果需要做動畫特效的話不推薦使用SurfaceView顯示視圖。可以使用下面的TextureView或者GlSurfaceView來顯示。

3. 如何通過TextureView顯示Camera預覽
基本步驟
TextureView和SurfaceView顯示Camera數據其實差不多,差別就兩點: 
1. SurfaceView顯示需要實現SurfaceHolder.Callback的回調而TextureView通過實現 TextureView.SurfaceTextureListener接口。 
2. 當Camera使用SurfaceView預覽時通過setPreviewDisplay(holder)方法來設置預覽視圖,而使用TextureView預覽時使用setPreviewTexture(mCameraTextureView.getSurfaceTexture())方法來設置。 
其他步驟同上。

代碼部分
public class CameraTextureViewShowActivity extends AppCompatActivity implements TextureView.SurfaceTextureListener {

    @BindView(R.id.camera_texture_view)
    TextureView mCameraTextureView;
    public Camera mCamera;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_camera_surface_texture);
        ButterKnife.bind(this);
        mCameraTextureView.setSurfaceTextureListener(this);
    }

    @Override
    public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
        try {
            mCamera = Camera.open(0);
            mCamera.setDisplayOrientation(90);
            mCamera.setPreviewTexture(mCameraTextureView.getSurfaceTexture());
            mCamera.startPreview();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

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

    }

    @Override
    public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
        if (mCamera != null) {
            mCamera.stopPreview();
            mCamera.release();
            mCamera = null;
        }
        return false;
    }

    @Override
    public void onSurfaceTextureUpdated(SurfaceTexture surface) {

    }

    @OnClick(R.id.btn_texture_anim)
    public void onViewClicked() {
        PropertyValuesHolder valuesHolder = PropertyValuesHolder.ofFloat("translationX", 0.0f, 0.0f);
        PropertyValuesHolder valuesHolder1 = PropertyValuesHolder.ofFloat("scaleX", 1.0f, 0.3f,1.0f);
        PropertyValuesHolder valuesHolder4 = PropertyValuesHolder.ofFloat("scaleY", 1.0f, 0.3f,1.0f);
        PropertyValuesHolder valuesHolder2 = PropertyValuesHolder.ofFloat("rotationX", 0.0f, 2 * 360.0f, 0.0F);
        PropertyValuesHolder valuesHolder5 = PropertyValuesHolder.ofFloat("rotationY", 0.0f, 2 * 360.0f, 0.0F);
        PropertyValuesHolder valuesHolder3 = PropertyValuesHolder.ofFloat("alpha", 1.0f, 0.7f, 1.0F);
        ObjectAnimator objectAnimator = ObjectAnimator.ofPropertyValuesHolder(mCameraTextureView, valuesHolder, valuesHolder1, valuesHolder2, valuesHolder3,valuesHolder4,valuesHolder5);
        objectAnimator.setDuration(5000).start();
    }
}
效果展示


4. 如何通過GlSurfaceView處理Camera預覽。
如果你在學習自定義相機,而且你的相機想要實現美顏,濾鏡,人臉識別AR場景 and so on。這時候你就必須要學習如何使用GlsurfaView羅。如果你沒有openGl的基本配置的知識或者你之前完全沒有學習過openGl的開發,再次強烈建議你看一下這篇文章 Android openGl開發詳解(一)——繪製簡單圖形,否則,下面內容可能會引起你的嚴重不適。當然,如果你還是覺得哪部分寫得不好或者有疑問的話,歡迎給我留言,或者關注同名微信號aserbao私聊我。

基本步驟
在xml中添加GlSurfaceView
創建渲染器類實現GlSurfaceView.Renderer
清除畫布,並創建一個紋理並綁定到。
創建一個用來最後顯示的SurfaceTexture來顯示處理後的數據。
創建Opengl ES程序並添加着色器到該程序中,創建openGl程序的可執行文件,並釋放shader資源。
打開攝像頭,並配置相關屬性。設置預覽視圖,並開啓預覽。
添加程序到ES環境中,並設置及啓用各類句柄。
在onDrawFrame中進行畫布的清理及繪製最新的數據到紋理圖形中。
設置一個SurfaceTexture.OnFrameAvailableListener的回調來通知GlSurfaceview渲染新的幀數據。
建議: 
GlSurfaceView作用簡單的理解OpenGl對相機數據進行處理完之後的顯示。我們需要明白的是渲染器的渲染週期及渲染方法的調用時機。 
1. onSurfaceCreated()當surface創建(第一次進入當前頁面)或者重新創建(切換後臺再進入)的時候調用。 
2. onSurfaceChanged()當surface大小發生改變的時候會被調用。 
3. onDrawFrame()繪製當前幀數據的時候被調用。

代碼部分
public class CameraGlSurfaceShowActivity extends AppCompatActivity implements SurfaceTexture.OnFrameAvailableListener {
    public SurfaceTexture mSurfaceTexture;

    public static Camera camera;
    private int camera_status = 1;
    @BindView(R.id.camera_glsurface_view)
    GLSurfaceView mCameraGlsurfaceView;
    public MyRender mRenderer;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_camera_gl_surface_show);
        ButterKnife.bind(this);
        mCameraGlsurfaceView.setEGLContextClientVersion(2);//在setRenderer()方法前調用此方法
        mRenderer = new MyRender();
        mCameraGlsurfaceView.setRenderer(mRenderer);
        mCameraGlsurfaceView.setRenderMode(RENDERMODE_WHEN_DIRTY);
    }


    @Override
    public void onFrameAvailable(SurfaceTexture surfaceTexture) {
        mCameraGlsurfaceView.requestRender();
    }

    @OnClick({R.id.btn_gl_surface_view_animator, R.id.btn_gl_surface_view_switch})
    public void onViewClicked(View view) {
        switch (view.getId()) {
            case R.id.btn_gl_surface_view_animator:
                PropertyValuesHolder valuesHolder1 = PropertyValuesHolder.ofFloat("scaleX", 1.0f, 0.5f,1.0f);
                PropertyValuesHolder valuesHolder4 = PropertyValuesHolder.ofFloat("scaleY", 1.0f, 0.5f,1.0f);
                PropertyValuesHolder valuesHolder5 = PropertyValuesHolder.ofFloat("rotationY", 0.0f, 360.0f, 0.0F);
                ObjectAnimator objectAnimator = ObjectAnimator.ofPropertyValuesHolder(mCameraGlsurfaceView, valuesHolder1, valuesHolder4,valuesHolder5);
                objectAnimator.setDuration(3000).start();
                break;
            case R.id.btn_gl_surface_view_switch:
                camera_status ^= 1;
                if (camera != null) {
                    camera.stopPreview();
                    camera.release();
                }
                mRenderer.mBoolean = true;
                camera = Camera.open(camera_status);
                try {
                    camera.setPreviewTexture(mSurfaceTexture);
                } catch (IOException e) {
                    e.printStackTrace();
                }
                camera.startPreview();
                break;
        }
    }

    public class MyRender implements GLSurfaceView.Renderer {
        private final String vertexShaderCode = "uniform mat4 textureTransform;\n" +
                "attribute vec2 inputTextureCoordinate;\n" +
                "attribute vec4 position;            \n" +//NDK座標點
                "varying   vec2 textureCoordinate; \n" +//紋理座標點變換後輸出
                "\n" +
                " void main() {\n" +
                "     gl_Position = position;\n" +
                "     textureCoordinate = inputTextureCoordinate;\n" +
                " }";

        private final String fragmentShaderCode = "#extension GL_OES_EGL_image_external : require\n" +
                "precision mediump float;\n" +
                "uniform samplerExternalOES videoTex;\n" +
                "varying vec2 textureCoordinate;\n" +
                "\n" +
                "void main() {\n" +
                 "    vec4 tc = texture2D(videoTex, textureCoordinate);\n" +
                "    float color = tc.r * 0.3 + tc.g * 0.59 + tc.b * 0.11;\n" +  //所有視圖修改成黑白
                "    gl_FragColor = vec4(color,color,color,1.0);\n" +
//                "    gl_FragColor = vec4(tc.r,tc.g,tc.b,1.0);\n" +
                "}\n";
        private FloatBuffer mPosBuffer;
        private FloatBuffer mTexBuffer;
        private float[] mPosCoordinate = {-1, -1, -1, 1, 1, -1, 1, 1};
        private float[] mTexCoordinateBackRight = {1, 1, 0, 1, 1, 0, 0, 0};//順時針轉90並沿Y軸翻轉  後攝像頭正確,前攝像頭上下顛倒
        private float[] mTexCoordinateForntRight = {0, 1, 1, 1, 0, 0, 1, 0};//順時針旋轉90  後攝像頭上下顛倒了,前攝像頭正確

        public int mProgram;
        public boolean mBoolean = false;

        public MyRender() {
            Matrix.setIdentityM(mProjectMatrix, 0);
            Matrix.setIdentityM(mCameraMatrix, 0);
            Matrix.setIdentityM(mMVPMatrix, 0);
            Matrix.setIdentityM(mTempMatrix, 0);
        }

        private int loadShader(int type, String shaderCode) {
            int shader = GLES20.glCreateShader(type);
            // 添加上面編寫的着色器代碼並編譯它
            GLES20.glShaderSource(shader, shaderCode);
            GLES20.glCompileShader(shader);
            return shader;
        }

        private void creatProgram() {
            //通常做法
//            String vertexSource = AssetsUtils.read(CameraGlSurfaceShowActivity.this, "vertex_texture.glsl");
//            int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexSource);
//            String fragmentSource = AssetsUtils.read(CameraGlSurfaceShowActivity.this, "fragment_texture.glsl");
//            int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource);
            int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexShaderCode);
            int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentShaderCode);
            // 創建空的OpenGL ES程序
            mProgram = GLES20.glCreateProgram();

            // 添加頂點着色器到程序中
            GLES20.glAttachShader(mProgram, vertexShader);

            // 添加片段着色器到程序中
            GLES20.glAttachShader(mProgram, fragmentShader);

            // 創建OpenGL ES程序可執行文件
            GLES20.glLinkProgram(mProgram);

            // 釋放shader資源
            GLES20.glDeleteShader(vertexShader);
            GLES20.glDeleteShader(fragmentShader);
        }

        private FloatBuffer convertToFloatBuffer(float[] buffer) {
            FloatBuffer fb = ByteBuffer.allocateDirect(buffer.length * 4)
                    .order(ByteOrder.nativeOrder())
                    .asFloatBuffer();
            fb.put(buffer);
            fb.position(0);
            return fb;
        }

        private int uPosHandle;
        private int aTexHandle;
        private int mMVPMatrixHandle;
        private float[] mProjectMatrix = new float[16];
        private float[] mCameraMatrix = new float[16];
        private float[] mMVPMatrix = new float[16];
        private float[] mTempMatrix = new float[16];

        //添加程序到ES環境中
        private void activeProgram() {
            // 將程序添加到OpenGL ES環境
            GLES20.glUseProgram(mProgram);

            mSurfaceTexture.setOnFrameAvailableListener(CameraGlSurfaceShowActivity.this);
            // 獲取頂點着色器的位置的句柄
            uPosHandle = GLES20.glGetAttribLocation(mProgram, "position");
            aTexHandle = GLES20.glGetAttribLocation(mProgram, "inputTextureCoordinate");
            mMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "textureTransform");

            mPosBuffer = convertToFloatBuffer(mPosCoordinate);
            if(camera_status == 0){
                mTexBuffer = convertToFloatBuffer(mTexCoordinateBackRight);
            }else{
                mTexBuffer = convertToFloatBuffer(mTexCoordinateForntRight);
            }

            GLES20.glVertexAttribPointer(uPosHandle, 2, GLES20.GL_FLOAT, false, 0, mPosBuffer);
            GLES20.glVertexAttribPointer(aTexHandle, 2, GLES20.GL_FLOAT, false, 0, mTexBuffer);

            // 啓用頂點位置的句柄
            GLES20.glEnableVertexAttribArray(uPosHandle);
            GLES20.glEnableVertexAttribArray(aTexHandle);
        }

        @Override
        public void onSurfaceCreated(GL10 gl, EGLConfig config) {
            GLES20.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
            mSurfaceTexture = new SurfaceTexture(createOESTextureObject());
            creatProgram();
//            mProgram = ShaderUtils.createProgram(CameraGlSurfaceShowActivity.this, "vertex_texture.glsl", "fragment_texture.glsl");
            camera = Camera.open(camera_status);
            try {
                camera.setPreviewTexture(mSurfaceTexture);
                camera.startPreview();
            } catch (IOException e) {
                e.printStackTrace();
            }
            activeProgram();

        }

        @Override
        public void onSurfaceChanged(GL10 gl, int width, int height) {
            GLES20.glViewport(0, 0, width, height);
            Matrix.scaleM(mMVPMatrix,0,1,-1,1);
            float ratio = (float) width / height;
            Matrix.orthoM(mProjectMatrix, 0, -1, 1, -ratio, ratio, 1, 7);// 3和7代表遠近視點與眼睛的距離,非座標點
            Matrix.setLookAtM(mCameraMatrix, 0, 0, 0, 3, 0f, 0f, 0f, 0f, 1.0f, 0.0f);// 3代表眼睛的座標點
            Matrix.multiplyMM(mMVPMatrix, 0, mProjectMatrix, 0, mCameraMatrix, 0);
        }

        @Override
        public void onDrawFrame(GL10 gl) {
            if(mBoolean){
                activeProgram();
                mBoolean = false;
            }
            if (mSurfaceTexture != null) {
                GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);
                mSurfaceTexture.updateTexImage();
                GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mMVPMatrix, 0);
                GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, mPosCoordinate.length / 2);
            }
        }
    }

    public static int createOESTextureObject() {
        int[] tex = new int[1];
        //生成一個紋理
        GLES20.glGenTextures(1, tex, 0);
        //將此紋理綁定到外部紋理上
        GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, tex[0]);
        //設置紋理過濾參數
        GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
                GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_NEAREST);
        GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
                GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR);
        GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
                GL10.GL_TEXTURE_WRAP_S, GL10.GL_CLAMP_TO_EDGE);
        GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
                GL10.GL_TEXTURE_WRAP_T, GL10.GL_CLAMP_TO_EDGE);
        GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, 0);
        return tex[0];
    }
}

當然,大多數情況下渲染頂點着色器及片段着色器的代碼會編寫一個glsl的文件放到assets目錄下進行訪問。 
下面是另外一個操作方式:

vertex_texture.glsl文件

uniform mat4 textureTransform;
attribute vec2 inputTextureCoordinate;
attribute vec4 position;            //NDK座標點
varying   vec2 textureCoordinate; //紋理座標點變換後輸出

 void main() {
     gl_Position = position;
     textureCoordinate = inputTextureCoordinate;
 }

fragment_texture.glsl文件:

#extension GL_OES_EGL_image_external : require
precision mediump float;
uniform samplerExternalOES videoTex;
varying vec2 textureCoordinate;

void main() {
    vec4 tc = texture2D(videoTex, textureCoordinate);
    float color = tc.r * 0.3 + tc.g * 0.59 + tc.b * 0.11;//這裏進行的顏色變換處理,傳說中的黑白濾鏡。
    gl_FragColor = vec4(color,color,color,1.0);
}

讀取文件內容方式:

public static String read(Context context, String fileName) {
        String result = null;
        try {
            InputStream is = context.getResources().getAssets().open("Shader/" + fileName);
            int length = is.available();
            byte[] buffer = new byte[length];
            is.read(buffer);
            result = new String(buffer, "utf-8");
        } catch (IOException e) {
            e.printStackTrace();
        }

        return result;
    }
具體實現在上面代碼creatProgram()下注釋掉通常做法的那部分。

實現效果


遇到的問題
使用OpenGl處理數據之後再GlsurfaceView上圖片鏡像了,咋辦?這種問題要麼出在繪製頂點座標,要麼出在源數據上,但是通過其他的比如SurfaceView直接預覽數據沒問題,那麼只能是第一種可能了。仔細回看下代碼,果然修改渲染顏色矩陣顯示出來的數據方向都是不同的。
下面是我改動矩陣產生的結果: 


問題根本找到了,如何解決呢?起始點就四個點,一個點就最多三個連接點,而且起始點不能和不相鄰的點進行連接(一旦連接就會出現上面的第二種情況),所以最後4個點只有8種可能性,將所有點的都試一遍,得出如下結論(經過幾輪測試,基本結論如下):

還有一個關於前後攝像頭的切換的問題,我上面的做法是在點擊切換攝像頭操作的時候只針對攝像頭進行了釋放重啓操作,直接在onDrawFrame方法中對渲染矩陣進行了修改,沒有對SurfaceTexture進行數據清除(具體看上面代碼)。然而也看了一些主流的第三方Demo,這裏不列出名字了。他們的做法是攝像頭和surfaceTexture一塊釋放。當然,兩種方式都可以,我上面的那種方式暫時沒找到什麼問題,而且我通過實測比第二種方式看到相機數據的時間要快一點。
運行效果


5. 總結
弄清楚沒個步驟之後你就會發現之前感覺無從下手的東西也就這樣。沒什麼大不了的。 


個人建議: 
做相機項目,最好能將每個步驟都弄清楚,邏輯理清楚了會節省很大一部分時間。剛開始的時候在做這部分是拿別人一個Demo,出錯了,無從下手,又去百度找解決辦法,浪費了一大堆時間,整個項目做完了,還是沒有對此有個深刻的認識,真要說還說不出個所以然來。重新整理下,會瞭解到很多東西。

最後,不多說了,如果有什麼不懂的地方可以給我留言或者關注同名微信號aserbao私聊我。如果覺得寫得不錯的話就給個讚唄。忘了忘了,還有源碼和關注,看樓下。

6. 源碼鏈接
Android’s Android如果覺得對你有用的話幫忙給個star吧 
————————————————
版權聲明:本文爲CSDN博主「aserbao」的原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/qq_32175491/article/details/79755424

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章