💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
**这一章在处理相片与录音档名称的时候有错误,建议在完成这一章的内容以后,再参考[错误修正:4-1 使用照相机与麦克风](https://github.com/macdidi5/AndroidTutorial/blob/master/AndroidTutorial4_1Update.md)修正错误。** 现在行动装置的硬件设备技术已经越来越好了,萤幕的尺寸不断的增加,提供使用者清楚又美观的画面。触控萤幕也几乎是目前行动装置的标准设备,使用触控的方式操作应用程式快速又方便。Android系统内建的音乐播放应用程式,也可以让行动装置成为随身的音乐播放设备。还有画素也越来越高的照像功能,一台行动装置几乎可以应付所有的需求。 行动装置提供高画质的摄影镜头,让使用者随时可以拍摄照片与录影,也几乎已经是行动装置基本的设备与功能了。使用Android系统内建的API与元件,可以在应用程式需要的时候,让使用者拍摄照片与录影,并且把照片或影片档案储存在指定的位置。例如在记事本应用程式中,可以加入照片与录影备忘的功能。 应用程式需要录音的时候,可以使用内建的API执行录音的工作,并且把录音完成的档案储存在指定的位置,例如在记事本应用程式中,可以加入录制语音备忘的功能,让使用者可以随时查询与播放这些录音资讯。 这一章为记事资料加入照相与录音的功能,让这个应用程式的功能可以更完整,使用者可以在新增或修改记事资料的时候,启动相机拍照,还有使用麦克风录制语音备忘。 ## 12-1 使用相机拍摄照片 不论是移动电话或平板电脑,几乎都有高画质的摄录镜头设备,让使用者可以随时拍摄与录影。加入拍摄照片的功能可以让应用程式的功能更完整,例如在记事本应用程式加入拍照的功能,记录影像会比文字更清楚与方便。 应用程式需要执行拍照的功能,可以启动系统相机元件执行拍照的工作,它的系统Action名称变量是“MediaStore.ACTION_IMAGE_CAPTURE”,使用这个Action名称建立好的Intent物件,可以呼叫putExtra方法加入照片档案储存位置的设定资料,资料的名称是“MediaStore.EXTRA_OUTPUT”,如果没有指定的话,会使用系统默认的名称储存在默认的位置。 应用程式要执行拍照的功能,装置必须有摄录镜头的设备才可以正确的执行,所以需要在应用程式设定档中加入硬件设备需求的设定。如果需要储存照片档案到外部储存设备,例如记忆卡,需要在应用程式设定档中加入授权设定: ~~~ <!-- 需要摄录镜头设备 --> <uses-feature android:name="android.hardware.camera" /> <!-- 写入外部储存设备 --> <uses-permission android:name= "android.permission.WRITE_EXTERNAL_STORAGE"/> ~~~ Android模拟装置也可以测试相机的功能,不过要先确认模拟装置的设定,关闭已经启动的模拟装置,在Android Studio选择功能表“Tools -> Android -> AVD Manager”,选择模拟装置的编辑图示: [![AndroidTutorial5_04_01_01](http://www.codedata.com.tw/wp-content/uploads/2015/02/AndroidTutorial5_04_01_01-300x171.png)](http://www.codedata.com.tw/wp-content/uploads/2015/02/AndroidTutorial5_04_01_01.png) 在模拟装置编辑视窗选择“Show Advanced Settings”: [![AndroidTutorial5_04_01_02](http://www.codedata.com.tw/wp-content/uploads/2015/02/AndroidTutorial5_04_01_02-300x152.png)](http://www.codedata.com.tw/wp-content/uploads/2015/02/AndroidTutorial5_04_01_02.png) 如果你的电脑没有连接WebCam,在“Front”与“Back”选择“Emulated”。如果电已经连接WebCam,就可以选择“Webcam0”。完成设定后选择“Finish”: [![AndroidTutorial5_04_01_03](http://www.codedata.com.tw/wp-content/uploads/2015/02/AndroidTutorial5_04_01_03-300x152.png)](http://www.codedata.com.tw/wp-content/uploads/2015/02/AndroidTutorial5_04_01_03.png) 回到AVD Manager视窗后,选择模拟装置的启动图示: [![AndroidTutorial5_04_01_04](http://www.codedata.com.tw/wp-content/uploads/2015/02/AndroidTutorial5_04_01_04-300x171.png)](http://www.codedata.com.tw/wp-content/uploads/2015/02/AndroidTutorial5_04_01_04.png) 模拟装置启动以后,开启“照相”应用程式,就可以看到模拟照相机的画面: [![AndroidTutorial5_04_01_05](http://www.codedata.com.tw/wp-content/uploads/2015/02/AndroidTutorial5_04_01_05-167x300.png)](http://www.codedata.com.tw/wp-content/uploads/2015/02/AndroidTutorial5_04_01_05.png) 因为记事元件的画面加入照片以后,在萤幕比较小的装置运作时,画面会超过萤幕的范围,所以需要调整画面的设计。另外也要加入显示照片用的ImageView元件。开启“res/layout/activity_item.xml”,参考下列的内容修改这个画面配置档: ~~~ <?xml version="1.0" encoding="utf-8"?> <!-- 使用ScrollView为最外层的元件 --> <ScrollView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <!-- 删除xmlns:android的设定 --> <TableLayout xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:stretchColumns="1" tools:context="net.macdidi.myandroidtutorial.ItemActivity"> <TableRow> ... </TableRow> <TableRow> ... </TableRow> <!-- 显示图片 --> <ImageView android:id="@+id/picture" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@drawable/retangle_drawable" android:padding="6sp" android:layout_margin="2sp" android:visibility="invisible" /> <TableLayout ...> <TableRow> ... </TableRow> </TableLayout> <TableLayout ...> <TableRow> ... </TableRow> </TableLayout> </TableLayout> <!-- ScrollView的结束标签 --> </ScrollView> ~~~ 因为需要储存照片与录音档案,所以撰写一个档案公用类别。在“net.macdidi.myandroidtutorial”套件按鼠标右键,选择“New -> Java CLass”,在Name输入“FileUtil”后选择“OK”。参考下列的内容完成这个程式码: ~~~ package net.macdidi.myandroidtutorial; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.os.Environment; import android.util.Log; import android.widget.ImageView; import java.io.File; import java.text.SimpleDateFormat; import java.util.Date; public class FileUtil { // 应用程式储存盘案的目录 public static final String APP_DIR = "androidtutorial"; // 外部储存设备是否可写入 public static boolean isExternalStorageWritable() { // 取得目前外部储存设备的状态 String state = Environment.getExternalStorageState(); // 判断是否可写入 if (Environment.MEDIA_MOUNTED.equals(state)) { return true; } return false; } // 外部储存设备是否可读取 public static boolean isExternalStorageReadable() { // 取得目前外部储存设备的状态 String state = Environment.getExternalStorageState(); // 判断是否可读取 if (Environment.MEDIA_MOUNTED.equals(state) || Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) { return true; } return false; } // 建立并传回在公用相簿下参数指定的路径 public static File getPublicAlbumStorageDir(String albumName) { // 取得公用的照片路径 File pictures = Environment.getExternalStoragePublicDirectory( Environment.DIRECTORY_PICTURES); // 准备在照片路径下建立一个指定的路径 File file = new File(pictures, albumName); // 如果建立路径不成功 if (!file.mkdirs()) { Log.e("getAlbumStorageDir", "Directory not created"); } return file; } // 建立并传回在应用程式专用相簿下参数指定的路径 public static File getAlbumStorageDir(Context context, String albumName) { // 取得应用程式专用的照片路径 File pictures = context.getExternalFilesDir( Environment.DIRECTORY_PICTURES); // 准备在照片路径下建立一个指定的路径 File file = new File(pictures, albumName); // 如果建立路径不成功 if (!file.mkdirs()) { Log.e("getAlbumStorageDir", "Directory not created"); } return file; } // 建立并传回外部储存媒体参数指定的路径 public static File getExternalStorageDir(String dir) { File result = new File( Environment.getExternalStorageDirectory(), dir); if (!isExternalStorageWritable()) { return null; } if (!result.exists() && !result.mkdirs()) { return null; } return result; } // 读取指定的照片档案名称设定给ImageView元件 public static void fileToImageView(String fileName, ImageView imageView) { if (new File(fileName).exists()) { Bitmap bitmap = BitmapFactory.decodeFile(fileName); imageView.setImageBitmap(bitmap); } else { Log.e("fileToImageView", fileName + " not found."); } } // 产生唯一的档案名称 public static String getUniqueFileName() { // 使用年月日_时分秒格式为档案名称 SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd_HHmmss"); return sdf.format(new Date()); } } ~~~ 开启在“net.macdidi.myandroidtutorial”套件下的“ItemActivity”类别,加入照相功能需要的字段变量: ~~~ // 档案名称 private String fileName; // 照片 private ImageView picture; ~~~ 同样在“ItemActivity”类别,找到“processViews”方法,参考下列的程式码,加入取得显示照片ImageView元件的程式码: ~~~ private void processViews() { title_text = (EditText) findViewById(R.id.title_text); content_text = (EditText) findViewById(R.id.content_text); // 取得显示照片的ImageView元件 picture = (ImageView) findViewById(R.id.picture); } ~~~ 同样在“ItemActivity”类别,找到“clickFunction”方法,参考下列的程式码,加入启动相机元件的程式码: ~~~ public void clickFunction(View view) { int id = view.getId(); switch (id) { case R.id.take_picture: // 启动相机元件用的Intent物件 Intent intentCamera = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); // 照片档案名称 File pictureFile = configFileName("P", ".jpg"); Uri uri = Uri.fromFile(pictureFile); // 设定档案名称 intentCamera.putExtra(MediaStore.EXTRA_OUTPUT, uri); // 启动相机元件 startActivityForResult(intentCamera, START_CAMERA); break; ... } } private File configFileName(String prefix, String extension) { // 如果记事资料已经有档案名称 if (item.getFileName() != null && item.getFileName().length() > 0) { fileName = item.getFileName(); } // 产生档案名称 else { fileName = FileUtil.getUniqueFileName(); } return new File(FileUtil.getExternalStorageDir(FileUtil.APP_DIR), prefix + fileName + extension); } ~~~ 同样在“ItemActivity”类别,找到“onActivityResult”方法,参考下列的程式码,处理完成照相工作后的程式码: ~~~ @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (resultCode == Activity.RESULT_OK) { switch (requestCode) { // 照像 case START_CAMERA: // 设定照片档案名称 item.setFileName(fileName); break; ... } } } ~~~ 同样在“ItemActivity”类别,新增覆写“onResume”方法的程式码,执行显示照片的工作: ~~~ @Override protected void onResume() { super.onResume(); // 如果有档案名称 if (item.getFileName() != null && item.getFileName().length() > 0) { // 照片档案物件 File file = configFileName("P", ".jpg"); // 如果照片档案存在 if (file.exists()) { // 显示照片元件 picture.setVisibility(View.VISIBLE); // 设定照片 FileUtil.fileToImageView(file.getAbsolutePath(), picture); } } } ~~~ 完成照相功能的工作了,执行应用程式,新增一个记事资料后选择照相功能: [![AndroidTutorial5_04_01_06](http://www.codedata.com.tw/wp-content/uploads/2015/02/AndroidTutorial5_04_01_06-167x300.png)](http://www.codedata.com.tw/wp-content/uploads/2015/02/AndroidTutorial5_04_01_06.png) 画面出现像这样的相机模拟画面,选择照像按钮: [![AndroidTutorial5_04_01_07](http://www.codedata.com.tw/wp-content/uploads/2015/02/AndroidTutorial5_04_01_07-167x300.png)](http://www.codedata.com.tw/wp-content/uploads/2015/02/AndroidTutorial5_04_01_07.png) 选择确定按钮: [![AndroidTutorial5_04_01_08](http://www.codedata.com.tw/wp-content/uploads/2015/02/AndroidTutorial5_04_01_08-167x300.png)](http://www.codedata.com.tw/wp-content/uploads/2015/02/AndroidTutorial5_04_01_08.png) 记事资料显示拍好的照片,储存记事资料后也会储存照片: [![AndroidTutorial5_04_01_09](http://www.codedata.com.tw/wp-content/uploads/2015/02/AndroidTutorial5_04_01_09-167x300.png)](http://www.codedata.com.tw/wp-content/uploads/2015/02/AndroidTutorial5_04_01_09.png) ## 12-2 录制语音备忘 在行动装置的应用程式使用录音功能,可以让很多工作变得更方便,例如语音备忘录的功能,可以省掉很多输入文字的时间。如果应用程式需要执行录音的工作,使用宣告在“android.media”套件下的“MediaRecorder”类别,应用程式可以设定录音的来源、输出格式、编码和储存盘案的位置。这些是执行设定与录音的方法,要特别注意在程式码中呼叫它们的顺序: * setAudioSource(int) – 设定录音来源,必须在setOutputFormat方法之前呼叫。设定为“MediaRecorder.AudioSource.MIC”表示录音来源是麦克风。 * setOutputFormat(int) –设定输出格式,必须在setAudioSource方法之后。设定为“MediaRecorder.OutputFormat.THREE_GPP”表示输出为3GP压缩格式。 * setAudioEncoder(int) –设定编码方式,必须在setOutputFormat方法之后。一般设定为“MediaRecorder.AudioEncoder.AMR_NB”。 * setOutputFile(String) –设定输出的档案名称,必须在setOutputFormat方法之后。 * prepare() – 使用设定的内容准备录音。 * start() – 开始录音。 * stop() – 停止录音。 * release() – 清除录音资源。 如果应用程式需要使用装置的录音设备,必须在应用程式设定档“AndroidManifest.xml”加入授权的设定: ~~~ <uses-permission android:name="android.permission.RECORD_AUDIO"/> ~~~ 加入录音功能的记事应用程式,可以让使用者选择录音功能按钮,启动一个负责录音的Activity元件,按下录音按钮就可以开始录音: [![AndroidTutorial5_04_01_10](http://www.codedata.com.tw/wp-content/uploads/2015/02/AndroidTutorial5_04_01_10-167x300.png)](http://www.codedata.com.tw/wp-content/uploads/2015/02/AndroidTutorial5_04_01_10.png) 录音的时候,录音按钮会切换为红色的图示,录音的音量变化会在右侧显示: [![AndroidTutorial5_04_01_11](http://www.codedata.com.tw/wp-content/uploads/2015/02/AndroidTutorial5_04_01_11-167x300.png)](http://www.codedata.com.tw/wp-content/uploads/2015/02/AndroidTutorial5_04_01_11.png) 设计录音元件的画面配置档,使用的图形资源可以在GitHub中取得,需要“record_dard_icon.png”与“record_red_icon.png”两个图示档案。开启“res/values/strings.xml”,加入这个元件需要的文字资源: ~~~ <string name="title_record">语音备忘</string> ~~~ 在“res/layout”目录按鼠标右键,选择“New -> Layout resrouce file”,在File name输入“activity_record”候选择“OK”。参考下列的内容,完成这个画面资源的设计: ~~~ <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <LinearLayout android:orientation="horizontal" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@drawable/retangle_drawable" android:layout_margin="6sp" android:padding="6sp"> <ImageButton android:id="@+id/record_button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/record_sound_icon" android:onClick="clickRecord" /> <ProgressBar android:id="@+id/record_volumn" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:layout_marginLeft="6dp" android:layout_marginRight="6dp" android:max="15" style="@android:style/Widget.ProgressBar.Horizontal" /> </LinearLayout> <TableLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:stretchColumns="*"> <TableRow> <Button android:text="@android:string/cancel" android:onClick="onSubmit" /> <Button android:id="@+id/record_ok" android:text="@android:string/ok" android:onClick="onSubmit" /> </TableRow> </TableLayout> </LinearLayout> ~~~ 在“net.macdidi.myandroidtutorial”套件按鼠标右键,选择“New -> Java CLass”,在Name输入“RecordActivity”后选择“OK”。参考下列的内容完成这个Activity元件的程式码:(这里提供的设计包含显示录音中的音量,你可以考虑移除这个部份的程式码,这个元件的设计就会比较简单一些) ~~~ package net.macdidi.myandroidtutorial; import android.app.Activity; import android.content.Intent; import android.media.MediaRecorder; import android.os.AsyncTask; import android.os.Bundle; import android.util.Log; import android.view.View; import android.widget.ImageButton; import android.widget.ProgressBar; import java.io.IOException; public class RecordActivity extends Activity { private ImageButton record_button; private boolean isRecording = false; private ProgressBar record_volumn; private MyRecoder myRecoder; private String fileName; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_record); processViews(); // 读取档案名称 Intent intent = getIntent(); fileName = intent.getStringExtra("fileName"); } public void onSubmit(View view) { if (isRecording) { // 停止录音 myRecoder.stop(); } // 确定 if (view.getId() == R.id.record_ok) { Intent result = getIntent(); setResult(Activity.RESULT_OK, result); } finish(); } private void processViews() { record_button = (ImageButton) findViewById(R.id.record_button); record_volumn = (ProgressBar) findViewById(R.id.record_volumn); // 隐藏状态列ProgressBar setProgressBarIndeterminateVisibility(false); } public void clickRecord(View view) { // 切换 isRecording = !isRecording; // 开始录音 if (isRecording) { // 设定按钮图示为录音中 record_button.setImageResource(R.drawable.record_red_icon); // 建立录音物件 myRecoder = new MyRecoder(fileName); // 开始录音 myRecoder.start(); // 建立并执行显示麦克风音量的AsyncTask物件 new MicLevelTask().execute(); } // 停止录音 else { // 设定按钮图示为停止录音 record_button.setImageResource(R.drawable.record_dark_icon); // 麦克风音量归零 record_volumn.setProgress(0); // 停止录音 myRecoder.stop(); } } // 在录音过程中显示麦克风音量 private class MicLevelTask extends AsyncTask<Void, Void, Void> { @Override protected Void doInBackground(Void... args) { while (isRecording) { publishProgress(); try { Thread.sleep(200); } catch (InterruptedException e) { Log.d("RecordActivity", e.toString()); } } return null; } @Override protected void onProgressUpdate(Void... values) { record_volumn.setProgress((int) myRecoder.getAmplitudeEMA()); } } // 执行录音并且可以取得麦克风音量的录音物件 private class MyRecoder { private static final double EMA_FILTER = 0.6; private MediaRecorder recorder = null; private double mEMA = 0.0; private String output; // 建立录音物件,参数为录音储存的位置与档名 MyRecoder(String output) { this.output = output; } // 开始录音 public void start() { if (recorder == null) { // 建立录音用的MediaRecorder物件 recorder = new MediaRecorder(); // 设定录音来源为麦克风,必须在setOutputFormat方法之前呼叫 recorder.setAudioSource(MediaRecorder.AudioSource.MIC); // 设定输出格式为3GP压缩格式,必须在setAudioSource方法之后, // 在prepare方法之前呼叫 recorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP); // 设定录音的编码方式,必须在setOutputFormat方法之后, // 在prepare方法之前呼叫 recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB); // 设定输出的档案名称,必须在setOutputFormat方法之后, // 在prepare方法之前呼叫 recorder.setOutputFile(output); try { // 准备执行录音工作,必须在所有设定之后呼叫 recorder.prepare(); } catch (IOException e) { Log.d("RecordActivity", e.toString()); } // 开始录音 recorder.start(); mEMA = 0.0; } } // 停止录音 public void stop() { if (recorder != null) { // 停止录音 recorder.stop(); // 清除录音资源 recorder.release(); recorder = null; } } public double getAmplitude() { if (recorder != null) return (recorder.getMaxAmplitude() / 2700.0); else return 0; } // 取得麦克风音量 public double getAmplitudeEMA() { double amp = getAmplitude(); mEMA = EMA_FILTER * amp + (1.0 - EMA_FILTER) * mEMA; return mEMA; } } } ~~~ 开启应用程式设定档“AndroidManifest.xml”,加入录音元件的设定: ~~~ <!-- 录音元件 --> <activity android:name="net.macdidi.myandroidtutorial.RecordActivity" android:theme="@android:style/Theme.Dialog" android:label="@string/title_record"/> ~~~ 完成录音元件的设计后,开启在“net.macdidi.myandroidtutorial”套件下的“ItemActivity”类别,找到“clickFunction”方法,加入启动录音元件的程式码,还有负责录音的goToRecord方法: ~~~ public void clickFunction(View view) { int id = view.getId(); switch (id) { case R.id.take_picture: ... case R.id.record_sound: // 录音档案名称 final File recordFile = configFileName("R", ".mp3"); // 如果已经有录音档,询问播放或重新录制 if (recordFile.exists()) { // 询问播放还是重新录制的对话框 AlertDialog.Builder d = new AlertDialog.Builder(this); d.setTitle(R.string.title_record) .setCancelable(false); d.setPositiveButton(R.string.record_play, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { // 播放 // 在后面的说明才会处理 } }); d.setNeutralButton(R.string.record_new, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { goToRecord(recordFile); } }); d.setNegativeButton(android.R.string.cancel, null); // 显示对话框 d.show(); } // 如果没有录音档,启动录音元件 else { goToRecord(recordFile); } break; ... } } private void goToRecord(File recordFile) { // 录音 Intent recordIntent = new Intent(this, RecordActivity.class); recordIntent.putExtra("fileName", recordFile.getAbsolutePath()); startActivityForResult(recordIntent, START_RECORD); } ~~~ 为记事资料完成录音的功能了,在完成后面的播放功能后,再一起执行测试的工作。 ## 12-3 播放语音备忘 在前面已经完成的功能,如果使用者选择的记事资料已经录制过语音备忘,应用程式可以选择播放或是重新录制: [![AndroidTutorial5_04_01_12](http://www.codedata.com.tw/wp-content/uploads/2015/02/AndroidTutorial5_04_01_12-167x300.png)](http://www.codedata.com.tw/wp-content/uploads/2015/02/AndroidTutorial5_04_01_12.png) 使用者选择播放功能,应用程式启动播放语音备忘元件,这个元件提供播放、暂停与停止三个功能按钮: [![AndroidTutorial5_04_01_13](http://www.codedata.com.tw/wp-content/uploads/2015/02/AndroidTutorial5_04_01_13-167x300.png)](http://www.codedata.com.tw/wp-content/uploads/2015/02/AndroidTutorial5_04_01_13.png) 现在设计录音元件的画面配置档,使用的图形资源可以在GitHub中取得,需要“play_icon.png”、“pause_icon”与“stop_icon.png”三个图示档案。开启“res/values/strings.xml”,加入这个元件需要的文字资源: ~~~ <string name="title_play">播放语音备忘</string> <string name="record_play">播放</string> <string name="record_new">重新录制</string> ~~~ 在“res/layout”目录按鼠标右键,选择“New -> Layout resrouce file”,在File name输入“activity_play”候选择“OK”。参考下列的内容,完成这个画面资源的设计: ~~~ <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <LinearLayout android:orientation="horizontal" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@drawable/retangle_drawable" android:layout_margin="6sp" android:padding="6sp" > <ImageButton android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/play_icon" android:onClick="clickPlay" /> <ImageButton android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/pause_icon" android:onClick="clickPause" /> <ImageButton android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/stop_icon" android:onClick="clickStop" /> <SeekBar android:id="@+id/control" android:layout_width="100dp" android:layout_height="wrap_content" android:layout_marginTop="8sp" /> </LinearLayout> <Button android:layout_width="match_parent" android:layout_height="wrap_content" android:id="@+id/ok_add_teim" android:text="@android:string/ok" android:onClick="onSubmit" /> </LinearLayout> ~~~ 在“net.macdidi.myandroidtutorial”套件按鼠标右键,选择“New -> Java CLass”,在Name输入“PlayActivity”后选择“OK”。参考下列的内容完成这个Activity元件的程式码: ~~~ package net.macdidi.myandroidtutorial; import android.app.Activity; import android.content.Intent; import android.media.MediaPlayer; import android.media.MediaPlayer.OnCompletionListener; import android.net.Uri; import android.os.Bundle; import android.view.View; public class PlayActivity extends Activity { private MediaPlayer mediaPlayer; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_play); Intent intent = getIntent(); String fileName = intent.getStringExtra("fileName"); // 建立指定资源的MediaPlayer物件 Uri uri = Uri.parse(fileName); mediaPlayer = MediaPlayer.create(this, uri); } @Override protected void onStop() { if (mediaPlayer.isPlaying()) { // 停止播放 mediaPlayer.stop(); } // 清除MediaPlayer物件 mediaPlayer.release(); super.onStop(); } public void onSubmit(View view) { // 结束Activity元件 finish(); } public void clickPlay(View view) { // 开始播放 mediaPlayer.start(); } public void clickPause(View view) { // 暂停播放 mediaPlayer.pause(); } public void clickStop(View view) { // 停止播放 if (mediaPlayer.isPlaying()) { mediaPlayer.stop(); } // 回到开始的位置 mediaPlayer.seekTo(0); } } ~~~ 开启应用程式设定档“AndroidManifest.xml”,加入这个Activity元件的设定: ~~~ <!-- 播放元件 --> <activity android:name="net.macdidi.myandroidtutorial.PlayActivity" android:theme="@android:style/Theme.Dialog" android:label="@string/title_play"/> ~~~ 完成元件的设计后,开启在“net.macdidi.myandroidtutorial”套件下的“ItemActivity”类别,找到“clickFunction”方法,加入启动这个元件的程式码: ~~~ public void clickFunction(View view) { int id = view.getId(); switch (id) { case R.id.take_picture: ... case R.id.record_sound: // 录音档案名称 final File recordFile = configFileName("R", ".mp3"); // 如果已经有录音档,询问播放或重新录制 if (recordFile.exists()) { // 询问播放还是重新录制的对话框 AlertDialog.Builder d = new AlertDialog.Builder(this); d.setTitle(R.string.title_record) .setCancelable(false); d.setPositiveButton(R.string.record_play, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { // 播放 Intent playIntent = new Intent( ItemActivity.this, PlayActivity.class); playIntent.putExtra("fileName", recordFile.getAbsolutePath()); startActivity(playIntent); } }); d.setNeutralButton(R.string.record_new, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { goToRecord(recordFile); } }); d.setNegativeButton(android.R.string.cancel, null); // 显示对话框 d.show(); } // 如果没有录音档,启动录音元件 else { goToRecord(recordFile); } break; ... } } ~~~ 同样在“net.macdidi.myandroidtutorial”套件下的“ItemActivity”类别,找到“onActivityResult”方法,加入设定档案名称的程式码: ~~~ @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (resultCode == Activity.RESULT_OK) { switch (requestCode) { ... case START_RECORD: // 设定录音档案名称 item.setFileName(fileName); break; ... } } } ~~~ 完成这一章所有的工作了,录音与播放的功能比较建议在实体的装置上测试,试试看加入的功能是不是都可以正确的运作。 ## 错误修正:4-1 使用照相机与麦克风 这一章加入的照相与录音功能,把照片与录音档案名称储存在同一个字段。在完成这一章的内容后依照下列的步骤修正错误: 1. 开启“Item.java”,加入下列的字段与方法宣告: ~~~ // 录音档案名称 private String recFileName; public String getRecFileName() { return recFileName; } public void setRecFileName(String recFileName) { this.recFileName = recFileName; } ~~~ 2. 同样在“Item.java”,为建构子加入录音档案名称参数: ~~~ // 录音档案名称参数:String recFileName public Item(long id, long datetime, Colors color, String title, String content, String fileName, String recFileName, double latitude, double longitude, long lastModify) { this.id = id; this.datetime = datetime; this.color = color; this.title = title; this.content = content; this.fileName = fileName; // 录音档案名称 this.recFileName = recFileName; this.latitude = latitude; this.longitude = longitude; this.lastModify = lastModify; } ~~~ 3. 开启“ItemDAO.java”,加入与修改下列的字段宣告: ~~~ ... // 录音档案名称 public static final String RECFILENAME_COLUMN = "recfilename"; ... // 在“FILENAME_COLUMN”下方加入录音档案名称字段 public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ... FILENAME_COLUMN + " TEXT, " + RECFILENAME_COLUMN + " TEXT, " + // 增加录音档案名称 ..."; ~~~ 4. 同样在“ItemDAO.java”,修改“insert”方法: ~~~ public Item insert(Item item) { ContentValues cv = new ContentValues(); ... cv.put(FILENAME_COLUMN, item.getFileName()); // 录音档案名称 cv.put(RECFILENAME_COLUMN, item.getRecFileName()); ... } ~~~ 5. 同样在“ItemDAO.java”,修改“update”方法: ~~~ public boolean update(Item item) { ContentValues cv = new ContentValues(); ... cv.put(FILENAME_COLUMN, item.getFileName()); // 录音档案名称 cv.put(RECFILENAME_COLUMN, item.getRecFileName()); ... } ~~~ 6. 同样在“ItemDAO.java”,修改“getRecord”方法: ~~~ public Item getRecord(Cursor cursor) { ... result.setFileName(cursor.getString(5)); // 录音档案名称 result.setRecFileName(cursor.getString(6)); // 后续的编号都要加一 result.setLatitude(cursor.getDouble(7)); ... } ~~~ 7. 同样在“ItemDAO.java”,修改“sample”方法: ~~~ public void sample() { // 增加录音档案名称参数“""” Item item = new Item(0, new Date().getTime(), Colors.RED, "关于Android Tutorial的事情.", "Hello content", "", "", 0, 0, 0); Item item2 = new Item(0, new Date().getTime(), Colors.BLUE, "一只非常可爱的小狗狗!", "她的名字叫“大热狗”,又叫\n作“奶嘴”,是一只非常可爱\n的小狗。", "", "", 25.04719, 121.516981, 0); Item item3 = new Item(0, new Date().getTime(), Colors.GREEN, "一首非常好听的音乐!", "Hello content", "", "", 0, 0, 0); Item item4 = new Item(0, new Date().getTime(), Colors.ORANGE, "储存在数据库的资料", "Hello content", "", "", 0, 0, 0); ... } ~~~ 8. 开启“MyDBHelper.java”,增加数据库版本编号: ~~~ // 数据库版本,资料结构改变的时候要更改这个数字,通常是加一 public static final int VERSION = 2; ~~~ 9. 开启“ItemActivity.java”,增加录音档案名称字段变量: ~~~ // 录音档案名称 private String recFileName; ~~~ 10. 同样在“ItemActivity.java”,增加取得录音档案名称的方法: ~~~ private File configRecFileName(String prefix, String extension) { // 如果记事资料已经有档案名称 if (item.getRecFileName() != null && item.getRecFileName().length() > 0) { recFileName = item.getRecFileName(); } // 产生档案名称 else { recFileName = FileUtil.getUniqueFileName(); } return new File(FileUtil.getExternalStorageDir(FileUtil.APP_DIR), prefix + recFileName + extension); } ~~~ 11. 同样在“ItemActivity.java”,修改启动录音元件的方法: ~~~ public void clickFunction(View view) { int id = view.getId(); switch (id) { ... case R.id.record_sound: // 修改呼叫方法的名称为“configRecFileName” final File recordFile = configRecFileName("R", ".mp3"); if (recordFile.exists()) { ... } // 如果没有录音档,启动录音元件 else { goToRecord(recordFile); } break; ... } } ~~~ 12. 同样在“ItemActivity.java”,找到“onActivityResult”方法,修改设定录音档案名称呼叫的方法: ~~~ @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (resultCode == Activity.RESULT_OK) { switch (requestCode) { ... case START_RECORD: // 修改设定录音档案名称 item.setRecFileName(recFileName); break; ... } } } ~~~ 完成全部的修改以后执行应用程式,测试同一个记事资料照相与录音的功能。