fun drawBoard(width: Int, height: Int){
val gridLayout = GridLayout(this)
gridLayout.layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
gridLayout.columnCount = width
for (i in 0 until (width * height)){
val button = Button(this)
button.tag = i
val buttonNumber = i + 1
button.setText("${buttonNumber}")
button.gravity = Gravity.CENTER
//todo: add touch listener
gridLayout.addView(button)
val layoutParams = GridLayout.LayoutParams()
layoutParams.columnSpec = GridLayout.spec(i % width,1f)
layoutParams.rowSpec = GridLayout.spec(i / width,1f)
button.layoutParams = layoutParams
}
binding.layout.addView(gridLayout)
}
The GridLayout.spec
object defines the weight for each element. Assigning the same weight ensures they are all the same sizeonCreate
method with values of 3 and 4 for the width and height respectively.Starting with Android Marshmallow (6.0), certain types of permissions require explicit permission from the user, from within the app, in addition to being specified in the AndroidManifest.xml file. This includes accessing the microphone and writing to external storage - both of which this app will need to do
<uses-permission android:name="android.permission.RECORD_AUDIO" />
private fun requestPermissions(){
val hasRecordPermission = ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED
if (!hasRecordPermission){
val requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()){
permitted: Boolean ->
if (!permitted){
Toast.makeText(this,"You need to give permissions for the app to work",Toast.LENGTH_LONG).show()
}
}
requestPermissionLauncher.launch(Manifest.permission.RECORD_AUDIO)
}
}
onCreate
methodThe application needs to be able to start and stop recording, as well as start and stop playback. Adding all this functionality within the Activity would make it less reuable, so we will create a class with the responsibility for recording and playback
val
named context
of type Context
var mediaRecorder : MediaRecorder? = null
var mediaPlayer : MediaPlayer? = null
private fun filePathForId(id: Int) : String {
return context.getExternalFilesDir(null)!!.absolutePath + "/$id.aac"
}
fun startRecording(id: Int): Boolean {
//check the device has a microphone
if (context.packageManager.hasSystemFeature(PackageManager.FEATURE_MICROPHONE)) {
mediaRecorder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
MediaRecorder(context)
} else {
@Suppress("DEPRECATION")
MediaRecorder()
}
//specify source of audio (Microphone)
mediaRecorder?.setAudioSource(MediaRecorder.AudioSource.MIC)
//specify file type and compression format
mediaRecorder?.setOutputFormat(MediaRecorder.OutputFormat.AAC_ADTS)
mediaRecorder?.setAudioEncoder(MediaRecorder.AudioEncoder.AAC)
//specify audio sampling rate and encoding bit rate (48kHz and 128kHz respectively)
mediaRecorder?.setAudioSamplingRate(48000)
mediaRecorder?.setAudioEncodingBitRate(128000)
//specify where to save
val fileLocation = filePathForId(id)
mediaRecorder?.setOutputFile(fileLocation)
//record
mediaRecorder?.prepare()
mediaRecorder?.start()
return true
} else {
return false
}
}
The MediaRecorder class can use a range for formats and file types, detailed here fun stopRecording() {
mediaRecorder?.stop()
mediaRecorder?.release()
mediaRecorder = null
}
fun startPlayback(id: Int): Boolean {
val path = filePathForId(id)
if (File(path).exists()) {
mediaPlayer = MediaPlayer()
mediaPlayer?.setDataSource(path)
mediaPlayer?.prepare()
mediaPlayer?.start()
return true
}
return false
}
fun stopPlayback() {
mediaPlayer?.stop()
mediaPlayer?.release()
mediaPlayer = null
}
lateinit var audioManager: AudioManager
onCreate
method, instantiate the audioManager object as follows:audioManager = AudioManager(this)
The app is designed to work in such a way that holding down on a button will either play or record audio associated with that button. Whether audio is played or recorded will depend on the state of the ToggleButton.
private fun setBackgroundColour(colour: Int, v: View) {
val filter = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
BlendModeColorFilter(colour, BlendMode.DARKEN)
} else {
@Suppress("DEPRECATION")
PorterDuffColorFilter(colour, PorterDuff.Mode.DARKEN)
//PorterDuff is a class with list of blending + compositing modes, named after the authors of a paper on the subject
}
v.background.colorFilter = filter
}
val touchListener = View.OnTouchListener { v: View?, event: MotionEvent? -> Boolean
val id = v?.tag as Int
if (event?.action == MotionEvent.ACTION_DOWN) {
if (binding.toggleButton.isChecked){ //recording
val isRecording = audioManager.startRecording(id)
if (isRecording) {
setBackgroundColour(Color.RED, v)
} else {
Toast.makeText(this,"Unable to start recording",Toast.LENGTH_LONG).show()
}
} else {
if (audioManager.startPlayback(id)){
setBackgroundColour(Color.GREEN, v)
}
}
binding.toggleButton.isEnabled = false
return@OnTouchListener true
}
if (event?.action == MotionEvent.ACTION_UP){
if (toggleButton.isChecked){
audioManager.stopRecording()
} else {
audioManager.stopPlayback()
}
v.background.clearColorFilter()
binding.toggleButton.isEnabled = true
return@OnTouchListener true
}
false
}
//todo:
comment in the drawBoard
method, and replace it with the following:
button.setOnTouchListener(touchListener)
A completed version of the project can be found on GitHub
prepare
method should not be called on the main thread. Use the prepareAsync
method instead and use an implementation of the OnPreparedListener
interface to start playback