@@ -22,6 +22,7 @@ import android.Manifest
2222import android.animation.LayoutTransition
2323import android.app.Activity
2424import android.app.DatePickerDialog
25+ import android.app.Dialog
2526import android.app.TimePickerDialog
2627import android.content.ActivityNotFoundException
2728import android.content.ContentValues
@@ -36,8 +37,6 @@ import android.provider.ContactsContract
3637import android.provider.MediaStore
3738import android.speech.RecognizerIntent
3839import android.speech.SpeechRecognizer
39- import android.text.Editable
40- import android.text.TextWatcher
4140import android.text.format.DateFormat
4241import android.view.ContextMenu
4342import android.view.DragEvent.ACTION_DRAG_ENDED
@@ -46,9 +45,10 @@ import android.view.DragEvent.ACTION_DROP
4645import android.view.Gravity
4746import android.view.Menu
4847import android.view.MenuItem
48+ import android.view.MotionEvent
4949import android.view.View
50- import android.view.WindowManager
51- import android.widget.EditText
50+ import android.view.ViewGroup
51+ import android.widget.FrameLayout
5252import android.widget.LinearLayout
5353import android.widget.PopupWindow
5454import android.widget.SeekBar
@@ -58,7 +58,9 @@ import androidx.appcompat.app.AlertDialog
5858import androidx.constraintlayout.widget.ConstraintSet
5959import androidx.core.app.ActivityCompat
6060import androidx.core.content.ContextCompat
61+ import androidx.core.view.WindowInsetsControllerCompat
6162import androidx.core.view.isVisible
63+ import androidx.emoji2.emojipicker.EmojiPickerView
6264import androidx.lifecycle.ViewModelProvider
6365import androidx.lifecycle.ViewModelProviders
6466import com.google.android.flexbox.FlexboxLayout
@@ -101,7 +103,6 @@ import io.reactivex.schedulers.Schedulers
101103import dev.octoshrimpy.quik.databinding.ComposeActivityBinding
102104import io.reactivex.subjects.PublishSubject
103105import io.reactivex.subjects.Subject
104- import java.text.BreakIterator
105106import java.text.SimpleDateFormat
106107import java.util.Calendar
107108import java.util.Date
@@ -873,46 +874,67 @@ class ComposeActivity : QkThemedActivity(), ComposeView {
873874 }
874875
875876 private fun promptForCustomEmoji (messageId : Long ) {
876- val input = EditText (this ).apply {
877- hint = getString(R .string.compose_reaction_emoji_hint)
878- setSingleLine()
877+ // Show a full emoji grid picker (categorised, tracks its own recents) rather than relying on
878+ // the system keyboard's emoji tab, which can't be opened programmatically. EmojiPickerView
879+ // needs a Material3 theme for its colours, and a plain fixed-height dialog (rather than a
880+ // bottom sheet) gives its internal grid an unambiguous bounded viewport so it scrolls.
881+ val dialog = Dialog (this , R .style.ReactionEmojiPickerTheme )
882+ val drawerHeight = (resources.displayMetrics.heightPixels * 0.7 ).toInt()
883+
884+ val picker = EmojiPickerView (dialog.context).apply {
885+ // opaque surface background since the window itself is transparent
886+ setBackgroundColor(dialog.context.resolveThemeColor(com.google.android.material.R .attr.colorSurface))
887+ setOnEmojiPickedListener { item ->
888+ reactionSelectedIntent.onNext(messageId to item.emoji)
889+ rememberRecentEmoji(item.emoji)
890+ dialog.dismiss()
891+ }
879892 }
880893
881- val dialog = AlertDialog .Builder (this )
882- .setTitle(R .string.compose_reaction_custom_title)
883- .setView(input)
884- .setNegativeButton(R .string.button_cancel, null )
885- .create()
886-
887- // pop the keyboard automatically so the user can type an emoji straight away
888- dialog.window?.setSoftInputMode(WindowManager .LayoutParams .SOFT_INPUT_STATE_ALWAYS_VISIBLE )
889-
890- // submit automatically as soon as an emoji is typed, then dismiss
891- input.addTextChangedListener(object : TextWatcher {
892- override fun beforeTextChanged (s : CharSequence? , start : Int , count : Int , after : Int ) {}
893- override fun onTextChanged (s : CharSequence? , start : Int , before : Int , count : Int ) {}
894- override fun afterTextChanged (s : Editable ? ) {
895- val emoji = firstEmoji(s?.toString() ? : " " ) ? : return
896- input.removeTextChangedListener(this )
897- reactionSelectedIntent.onNext(messageId to emoji)
898- rememberRecentEmoji(emoji)
899- dialog.dismiss()
894+ // Full-screen root: a dim scrim with the drawer pinned to the bottom. A gesture that begins
895+ // above the drawer is captured here (so it dismisses even if it travels down into the
896+ // drawer), while a gesture that begins inside the drawer falls through to the grid so it
897+ // scrolls.
898+ val root = object : FrameLayout (dialog.context) {
899+ override fun onInterceptTouchEvent (ev : MotionEvent ): Boolean =
900+ ev.actionMasked == MotionEvent .ACTION_DOWN && ev.y < picker.top
901+
902+ override fun onTouchEvent (ev : MotionEvent ): Boolean {
903+ if (ev.actionMasked == MotionEvent .ACTION_UP ) dialog.dismiss()
904+ return true
900905 }
901- })
906+ }.apply {
907+ setBackgroundColor(0x80000000 .toInt())
908+ addView(
909+ picker,
910+ FrameLayout .LayoutParams (
911+ FrameLayout .LayoutParams .MATCH_PARENT , drawerHeight, Gravity .BOTTOM
912+ )
913+ )
914+ }
902915
916+ dialog.setContentView(
917+ root,
918+ ViewGroup .LayoutParams (
919+ ViewGroup .LayoutParams .MATCH_PARENT ,
920+ ViewGroup .LayoutParams .MATCH_PARENT
921+ )
922+ )
923+ dialog.window?.let { dialogWindow ->
924+ dialogWindow.setLayout(
925+ ViewGroup .LayoutParams .MATCH_PARENT ,
926+ ViewGroup .LayoutParams .MATCH_PARENT
927+ )
928+ // The full-screen dialog window would otherwise recolour the system bars; copy the
929+ // activity's bar colours and light/dark icon appearance so they look unchanged.
930+ dialogWindow.statusBarColor = window.statusBarColor
931+ dialogWindow.navigationBarColor = window.navigationBarColor
932+ val activityBars = WindowInsetsControllerCompat (window, window.decorView)
933+ val dialogBars = WindowInsetsControllerCompat (dialogWindow, dialogWindow.decorView)
934+ dialogBars.isAppearanceLightStatusBars = activityBars.isAppearanceLightStatusBars
935+ dialogBars.isAppearanceLightNavigationBars = activityBars.isAppearanceLightNavigationBars
936+ }
903937 dialog.show()
904- input.requestFocus()
905- }
906-
907- /* * Returns the first grapheme cluster (which handles multi-codepoint emoji) of [text], or null */
908- private fun firstEmoji (text : String ): String? {
909- val trimmed = text.trim()
910- if (trimmed.isEmpty()) return null
911-
912- val iterator = BreakIterator .getCharacterInstance()
913- iterator.setText(trimmed)
914- val end = iterator.next()
915- return if (end == BreakIterator .DONE ) trimmed else trimmed.substring(0 , end)
916938 }
917939
918940 override fun onCreateOptionsMenu (menu : Menu ? ): Boolean {
0 commit comments