Browse Source

feat: responsive keyboard layout, auto-expanding input box and mini hold-to-talk mic button

main
Ali Alavi 1 day ago
parent
commit
92b945aac0
  1. 73
      android/app/src/main/java/com/soniox/remotemic/MainActivity.kt
  2. 157
      android/app/src/main/res/layout/activity_main.xml

73
android/app/src/main/java/com/soniox/remotemic/MainActivity.kt

@ -25,6 +25,8 @@ import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.lifecycle.lifecycleScope
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.button.MaterialButton
@ -73,6 +75,7 @@ class MainActivity : AppCompatActivity() {
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
setupKeyboardInsetHandling()
setupUI()
checkPermissions()
@ -140,6 +143,29 @@ class MainActivity : AppCompatActivity() {
)
}
/**
* Dynamically adjusts the layout when virtual keyboard opens or closes
*/
private fun setupKeyboardInsetHandling() {
ViewCompat.setOnApplyWindowInsetsListener(binding.rootLayout) { _, insets ->
val imeVisible = insets.isVisible(WindowInsetsCompat.Type.ime())
if (imeVisible) {
// Keyboard OPEN: Hide huge bottom circle, expand editor box to full height directly above keyboard
binding.bottomVoiceSection.visibility = View.GONE
binding.tvSubtitle.visibility = View.GONE
binding.btnMiniMic.visibility = View.VISIBLE
} else {
// Keyboard CLOSED: Restore spacious layout with large glowing mic button
binding.bottomVoiceSection.visibility = View.VISIBLE
binding.tvSubtitle.visibility = View.VISIBLE
binding.btnMiniMic.visibility = View.GONE
}
insets
}
}
/**
* Slices speech text right at the exact cursor/selection location
*/
@ -245,30 +271,39 @@ class MainActivity : AppCompatActivity() {
showLogsBottomSheet()
}
// Touch listener for Hold to Speak (Captures cursor position on touch)
// Touch listener for Large Main Mic Button
binding.btnMic.setOnTouchListener { _, event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> {
if (checkAudioPermission()) {
val selStart = binding.etTranscript.selectionStart
val selEnd = binding.etTranscript.selectionEnd
val totalLen = binding.etTranscript.text?.length ?: 0
handleMicTouch(event)
}
voiceInsertionCursorStart = if (selStart >= 0) selStart else totalLen
voiceInsertionCursorEnd = if (selEnd >= 0) selEnd else totalLen
// Touch listener for Compact Mini Mic Button (above keyboard)
binding.btnMiniMic.setOnTouchListener { _, event ->
handleMicTouch(event)
}
}
startRecording()
}
true
private fun handleMicTouch(event: MotionEvent): Boolean {
return when (event.action) {
MotionEvent.ACTION_DOWN -> {
if (checkAudioPermission()) {
val selStart = binding.etTranscript.selectionStart
val selEnd = binding.etTranscript.selectionEnd
val totalLen = binding.etTranscript.text?.length ?: 0
voiceInsertionCursorStart = if (selStart >= 0) selStart else totalLen
voiceInsertionCursorEnd = if (selEnd >= 0) selEnd else totalLen
startRecording()
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
if (isCurrentlyRecording) {
stopRecordingAndProcess()
}
true
true
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
if (isCurrentlyRecording) {
stopRecordingAndProcess()
}
else -> false
true
}
else -> false
}
}
@ -366,6 +401,7 @@ class MainActivity : AppCompatActivity() {
binding.tvInstruction.text = getString(R.string.release_to_type)
binding.tvInstruction.setTextColor(ContextCompat.getColor(this, R.color.accent_red))
binding.btnMic.setBackgroundResource(R.drawable.bg_mic_button_active)
binding.btnMiniMic.setBackgroundColor(ContextCompat.getColor(this, R.color.accent_red))
startPulseAnimation()
streamDictationClient?.startRecording(voiceInsertionCursorStart)
@ -377,6 +413,7 @@ class MainActivity : AppCompatActivity() {
stopPulseAnimation()
binding.btnMic.setBackgroundResource(R.drawable.bg_mic_button)
binding.btnMiniMic.setBackgroundColor(ContextCompat.getColor(this, R.color.accent_blue))
binding.tvInstruction.text = "⏳ در حال درج و همگام‌سازی..."
binding.tvInstruction.setTextColor(ContextCompat.getColor(this, R.color.accent_yellow))

157
android/app/src/main/res/layout/activity_main.xml

@ -2,10 +2,11 @@
<androidx.constraintlayout.widget.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:id="@+id/rootLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/bg_dark"
android:padding="16dp">
android:padding="12dp">
<!-- Header Section -->
<LinearLayout
@ -23,20 +24,22 @@
android:orientation="vertical">
<TextView
android:id="@+id/tvTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="🎙️ Soniox Mobile to Mac"
android:textColor="@color/text_primary"
android:textSize="18sp"
android:textSize="17sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tvSubtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:text="تایپ صوتی بیسیم و کنترل اینپوت‌باکس مک"
android:text="همگام‌سازی زنده و تایپ صوتی بیسیم"
android:textColor="@color/text_secondary"
android:textSize="12sp" />
android:textSize="11sp" />
</LinearLayout>
<!-- Status Pill Badge -->
@ -61,7 +64,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="6dp"
android:text="متصل ✅"
android:text="متصل به مک ✅"
android:textColor="@color/text_primary"
android:textSize="11sp"
android:textStyle="bold" />
@ -70,8 +73,8 @@
<!-- Logs Icon Button -->
<ImageButton
android:id="@+id/btnOpenLogs"
android:layout_width="38dp"
android:layout_height="38dp"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_marginStart="8dp"
android:background="@drawable/bg_card"
android:contentDescription="لاگ‌ها"
@ -85,12 +88,12 @@
android:id="@+id/mainBoxCard"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="12dp"
android:layout_marginBottom="12dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:background="@drawable/bg_card"
android:orientation="vertical"
android:padding="12dp"
app:layout_constraintBottom_toTopOf="@id/micContainer"
app:layout_constraintBottom_toTopOf="@id/bottomVoiceSection"
app:layout_constraintTop_toBottomOf="@id/headerLayout">
<!-- Top Row: Box Title + Word Count + Clear Button -->
@ -104,7 +107,7 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="📝 متن گفتار و ویرایشگر مک"
android:text="📝 ویرایشگر همگام با مک"
android:textColor="@color/text_secondary"
android:textSize="12sp"
android:textStyle="bold" />
@ -134,11 +137,11 @@
android:id="@+id/etTranscript"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="8dp"
android:layout_marginTop="6dp"
android:layout_weight="1"
android:background="@null"
android:gravity="top|start"
android:hint="صحبت کنید یا متن خود را در اینجا بنویسید و ویرایش کنید..."
android:hint="متن مک در اینجا نمایش داده می‌شود؛ صحبت کنید یا بنویسید..."
android:inputType="textMultiLine|textCapSentences"
android:lineSpacingExtra="4dp"
android:scrollbars="vertical"
@ -149,11 +152,12 @@
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginVertical="8dp"
android:layout_marginVertical="6dp"
android:background="@color/card_border" />
<!-- Action Buttons Row -->
<!-- Action Buttons Row (sits directly above keyboard when open) -->
<LinearLayout
android:id="@+id/actionButtonsRow"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
@ -163,12 +167,29 @@
android:id="@+id/btnCopyText"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="40dp"
android:layout_height="38dp"
android:text="📋 کپی"
android:textColor="@color/text_secondary"
android:textSize="12sp"
android:textSize="11sp"
app:cornerRadius="8dp" />
<!-- Compact Mini Mic Button (visible when keyboard is open) -->
<com.google.android.material.button.MaterialButton
android:id="@+id/btnMiniMic"
style="@style/Widget.Material3.Button"
android:layout_width="wrap_content"
android:layout_height="38dp"
android:layout_marginStart="8dp"
android:backgroundTint="@color/accent_blue"
android:text="🎙️ گفتار (نگه‌دارید)"
android:textColor="@color/text_primary"
android:textSize="11sp"
android:textStyle="bold"
android:visibility="gone"
app:cornerRadius="8dp"
app:icon="@drawable/ic_mic"
app:iconSize="16dp" />
<View
android:layout_width="0dp"
android:layout_height="1dp"
@ -178,66 +199,70 @@
android:id="@+id/btnSendToMac"
style="@style/Widget.Material3.Button"
android:layout_width="wrap_content"
android:layout_height="42dp"
android:layout_height="38dp"
android:backgroundTint="@color/accent_blue"
android:text="🚀 درج متن در مک (Cmd+V)"
android:text="🚀 درج در مک"
android:textColor="@color/text_primary"
android:textSize="12sp"
android:textSize="11sp"
android:textStyle="bold"
app:cornerRadius="8dp" />
</LinearLayout>
</LinearLayout>
<!-- Center Glowing Mic Button Container -->
<FrameLayout
android:id="@+id/micContainer"
android:layout_width="125dp"
android:layout_height="125dp"
android:layout_marginBottom="8dp"
app:layout_constraintBottom_toTopOf="@id/tvInstruction"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<!-- Bottom Voice Section (Hidden when soft keyboard opens) -->
<LinearLayout
android:id="@+id/bottomVoiceSection"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent">
<!-- Outer Pulsing Glow -->
<View
android:id="@+id/viewGlow"
android:layout_width="125dp"
android:layout_height="125dp"
android:layout_gravity="center"
android:alpha="0.3"
android:background="@drawable/bg_mic_button" />
<!-- Main Touch Button -->
<!-- Center Glowing Mic Button Container -->
<FrameLayout
android:id="@+id/btnMic"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_gravity="center"
android:background="@drawable/bg_mic_button"
android:clickable="true"
android:focusable="true">
<ImageView
android:id="@+id/ivMicIcon"
android:layout_width="42dp"
android:layout_height="42dp"
android:id="@+id/micContainer"
android:layout_width="120dp"
android:layout_height="120dp"
android:layout_marginBottom="6dp">
<!-- Outer Pulsing Glow -->
<View
android:id="@+id/viewGlow"
android:layout_width="120dp"
android:layout_height="120dp"
android:layout_gravity="center"
android:src="@drawable/ic_mic" />
android:alpha="0.3"
android:background="@drawable/bg_mic_button" />
<!-- Main Touch Button -->
<FrameLayout
android:id="@+id/btnMic"
android:layout_width="95dp"
android:layout_height="95dp"
android:layout_gravity="center"
android:background="@drawable/bg_mic_button"
android:clickable="true"
android:focusable="true">
<ImageView
android:id="@+id/ivMicIcon"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_gravity="center"
android:src="@drawable/ic_mic" />
</FrameLayout>
</FrameLayout>
</FrameLayout>
<!-- Bottom Instruction / Status Label -->
<TextView
android:id="@+id/tvInstruction"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:text="@string/hold_to_speak"
android:textColor="@color/text_secondary"
android:textSize="13sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<!-- Bottom Instruction / Status Label -->
<TextView
android:id="@+id/tvInstruction"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:text="@string/hold_to_speak"
android:textColor="@color/text_secondary"
android:textSize="13sp"
android:textStyle="bold" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
Loading…
Cancel
Save