Android錄音--AudioRecord、MediaRecorder的使用
Android提供了兩個(gè)API用于實(shí)現(xiàn)錄音功能:android.media.AudioRecord、android.media.MediaRecorder。
網(wǎng)上有很多談?wù)撨@兩個(gè)類的資料?,F(xiàn)在大致總結(jié)下:
1、AudioRecord
主要是實(shí)現(xiàn)邊錄邊播(AudioRecord+AudioTrack)以及對(duì)音頻的實(shí)時(shí)處理(如會(huì)說(shuō)話的湯姆貓、語(yǔ)音)
優(yōu)點(diǎn):語(yǔ)音的實(shí)時(shí)處理,可以用代碼實(shí)現(xiàn)各種音頻的封裝
缺點(diǎn):輸出是PCM語(yǔ)音數(shù)據(jù),如果保存成音頻文件,是不能夠被播放器播放的,所以必須先寫代碼實(shí)現(xiàn)數(shù)據(jù)編碼以及壓縮
示例:
使用AudioRecord類錄音,并實(shí)現(xiàn)WAV格式封裝。錄音20s,輸出的音頻文件大概為3.5M左右(已寫測(cè)試代碼)
2、MediaRecorder
已經(jīng)集成了錄音、編碼、壓縮等,支持少量的錄音音頻格式,大概有.aac(API = 16) .amr .3gp
優(yōu)點(diǎn):大部分以及集成,直接調(diào)用相關(guān)接口即可,代碼量小
缺點(diǎn):無(wú)法實(shí)時(shí)處理音頻;輸出的音頻格式不是很多,例如沒(méi)有輸出mp3格式文件
示例:
使用MediaRecorder類錄音,輸出amr格式文件。錄音20s,輸出的音頻文件大概為33K(已寫測(cè)試代碼)
3、音頻格式比較
WAV格式:錄音質(zhì)量高,但是壓縮率小,文件大
AAC格式:相對(duì)于mp3,AAC格式的音質(zhì)更佳,文件更小;有損壓縮;一般蘋果或者Android SDK4.1.2(API 16)及以上版本支持播放
AMR格式:壓縮比比較大,但相對(duì)其他的壓縮格式質(zhì)量比較差,多用于人聲,通話錄音
至于常用的mp3格式,使用MediaRecorder沒(méi)有該視頻格式輸出。一些人的做法是使用AudioRecord錄音,然后編碼成wav格式,再轉(zhuǎn)換成mp3格式
再貼上一些測(cè)試工程。
功能描述:
1、點(diǎn)擊“錄音WAV文件”,開(kāi)始錄音。錄音完成后,生成文件/sdcard/FinalAudio.wav
2、點(diǎn)擊“錄音AMR文件”,開(kāi)始錄音。錄音完成后,生成文件/sdcard/FinalAudio.amr
3、點(diǎn)擊“停止錄音”,停止錄音,并顯示錄音輸出文件以及該文件大小。
大致代碼如下:
1、AudioRecord錄音,封裝成WAV格式
package com.example.audiorecordtest; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import android.media.AudioFormat; import android.media.AudioRecord; public class AudioRecordFunc { // 緩沖區(qū)字節(jié)大小 private int bufferSizeInBytes = 0; //AudioName裸音頻數(shù)據(jù)文件 ,麥克風(fēng) private String AudioName = ""; //NewAudioName可播放的音頻文件 private String NewAudioName = ""; private AudioRecord audioRecord; private boolean isRecord = false;// 設(shè)置正在錄制的狀態(tài) private static AudioRecordFunc mInstance; private AudioRecordFunc(){ } public synchronized static AudioRecordFunc getInstance() { if(mInstance == null) mInstance = new AudioRecordFunc(); return mInstance; } public int startRecordAndFile() { //判斷是否有外部存儲(chǔ)設(shè)備sdcard if(AudioFileFunc.isSdcardExit()) { if(isRecord) { return ErrorCode.E_STATE_RECODING; } else { if(audioRecord == null) creatAudioRecord(); audioRecord.startRecording(); // 讓錄制狀態(tài)為true isRecord = true; // 開(kāi)啟音頻文件寫入線程 new Thread(new AudioRecordThread()).start(); return ErrorCode.SUCCESS; } } else { return ErrorCode.E_NOSDCARD; } } public void stopRecordAndFile() { close(); } public long getRecordFileSize(){ return AudioFileFunc.getFileSize(NewAudioName); } private void close() { if (audioRecord != null) { System.out.println("stopRecord"); isRecord = false;//停止文件寫入 audioRecord.stop(); audioRecord.release();//釋放資源 audioRecord = null; } } private void creatAudioRecord() { // 獲取音頻文件路徑 AudioName = AudioFileFunc.getRawFilePath(); NewAudioName = AudioFileFunc.getWavFilePath(); // 獲得緩沖區(qū)字節(jié)大小 bufferSizeInBytes = AudioRecord.getMinBufferSize(AudioFileFunc.AUDIO_SAMPLE_RATE, AudioFormat.CHANNEL_IN_STEREO, AudioFormat.ENCODING_PCM_16BIT); // 創(chuàng)建AudioRecord對(duì)象 audioRecord = new AudioRecord(AudioFileFunc.AUDIO_INPUT, AudioFileFunc.AUDIO_SAMPLE_RATE, AudioFormat.CHANNEL_IN_STEREO, AudioFormat.ENCODING_PCM_16BIT, bufferSizeInBytes); } class AudioRecordThread implements Runnable { @Override public void run() { writeDateTOFile();//往文件中寫入裸數(shù)據(jù) copyWaveFile(AudioName, NewAudioName);//給裸數(shù)據(jù)加上頭文件 } } /** * 這里將數(shù)據(jù)寫入文件,但是并不能播放,因?yàn)锳udioRecord獲得的音頻是原始的裸音頻, * 如果需要播放就必須加入一些格式或者編碼的頭信息。但是這樣的好處就是你可以對(duì)音頻的 裸數(shù)據(jù)進(jìn)行處理,比如你要做一個(gè)愛(ài)說(shuō)話的TOM * 貓?jiān)谶@里就進(jìn)行音頻的處理,然后重新封裝 所以說(shuō)這樣得到的音頻比較容易做一些音頻的處理。 */ private void writeDateTOFile() { // new一個(gè)byte數(shù)組用來(lái)存一些字節(jié)數(shù)據(jù),大小為緩沖區(qū)大小 byte[] audiodata = new byte[bufferSizeInBytes]; FileOutputStream fos = null; int readsize = 0; try { File file = new File(AudioName); if (file.exists()) { file.delete(); } fos = new FileOutputStream(file);// 建立一個(gè)可存取字節(jié)的文件 } catch (Exception e) { e.printStackTrace(); } while (isRecord == true) { readsize = audioRecord.read(audiodata, 0, bufferSizeInBytes); if (AudioRecord.ERROR_INVALID_OPERATION != readsize && fos!=null) { try { fos.write(audiodata); } catch (IOException e) { e.printStackTrace(); } } } try { if(fos != null) fos.close();// 關(guān)閉寫入流 } catch (IOException e) { e.printStackTrace(); } } // 這里得到可播放的音頻文件 private void copyWaveFile(String inFilename, String outFilename) { FileInputStream in = null; FileOutputStream out = null; long totalAudioLen = 0; long totalDataLen = totalAudioLen + 36; long longSampleRate = AudioFileFunc.AUDIO_SAMPLE_RATE; int channels = 2; long byteRate = 16 * AudioFileFunc.AUDIO_SAMPLE_RATE * channels / 8; byte[] data = new byte[bufferSizeInBytes]; try { in = new FileInputStream(inFilename); out = new FileOutputStream(outFilename); totalAudioLen = in.getChannel().size(); totalDataLen = totalAudioLen + 36; WriteWaveFileHeader(out, totalAudioLen, totalDataLen, longSampleRate, channels, byteRate); while (in.read(data) != -1) { out.write(data); } in.close(); out.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } /** * 這里提供一個(gè)頭信息。插入這些信息就可以得到可以播放的文件。 * 為我為啥插入這44個(gè)字節(jié),這個(gè)還真沒(méi)深入研究,不過(guò)你隨便打開(kāi)一個(gè)wav * 音頻的文件,可以發(fā)現(xiàn)前面的頭文件可以說(shuō)基本一樣哦。每種格式的文件都有 * 自己特有的頭文件。 */ private void WriteWaveFileHeader(FileOutputStream out, long totalAudioLen, long totalDataLen, long longSampleRate, int channels, long byteRate) throws IOException { byte[] header = new byte[44]; header[0] = 'R'; // RIFF/WAVE header header[1] = 'I'; header[2] = 'F'; header[3] = 'F'; header[4] = (byte) (totalDataLen & 0xff); header[5] = (byte) ((totalDataLen >> 8) & 0xff); header[6] = (byte) ((totalDataLen >> 16) & 0xff); header[7] = (byte) ((totalDataLen >> 24) & 0xff); header[8] = 'W'; header[9] = 'A'; header[10] = 'V'; header[11] = 'E'; header[12] = 'f'; // 'fmt ' chunk header[13] = 'm'; header[14] = 't'; header[15] = ' '; header[16] = 16; // 4 bytes: size of 'fmt ' chunk header[17] = 0; header[18] = 0; header[19] = 0; header[20] = 1; // format = 1 header[21] = 0; header[22] = (byte) channels; header[23] = 0; header[24] = (byte) (longSampleRate & 0xff); header[25] = (byte) ((longSampleRate >> 8) & 0xff); header[26] = (byte) ((longSampleRate >> 16) & 0xff); header[27] = (byte) ((longSampleRate >> 24) & 0xff); header[28] = (byte) (byteRate & 0xff); header[29] = (byte) ((byteRate >> 8) & 0xff); header[30] = (byte) ((byteRate >> 16) & 0xff); header[31] = (byte) ((byteRate >> 24) & 0xff); header[32] = (byte) (2 * 16 / 8); // block align header[33] = 0; header[34] = 16; // bits per sample header[35] = 0; header[36] = 'd'; header[37] = 'a'; header[38] = 't'; header[39] = 'a'; header[40] = (byte) (totalAudioLen & 0xff); header[41] = (byte) ((totalAudioLen >> 8) & 0xff); header[42] = (byte) ((totalAudioLen >> 16) & 0xff); header[43] = (byte) ((totalAudioLen >> 24) & 0xff); out.write(header, 0, 44); } }
2、MediaRecorder錄音,輸出amr格式音頻
package com.example.audiorecordtest; import java.io.File; import java.io.IOException; import android.media.MediaRecorder; public class MediaRecordFunc { private boolean isRecord = false; private MediaRecorder mMediaRecorder; private MediaRecordFunc(){ } private static MediaRecordFunc mInstance; public synchronized static MediaRecordFunc getInstance(){ if(mInstance == null) mInstance = new MediaRecordFunc(); return mInstance; } public int startRecordAndFile(){ //判斷是否有外部存儲(chǔ)設(shè)備sdcard if(AudioFileFunc.isSdcardExit()) { if(isRecord) { return ErrorCode.E_STATE_RECODING; } else { if(mMediaRecorder == null) createMediaRecord(); try{ mMediaRecorder.prepare(); mMediaRecorder.start(); // 讓錄制狀態(tài)為true isRecord = true; return ErrorCode.SUCCESS; }catch(IOException ex){ ex.printStackTrace(); return ErrorCode.E_UNKOWN; } } } else { return ErrorCode.E_NOSDCARD; } } public void stopRecordAndFile(){ close(); } public long getRecordFileSize(){ return AudioFileFunc.getFileSize(AudioFileFunc.getAMRFilePath()); } private void createMediaRecord(){ /* ①Initial:實(shí)例化MediaRecorder對(duì)象 */ mMediaRecorder = new MediaRecorder(); /* setAudioSource/setVedioSource*/ mMediaRecorder.setAudioSource(AudioFileFunc.AUDIO_INPUT);//設(shè)置麥克風(fēng) /* 設(shè)置輸出文件的格式:THREE_GPP/MPEG-4/RAW_AMR/Default * THREE_GPP(3gp格式,H263視頻/ARM音頻編碼)、MPEG-4、RAW_AMR(只支持音頻且音頻編碼要求為AMR_NB) */ mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.DEFAULT); /* 設(shè)置音頻文件的編碼:AAC/AMR_NB/AMR_MB/Default */ mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT); /* 設(shè)置輸出文件的路徑 */ File file = new File(AudioFileFunc.getAMRFilePath()); if (file.exists()) { file.delete(); } mMediaRecorder.setOutputFile(AudioFileFunc.getAMRFilePath()); } private void close(){ if (mMediaRecorder != null) { System.out.println("stopRecord"); isRecord = false; mMediaRecorder.stop(); mMediaRecorder.release(); mMediaRecorder = null; } } }
3、其他文件
AudioFileFunc.java
package com.example.audiorecordtest; import java.io.File; import android.media.MediaRecorder; import android.os.Environment; public class AudioFileFunc { //音頻輸入-麥克風(fēng) public final static int AUDIO_INPUT = MediaRecorder.AudioSource.MIC; //采用頻率 //44100是目前的標(biāo)準(zhǔn),但是某些設(shè)備仍然支持22050,16000,11025 public final static int AUDIO_SAMPLE_RATE = 44100; //44.1KHz,普遍使用的頻率 //錄音輸出文件 private final static String AUDIO_RAW_FILENAME = "RawAudio.raw"; private final static String AUDIO_WAV_FILENAME = "FinalAudio.wav"; public final static String AUDIO_AMR_FILENAME = "FinalAudio.amr"; /** * 判斷是否有外部存儲(chǔ)設(shè)備sdcard * @return true | false */ public static boolean isSdcardExit(){ if (Environment.getExternalStorageState().equals(android.os.Environment.MEDIA_MOUNTED)) return true; else return false; } /** * 獲取麥克風(fēng)輸入的原始音頻流文件路徑 * @return */ public static String getRawFilePath(){ String mAudioRawPath = ""; if(isSdcardExit()){ String fileBasePath = Environment.getExternalStorageDirectory().getAbsolutePath(); mAudioRawPath = fileBasePath+"/"+AUDIO_RAW_FILENAME; } return mAudioRawPath; } /** * 獲取編碼后的WAV格式音頻文件路徑 * @return */ public static String getWavFilePath(){ String mAudioWavPath = ""; if(isSdcardExit()){ String fileBasePath = Environment.getExternalStorageDirectory().getAbsolutePath(); mAudioWavPath = fileBasePath+"/"+AUDIO_WAV_FILENAME; } return mAudioWavPath; } /** * 獲取編碼后的AMR格式音頻文件路徑 * @return */ public static String getAMRFilePath(){ String mAudioAMRPath = ""; if(isSdcardExit()){ String fileBasePath = Environment.getExternalStorageDirectory().getAbsolutePath(); mAudioAMRPath = fileBasePath+"/"+AUDIO_AMR_FILENAME; } return mAudioAMRPath; } /** * 獲取文件大小 * @param path,文件的絕對(duì)路徑 * @return */ public static long getFileSize(String path){ File mFile = new File(path); if(!mFile.exists()) return -1; return mFile.length(); } }
4、其他文件
ErrorCode.java
package com.example.audiorecordtest; import android.content.Context; import android.content.res.Resources.NotFoundException; public class ErrorCode { public final static int SUCCESS = 1000; public final static int E_NOSDCARD = 1001; public final static int E_STATE_RECODING = 1002; public final static int E_UNKOWN = 1003; public static String getErrorInfo(Context vContext, int vType) throws NotFoundException { switch(vType) { case SUCCESS: return "success"; case E_NOSDCARD: return vContext.getResources().getString(R.string.error_no_sdcard); case E_STATE_RECODING: return vContext.getResources().getString(R.string.error_state_record); case E_UNKOWN: default: return vContext.getResources().getString(R.string.error_unknown); } } }
5、string.xml
<?xml version="1.0" encoding="utf-8"?> <resources> <string name="app_name">AudioRecordTest</string> <string name="hello_world">測(cè)試AudioRecord,實(shí)現(xiàn)錄音功能</string> <string name="menu_settings">Settings</string> <string name="view_record_wav">錄音WAV文件</string> <string name="view_record_amr">錄音AMR文件</string> <string name="view_stop">停止錄音</string> <string name="error_no_sdcard">沒(méi)有SD卡,無(wú)法存儲(chǔ)錄音數(shù)據(jù)</string> <string name="error_state_record">正在錄音中,請(qǐng)先停止錄音</string> <string name="error_unknown">無(wú)法識(shí)別的錯(cuò)誤</string> </resources>
6、主程序MainActivity
package com.example.audiorecordtest; import android.app.Activity; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.util.Log; import android.view.Menu; import android.view.View; import android.widget.Button; import android.widget.TextView; public class MainActivity extends Activity { private final static int FLAG_WAV = 0; private final static int FLAG_AMR = 1; private int mState = -1; //-1:沒(méi)再錄制,0:錄制wav,1:錄制amr private Button btn_record_wav; private Button btn_record_amr; private Button btn_stop; private TextView txt; private UIHandler uiHandler; private UIThread uiThread; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); findViewByIds(); setListeners(); init(); } @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.activity_main, menu); return true; } private void findViewByIds(){ btn_record_wav = (Button)this.findViewById(R.id.btn_record_wav); btn_record_amr = (Button)this.findViewById(R.id.btn_record_amr); btn_stop = (Button)this.findViewById(R.id.btn_stop); txt = (TextView)this.findViewById(R.id.text); } private void setListeners(){ btn_record_wav.setOnClickListener(btn_record_wav_clickListener); btn_record_amr.setOnClickListener(btn_record_amr_clickListener); btn_stop.setOnClickListener(btn_stop_clickListener); } private void init(){ uiHandler = new UIHandler(); } private Button.OnClickListener btn_record_wav_clickListener = new Button.OnClickListener(){ public void onClick(View v){ record(FLAG_WAV); } }; private Button.OnClickListener btn_record_amr_clickListener = new Button.OnClickListener(){ public void onClick(View v){ record(FLAG_AMR); } }; private Button.OnClickListener btn_stop_clickListener = new Button.OnClickListener(){ public void onClick(View v){ stop(); } }; /** * 開(kāi)始錄音 * @param mFlag,0:錄制wav格式,1:錄音amr格式 */ private void record(int mFlag){ if(mState != -1){ Message msg = new Message(); Bundle b = new Bundle();// 存放數(shù)據(jù) b.putInt("cmd",CMD_RECORDFAIL); b.putInt("msg", ErrorCode.E_STATE_RECODING); msg.setData(b); uiHandler.sendMessage(msg); // 向Handler發(fā)送消息,更新UI return; } int mResult = -1; switch(mFlag){ case FLAG_WAV: AudioRecordFunc mRecord_1 = AudioRecordFunc.getInstance(); mResult = mRecord_1.startRecordAndFile(); break; case FLAG_AMR: MediaRecordFunc mRecord_2 = MediaRecordFunc.getInstance(); mResult = mRecord_2.startRecordAndFile(); break; } if(mResult == ErrorCode.SUCCESS){ uiThread = new UIThread(); new Thread(uiThread).start(); mState = mFlag; }else{ Message msg = new Message(); Bundle b = new Bundle();// 存放數(shù)據(jù) b.putInt("cmd",CMD_RECORDFAIL); b.putInt("msg", mResult); msg.setData(b); uiHandler.sendMessage(msg); // 向Handler發(fā)送消息,更新UI } } /** * 停止錄音 */ private void stop(){ if(mState != -1){ switch(mState){ case FLAG_WAV: AudioRecordFunc mRecord_1 = AudioRecordFunc.getInstance(); mRecord_1.stopRecordAndFile(); break; case FLAG_AMR: MediaRecordFunc mRecord_2 = MediaRecordFunc.getInstance(); mRecord_2.stopRecordAndFile(); break; } if(uiThread != null){ uiThread.stopThread(); } if(uiHandler != null) uiHandler.removeCallbacks(uiThread); Message msg = new Message(); Bundle b = new Bundle();// 存放數(shù)據(jù) b.putInt("cmd",CMD_STOP); b.putInt("msg", mState); msg.setData(b); uiHandler.sendMessageDelayed(msg,1000); // 向Handler發(fā)送消息,更新UI mState = -1; } } private final static int CMD_RECORDING_TIME = 2000; private final static int CMD_RECORDFAIL = 2001; private final static int CMD_STOP = 2002; class UIHandler extends Handler{ public UIHandler() { } @Override public void handleMessage(Message msg) { // TODO Auto-generated method stub Log.d("MyHandler", "handleMessage......"); super.handleMessage(msg); Bundle b = msg.getData(); int vCmd = b.getInt("cmd"); switch(vCmd) { case CMD_RECORDING_TIME: int vTime = b.getInt("msg"); MainActivity.this.txt.setText("正在錄音中,已錄制:"+vTime+" s"); break; case CMD_RECORDFAIL: int vErrorCode = b.getInt("msg"); String vMsg = ErrorCode.getErrorInfo(MainActivity.this, vErrorCode); MainActivity.this.txt.setText("錄音失敗:"+vMsg); break; case CMD_STOP: int vFileType = b.getInt("msg"); switch(vFileType){ case FLAG_WAV: AudioRecordFunc mRecord_1 = AudioRecordFunc.getInstance(); long mSize = mRecord_1.getRecordFileSize(); MainActivity.this.txt.setText("錄音已停止.錄音文件:"+AudioFileFunc.getWavFilePath()+"\n文件大?。?+mSize); break; case FLAG_AMR: MediaRecordFunc mRecord_2 = MediaRecordFunc.getInstance(); mSize = mRecord_2.getRecordFileSize(); MainActivity.this.txt.setText("錄音已停止.錄音文件:"+AudioFileFunc.getAMRFilePath()+"\n文件大小:"+mSize); break; } break; default: break; } } }; class UIThread implements Runnable { int mTimeMill = 0; boolean vRun = true; public void stopThread(){ vRun = false; } public void run() { while(vRun){ try { Thread.sleep(1000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } mTimeMill ++; Log.d("thread", "mThread........"+mTimeMill); Message msg = new Message(); Bundle b = new Bundle();// 存放數(shù)據(jù) b.putInt("cmd",CMD_RECORDING_TIME); b.putInt("msg", mTimeMill); msg.setData(b); MainActivity.this.uiHandler.sendMessage(msg); // 向Handler發(fā)送消息,更新UI } } } }
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- android MediaRecorder實(shí)現(xiàn)錄屏?xí)r帶錄音功能
- Android編程錄音工具類RecorderUtil定義與用法示例
- Android實(shí)現(xiàn)錄音功能實(shí)現(xiàn)實(shí)例(MediaRecorder)
- Android使用AudioRecord實(shí)現(xiàn)暫停錄音功能實(shí)例代碼
- android 通過(guò)MediaRecorder實(shí)現(xiàn)簡(jiǎn)單的錄音示例
- Android使用MediaRecorder實(shí)現(xiàn)錄音及播放
- Android App調(diào)用MediaRecorder實(shí)現(xiàn)錄音功能的實(shí)例
- Android音頻錄制MediaRecorder之簡(jiǎn)易的錄音軟件實(shí)現(xiàn)代碼
- Android簡(jiǎn)單的利用MediaRecorder進(jìn)行錄音的實(shí)例代碼
- Android使用AudioRecord實(shí)現(xiàn)錄音功能
相關(guān)文章
Android Intent發(fā)送廣播消息實(shí)例詳解
這篇文章主要介紹了Android Intent發(fā)送廣播消息實(shí)例詳解的相關(guān)資料,需要的朋友可以參考下2017-04-04android 修改launcher行數(shù)和列數(shù)的方法
這篇文章主要介紹了android 修改launcher行數(shù)和列數(shù)的方法,非常不錯(cuò),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2018-07-07IOS開(kāi)發(fā)向右滑動(dòng)返回前一個(gè)頁(yè)面功能(demo)
本文給大家介紹使用android實(shí)現(xiàn)向右滑動(dòng)返回一個(gè)頁(yè)面的功能,大家都知道在ios7中,蘋果的原生態(tài)應(yīng)用幾乎都能夠通過(guò)向右滑動(dòng)來(lái)返回到前一個(gè)頁(yè)面,這樣可以避免用戶在單手操作時(shí)用大拇指去點(diǎn)擊那個(gè)遙遠(yuǎn)的返回鍵,下面小編就給帶來(lái)了實(shí)現(xiàn)代碼,有需要的朋友可以參考下2016-06-06Jetpack?Compose實(shí)現(xiàn)對(duì)角線滾動(dòng)效果
這篇文章主要為大家詳細(xì)介紹了如何利用Jetpack?Compose實(shí)現(xiàn)一個(gè)簡(jiǎn)單的對(duì)角線滾動(dòng)效果,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以了解一下2023-02-02Android 雙擊Back鍵退出應(yīng)用的實(shí)現(xiàn)方法
這篇文章主要介紹了Android 雙擊Back鍵退出應(yīng)用的實(shí)現(xiàn)方法的相關(guān)資料,希望通過(guò)本文能幫助到大家,讓大家實(shí)現(xiàn)這樣的功能,需要的朋友可以參考下2017-10-10Android?獲取實(shí)時(shí)網(wǎng)速實(shí)現(xiàn)詳解
這篇文章主要為大家介紹了Android?獲取實(shí)時(shí)網(wǎng)速實(shí)現(xiàn)詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-11-11