Android使用OpenGL和MediaCodec錄制功能
一,什么是opengl
Open Graphics Library 圖形領(lǐng)域的工業(yè)標準,是一套跨編程語言、跨平臺的、專業(yè)的圖形編程(軟件)接口。它用于二維、三維圖像,是一個功能強大,調(diào)用方便的底層圖形庫。
與硬件無關(guān)??梢栽诓煌钠脚_如Windows、Linux、Mac、Android、IOS之間進行移植。因此,支持OpenGL的軟件具有很好的移植性,可以獲得非常廣泛的應(yīng)用。
OpenGL ES 1.0 和 1.1 :Android 1.0和更高的版本支持這個API規(guī)范。 OpenGL ES 2.0 :Android 2.2(API 8)和更高的版本支持這個API規(guī)范。 OpenGL ES 3.0 :Android 4.3(API 18)和更高的版本支持這個API規(guī)范。 OpenGL ES 3.1 : Android 5.0(API 21)和更高的版本支持這個API規(guī)范
還須要由設(shè)備制造商提供了實現(xiàn)支持 目前廣泛支持的是2.0 <uses-feature android:glEsVersion="0x00020000" android:required="true"/>
二,什么是Android OpenGL ES
針對手機、PDA和游戲主機等嵌入式設(shè)備而設(shè)計的OpenGL API 子集。
GLSurfaceView
繼承至SurfaceView,它內(nèi)嵌的surface專門負責OpenGL渲染。 管理Surface與EGL 允許自定義渲染器(render)。 讓渲染器在獨立的線程里運作,和UI線程分離。 支持按需渲染(on-demand)和連續(xù)渲染(continuous)。
OpenGL是一個跨平臺的操作GPU的API,但OpenGL需要本地視窗系統(tǒng)進行交互,這就需要一個中間控制層, EGL就是連接OpenGL ES和本地窗口系統(tǒng)的接口,引入EGL就是為了屏蔽不同平臺上的區(qū)別。
三, OpenGL 繪制流程
其實就是一個可編程管線pipline
渲染管線做的事情就是讓計算機完成圖形功能。 固定管線:程序員只能設(shè)置參數(shù)。比如 f(x)=axx + bx + c程序員只能設(shè)置a,b,c的值,卻不能修改這個公式。 可編程管線:程序員掌控一切。
四, OpenGL坐標系
五, OpenGL 著色器
著色器(Shader)是運行在GPU上的小程序。頂點著色器(vertex shader) 如何處理頂點、法線等數(shù)據(jù)的小程序。片元著色器(fragment shader) 如何處理光、陰影、遮擋、環(huán)境等等對物體表面的影響,最終生成一副圖像的小程序
六, GLSL編程語言
七,使用MediaCodec錄制在Opengl中渲染架構(gòu)
八,代碼實現(xiàn)
8.1 自定義渲染view繼承GLSurfaceView
public class TigerView extends GLSurfaceView { private TigerRender mTigerRender; //默認正常速度 private Speed mSpeed = Speed.MODE_NORMAL; public void setSpeed(Speed speed) { mSpeed = speed; } public enum Speed { MODE_EXTRA_SLOW, MODE_SLOW, MODE_NORMAL, MODE_FAST, MODE_EXTRA_FAST } public TigerView(Context context) { super(context); } public TigerView(Context context, AttributeSet attrs) { super(context, attrs); /** * 設(shè)置egl版本 */ setEGLContextClientVersion(2); /** * 設(shè)置渲染器 */ mTigerRender = new TigerRender(this); setRenderer(mTigerRender); /** * 設(shè)置按需渲染,當我們調(diào)用requestRender()的時候就會調(diào)用GlThread回調(diào)一次onDrawFrame() */ setRenderMode(RENDERMODE_WHEN_DIRTY); } public void startRecord() { float speed = 1.f; switch (mSpeed) { case MODE_EXTRA_SLOW: speed = 0.3f; break; case MODE_SLOW: speed = 0.5f; break; case MODE_NORMAL: speed = 1.f; break; case MODE_FAST: speed = 1.5f; break; case MODE_EXTRA_FAST: speed = 3.f; break; } mTigerRender.startRecord(speed); } public void stopRecord() { mTigerRender.stopRecord(); } }
8.2 自定義渲染器TigerRender
public class TigerRender implements GLSurfaceView.Renderer, SurfaceTexture.OnFrameAvailableListener { private final TigerView mView; private CameraHelper mCameraHelper; private SurfaceTexture mSurfaceTexture; float[] mtx = new float[16]; private ScreeFilter mScreeFilter; private int[] mTextures; private CameraFilter mCameraFilter; private MediaRecorder mMediaRecorder; public TigerRender(TigerView tigerView) { mView = tigerView; } /** * 畫布創(chuàng)建好了 * * @param gl the GL interface. Use <code>instanceof</code> to * test if the interface supports GL11 or higher interfaces. * @param config the EGLConfig of the created surface. Can be used * to create matching pbuffers. */ @Override public void onSurfaceCreated(GL10 gl, EGLConfig config) { mCameraHelper = new CameraHelper(Camera.CameraInfo.CAMERA_FACING_BACK); //準備好攝像頭繪制的畫布 //通過open gl創(chuàng)建一個紋理id mTextures = new int[1]; GLES20.glGenTextures(mTextures.length, mTextures, 0); mSurfaceTexture = new SurfaceTexture(mTextures[0]); //設(shè)置有一幀新的數(shù)據(jù)到來的時候,回調(diào)監(jiān)聽 mSurfaceTexture.setOnFrameAvailableListener(this); //必須要在GlThread里面創(chuàng)建著色器程序 mCameraFilter = new CameraFilter(mView.getContext()); mScreeFilter = new ScreeFilter(mView.getContext()); EGLContext eglContext = EGL14.eglGetCurrentContext(); mMediaRecorder = new MediaRecorder(mView.getContext(), "/mnt/sdcard/test.mp4", CameraHelper.HEIGHT, CameraHelper.WIDTH, eglContext); } /** * 畫布發(fā)生改變 * * @param gl the GL interface. Use <code>instanceof</code> to * test if the interface supports GL11 or higher interfaces. * @param width * @param height */ @Override public void onSurfaceChanged(GL10 gl, int width, int height) { mCameraHelper.startPreview(mSurfaceTexture); mCameraFilter.onReady(width, height); mScreeFilter.onReady(width, height); } /** * 畫畫 * * @param gl the GL interface. Use <code>instanceof</code> to * test if the interface supports GL11 or higher interfaces. */ @Override public void onDrawFrame(GL10 gl) { //告訴open gl需要把屏幕清理成 什么樣子的顏色 GLES20.glClearColor(0, 0, 0, 0); //開始真正的屏幕顏色清理,也就是上一次設(shè)置的屏幕顏色 GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); //把攝像頭采集的數(shù)據(jù)輸出出來 //更新紋理,然后我們才可以使用opengl從SurfaceTexure當中獲取數(shù)據(jù),進行渲染 mSurfaceTexture.updateTexImage(); //mSurfaceTexture比較特殊,在設(shè)置坐標的時候,需要一個變換矩陣,使用的是特殊的采樣器samplerExternalOES //這種采樣器,正常的是sample2D mSurfaceTexture.getTransformMatrix(mtx); mCameraFilter.setMatrix(mtx); int id = mCameraFilter.onDrawFrame(mTextures[0]); //在這里添加各種效果,相當于責任鏈 //開始畫畫 mScreeFilter.onDrawFrame(id); mMediaRecorder.encodeFrame(id, mSurfaceTexture.getTimestamp()); } /** * SurfaceTexture有一個新的有效的圖片的時候會被回調(diào),此時可以把這個數(shù)據(jù)回調(diào)給GLSurfaceView的onDrawFrame * * @param surfaceTexture */ @Override public void onFrameAvailable(SurfaceTexture surfaceTexture) { if (mView != null) { //開始渲染,有一幀新的圖像,就開始調(diào)用GLSurfaceView的onDrawFrame進行繪制 mView.requestRender(); } } public void startRecord(float speed) { try { mMediaRecorder.start(speed); } catch (IOException e) { throw new RuntimeException(e); } } public void stopRecord() { mMediaRecorder.stop(); } }
8.3 創(chuàng)建編碼器MediaRecorder
/** * 視頻錄制 */ public class MediaRecorder { private final Context mContext; private final String mPath; private final int mWidth; private final int mHeight; private final EGLContext mEglContext; private MediaCodec mMediaCodec; private Surface mInputSurface; private MediaMuxer mMediaMuxer; private Handler mHandler; private EGLBase mEglBase; private boolean isStart; private int index; private float mSpeed; /** * @param context * @param path 視頻保存地址 * @param width 視頻寬 * @param height 視頻高 */ public MediaRecorder(Context context, String path, int width, int height, EGLContext eglContext) { mContext = context.getApplicationContext(); mPath = path; mWidth = width; mHeight = height; mEglContext = eglContext; } /** * 開始錄制視頻 */ public void start(float speed) throws IOException { /** * 配置MediaCodec編碼器,視頻編碼的寬,高,幀率,碼率 * 錄制成mp4格式,視頻編碼格式是h264 MIMETYPE_VIDEO_AVC 高級編碼 */ mSpeed = speed; MediaFormat videoFormat = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, mWidth, mHeight); //配置碼率 1500kbs videoFormat.setInteger(MediaFormat.KEY_BIT_RATE, 1500_000); //幀率 videoFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 20); //關(guān)鍵字間隔 videoFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 20); //創(chuàng)建視頻高級編碼器 mMediaCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC); //因為是從Surface中讀取的,所以不需要設(shè)置這個顏色格式 videoFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); //配置編碼器,CONFIGURE_FLAG_ENCODE, mMediaCodec.configure(videoFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); //交給虛擬屏幕,通過opengl 將預(yù)覽的紋理繪制到這一個虛擬屏幕中,這樣子Mediacodc 就會自動編碼這一幀圖像 mInputSurface = mMediaCodec.createInputSurface(); //mp4 播放流程 解復(fù)用--》解碼 》繪制 //mp4 編碼流程 封裝器--》編碼 //MUXER_OUTPUT_MPEG_4 MP4格式封裝器,將h.264通過他寫出到文件就可以了 mMediaMuxer = new MediaMuxer(mPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4); /** * 配置EGL環(huán)境,也就是配置我們的虛擬屏幕環(huán)境 */ HandlerThread handlerThread = new HandlerThread("ViewoCodec"); handlerThread.start(); Looper looper = handlerThread.getLooper(); //子線程和子線程之間的通信 mHandler = new Handler(looper); //EGl的綁定線程,對我們自己創(chuàng)建的EGl環(huán)境,都是在這個線程里面進行 mHandler.post(() -> { //創(chuàng)建我們的EGL環(huán)境(虛擬設(shè)備,EGL上下文 ) mEglBase = new EGLBase(mContext, mWidth, mHeight, mInputSurface, mEglContext); //啟動編碼器 mMediaCodec.start(); isStart = true; }); } /** * textureId 紋理id * 調(diào)用一次,就有一個新的圖片需要編碼 */ public void encodeFrame(int textureId, long timesnap) { if (!isStart) { return; } //切換到子線程中編碼 mHandler.post(() -> { //把圖像紋理畫到虛擬屏幕里面 mEglBase.draw(textureId, timesnap); //此時我們需要從編碼器里面的輸出緩沖區(qū)獲取編碼以后的數(shù)據(jù)就可以了, getCodec(false); }); } private void getCodec(boolean endOfStream) { if (endOfStream) { //表示停止錄制,此時我們不錄制了,需要給mediacoic 通知 mMediaCodec.signalEndOfInputStream(); } MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); //希望將已經(jīng)編碼完成的數(shù)據(jù)都獲取到,然后寫出到mp4文件中 while (true) { //并不是傳給mediacodec一幀數(shù)據(jù)就表示可以編碼出一幀數(shù)據(jù),有可能需要好多幀數(shù)據(jù)才可以同時編碼出數(shù)據(jù)出來 //輸出緩沖區(qū) //傳遞-1表示一直等到輸出緩沖區(qū)有一個編碼好的有效的數(shù)據(jù)以后才會繼續(xù)向下走,不然就會一直卡在127行, //10_000超時時間 int status = mMediaCodec.dequeueOutputBuffer(bufferInfo, 10_000); if (status == MediaCodec.INFO_TRY_AGAIN_LATER) { //如果是停止,就繼續(xù)循環(huán), // 繼續(xù)循環(huán)就表示不會接收到新的等待編碼的圖像了 //相當于保證mediacodic中所有待編碼的數(shù)據(jù)都編碼完成 // 標記不是停止,我們退出,下一輪接收到更多的數(shù)據(jù)才來輸出編碼以后的數(shù)據(jù),我們就讓繼續(xù)走 // 表示需要更多數(shù)據(jù)才可以編碼出圖像 false是繼續(xù)錄制,未來還有機會在調(diào)用getCodec if (!endOfStream) { //結(jié)束錄制了 break; } //否則繼續(xù) } else if (status == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { //開始編碼,就會調(diào)用一次 MediaFormat outputFormat = mMediaCodec.getOutputFormat(); //配置封裝器,增加一路指定格式的媒體流 index = mMediaMuxer.addTrack(outputFormat); //啟動封裝器 mMediaMuxer.start(); } else if (status == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) { //忽略 } else { //成功取出一個有效的輸出 ByteBuffer outputBuffer = mMediaCodec.getOutputBuffer(status); if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) { //如果獲取的ByteBuffer是配置信息,那么就不需要寫出到mp4文件中 bufferInfo.size = 0; } if (bufferInfo.size != 0) { bufferInfo.presentationTimeUs = (long) (bufferInfo.presentationTimeUs / mSpeed); //寫出到 mp4文件中 //根據(jù)偏移定位去獲取數(shù)據(jù),而不是從0開始 outputBuffer.position(bufferInfo.offset); //設(shè)置可讀可寫的總長度 outputBuffer.limit(bufferInfo.offset + bufferInfo.size); mMediaMuxer.writeSampleData(index, outputBuffer, bufferInfo); } //輸出緩沖區(qū)使用完畢了, 此時就可以回收了,讓mediacodec繼續(xù)使用 mMediaCodec.releaseOutputBuffer(status, false); if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) { //結(jié)束了, break; } } } } public void stop() { isStart = false; mHandler.post(() -> { getCodec(true); mMediaCodec.stop(); mMediaCodec.release(); mMediaCodec = null; mMediaMuxer.stop(); mMediaMuxer.release(); mMediaMuxer = null; mEglBase.release(); mEglBase = null; mInputSurface = null; mHandler.getLooper().quitSafely(); mHandler = null; }); } }
8.4 配置egl環(huán)境
/** * EGL配置 和錄制opengl的操作 */ public class EGLBase { private final EGLSurface mEglSurface; private final ScreeFilter mScreeFilter; private EGLDisplay mEglDisplay; private EGLConfig mEglConfig; private EGLContext mEGLContext; /** * @param context * @param width * @param height * @param surface MediaCodec創(chuàng)建的surface, 我們需要將這個surface貼到虛擬屏幕里面 */ public EGLBase(Context context, int width, int height, Surface surface, EGLContext eglContext) { createEGL(eglContext); //把surface貼到EGLDisplay 虛擬屏幕里面 int[] attrib_list = { //不需要配置什么屬性 EGL14.EGL_NONE}; //就是向mEglDisplay這個虛擬屏幕上面畫畫 mEglSurface = EGL14.eglCreateWindowSurface(mEglDisplay, mEglConfig, surface, attrib_list, 0); //必須要綁定當前線程的顯示上下文,不然就繪制不上去,這樣子之后操作的opelgl就是在這個虛擬屏幕上操作,讀和寫都是在同一個surface里面 if (!EGL14.eglMakeCurrent(mEglDisplay, mEglSurface, mEglSurface, mEGLContext)) { throw new RuntimeException("eglMakeCurrent failed"); } //向虛擬屏幕畫畫 mScreeFilter = new ScreeFilter(context); mScreeFilter.onReady(width, height); } private void createEGL(EGLContext eglContext) { //創(chuàng)建虛擬顯示器 mEglDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY); if (mEglDisplay == EGL14.EGL_NO_DISPLAY) { throw new RuntimeException("eglGetDisplay failed"); } int[] version = new int[2]; //初始化虛擬設(shè)備 if (!EGL14.eglInitialize(mEglDisplay, version, 0, version, 1)) { throw new RuntimeException("eglInitialize failed"); } int[] attrib_list = new int[]{//rgba 紅綠藍透明度 EGL14.EGL_RED_SIZE, 8, EGL14.EGL_GREEN_SIZE, 8, EGL14.EGL_BLUE_SIZE, 8, EGL14.EGL_ALPHA_SIZE, 8, EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT,//和egl的版本有關(guān)系 EGL14.EGL_NONE//這個很重要,一定要配置為NONE,表示配置結(jié)束了 }; EGLConfig[] configs = new EGLConfig[1]; int[] num_config = new int[1]; boolean eglChooseConfig = EGL14.eglChooseConfig(mEglDisplay, attrib_list, 0, configs, 0, configs.length, num_config, 0); if (!eglChooseConfig) { //如果配置失敗 throw new IllegalArgumentException("eglChooseConfig failed"); } mEglConfig = configs[0]; int[] attriblist = {EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, EGL14.EGL_NONE}; //創(chuàng)建EGL上下文 //share_context 共享上下,傳遞繪制線程的(GLThread)EGL上下文,達到共享資源的目的 mEGLContext = EGL14.eglCreateContext(mEglDisplay, mEglConfig, eglContext, attriblist, 0); if (mEGLContext == null || mEGLContext == EGL14.EGL_NO_CONTEXT) { mEGLContext = null; throw new RuntimeException("createContex error !"); } } /** * @param textureId 紋理id,代表一張圖片 * @param timesnap 時間戳 */ public void draw(int textureId, long timesnap) { //必須要綁定當前線程的顯示上下文,不然就繪制不上去,這樣子之后操作的opelgl就是在這個虛擬屏幕上操作,讀和寫都是在同一個surface里面 //畫畫之前也必須要綁定 if (!EGL14.eglMakeCurrent(mEglDisplay, mEglSurface, mEglSurface, mEGLContext)) { throw new RuntimeException("eglMakeCurrent failed"); } //向虛擬屏幕畫畫 mScreeFilter.onDrawFrame(textureId); //刷新eglSurface時間戳 EGLExt.eglPresentationTimeANDROID(mEglDisplay, mEglSurface, timesnap); //交換數(shù)據(jù),EGL工作模式,雙緩存模式,內(nèi)部有兩個frameBuff,當EGL將一個frame顯示到屏幕上以后, // 另一個frame就在后臺等待opengl進行交換 //也就是畫完一次,交換一次 EGL14.eglSwapBuffers(mEglDisplay, mEglSurface); } public void release() { EGL14.eglDestroySurface(mEglDisplay, mEglSurface); EGL14.eglMakeCurrent(mEglDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_CONTEXT); EGL14.eglDestroyContext(mEglDisplay, mEGLContext); EGL14.eglReleaseThread(); EGL14.eglTerminate(mEglDisplay); } }
8.5 配置shader
public class AbstractFilter { protected FloatBuffer mGLVertexBuffer; protected FloatBuffer mGLTextureBuffer; //頂點著色 protected int mVertexShaderId; //片段著色 protected int mFragmentShaderId; protected int mGLProgramId; /** * 頂點著色器 * attribute vec4 position; * 賦值給gl_Position(頂點) */ protected int vPosition; /** * varying vec2 textureCoordinate; */ protected int vCoord; /** * uniform mat4 vMatrix; */ protected int vMatrix; /** * 片元著色器 * Samlpe2D 擴展 samplerExternalOES */ protected int vTexture; protected int mOutputWidth; protected int mOutputHeight; public AbstractFilter(Context context, int vertexShaderId, int fragmentShaderId) { this.mVertexShaderId = vertexShaderId; this.mFragmentShaderId = fragmentShaderId; // 4個點 x,y = 4*2 float 4字節(jié) 所以 4*2*4 mGLVertexBuffer = ByteBuffer.allocateDirect(4 * 2 * 4).order(ByteOrder.nativeOrder()).asFloatBuffer(); mGLVertexBuffer.clear(); float[] VERTEX = {-1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f}; mGLVertexBuffer.put(VERTEX); mGLTextureBuffer = ByteBuffer.allocateDirect(4 * 2 * 4).order(ByteOrder.nativeOrder()).asFloatBuffer(); mGLTextureBuffer.clear(); float[] TEXTURE = {0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f}; mGLTextureBuffer.put(TEXTURE); initilize(context); initCoordinate(); } protected void initilize(Context context) { String vertexSharder = OpenGLUtils.readRawTextFile(context, mVertexShaderId); String framentShader = OpenGLUtils.readRawTextFile(context, mFragmentShaderId); mGLProgramId = OpenGLUtils.loadProgram(vertexSharder, framentShader); // 獲得著色器中的 attribute 變量 position 的索引值 vPosition = GLES20.glGetAttribLocation(mGLProgramId, "vPosition"); vCoord = GLES20.glGetAttribLocation(mGLProgramId, "vCoord"); vMatrix = GLES20.glGetUniformLocation(mGLProgramId, "vMatrix"); // 獲得Uniform變量的索引值 vTexture = GLES20.glGetUniformLocation(mGLProgramId, "vTexture"); } public void onReady(int width, int height) { mOutputWidth = width; mOutputHeight = height; } public void release() { GLES20.glDeleteProgram(mGLProgramId); } public int onDrawFrame(int textureId) { //設(shè)置顯示窗口 GLES20.glViewport(0, 0, mOutputWidth, mOutputHeight); //使用著色器 GLES20.glUseProgram(mGLProgramId); //傳遞坐標 mGLVertexBuffer.position(0); GLES20.glVertexAttribPointer(vPosition, 2, GLES20.GL_FLOAT, false, 0, mGLVertexBuffer); GLES20.glEnableVertexAttribArray(vPosition); mGLTextureBuffer.position(0); GLES20.glVertexAttribPointer(vCoord, 2, GLES20.GL_FLOAT, false, 0, mGLTextureBuffer); GLES20.glEnableVertexAttribArray(vCoord); GLES20.glActiveTexture(GLES20.GL_TEXTURE0); //綁定 GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId); GLES20.glUniform1i(vTexture, 0); GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); //解綁 GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0); return textureId; } //修改坐標 protected void initCoordinate() { } }
到此這篇關(guān)于Android使用OpenGL和MediaCodec錄制的文章就介紹到這了,更多相關(guān)Android使用OpenGL和MediaCodec內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Android編程之界面跳動提示動畫效果實現(xiàn)方法
這篇文章主要介紹了Android編程之界面跳動提示動畫效果實現(xiàn)方法,實例分析了Android動畫效果的布局及功能相關(guān)實現(xiàn)技巧,具有一定參考借鑒價值,需要的朋友可以參考下2015-11-11解決Android Studio 格式化 Format代碼快捷鍵問題
這篇文章主要介紹了解決Android Studio 格式化 Format代碼快捷鍵問題,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-03-03Android Studio 合并module到統(tǒng)一文件夾的方法
這篇文章主要介紹了Android Studio 合并module到統(tǒng)一文件夾的方法,補充介紹了android studio關(guān)于同名資源文件的合并技巧,需要的朋友可以參考下2018-04-04設(shè)置Android系統(tǒng)永不鎖屏永不休眠的方法
在進行Android系統(tǒng)開發(fā)的時候,有些特定的情況需要設(shè)置系統(tǒng)永不鎖屏,永不休眠。本篇文章給大家介紹Android 永不鎖屏,開機不鎖屏,刪除設(shè)置中休眠時間選項,需要的朋友一起學(xué)習吧2016-03-03Android開發(fā)之TimePicker控件用法實例詳解
這篇文章主要介紹了Android開發(fā)之TimePicker控件用法,結(jié)合實例形式詳細分析了Android項目的建立及TimePicker控件的具體使用技巧,需要的朋友可以參考下2016-02-02