diff --git a/app/src/main/java/jp/juggler/subwaytooter/dialog/EmojiPicker.kt b/app/src/main/java/jp/juggler/subwaytooter/dialog/EmojiPicker.kt index f902edc5..104a348a 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/dialog/EmojiPicker.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/dialog/EmojiPicker.kt @@ -6,10 +6,7 @@ import android.graphics.drawable.PictureDrawable import android.text.Editable import android.text.TextWatcher import android.util.TypedValue -import android.view.Gravity -import android.view.View -import android.view.ViewGroup -import android.view.WindowManager +import android.view.* import android.widget.ImageButton import android.widget.ImageView import androidx.appcompat.app.AppCompatActivity @@ -17,6 +14,7 @@ import androidx.appcompat.widget.AppCompatButton import androidx.appcompat.widget.AppCompatImageView import androidx.appcompat.widget.AppCompatTextView import androidx.core.content.ContextCompat +import androidx.core.view.children import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView.ItemDecoration @@ -35,6 +33,8 @@ import jp.juggler.subwaytooter.util.minHeightCompat import jp.juggler.subwaytooter.util.minWidthCompat import jp.juggler.subwaytooter.view.NetworkEmojiView import jp.juggler.util.* +import kotlin.math.abs +import kotlin.math.sign private class EmojiPicker( private val activity: AppCompatActivity, @@ -68,7 +68,7 @@ private class EmojiPicker( private open class PickerItemCategory( var name: String, - val category: EmojiCategory? = null, + val category: EmojiCategory, ) : PickerItem { val items = ArrayList() @@ -94,9 +94,8 @@ private class EmojiPicker( private class PickerItemCategoryRecent( name: String, - category: EmojiCategory = EmojiCategory.Recent, val accessInfo: SavedAccount?, - ) : PickerItemCategory(name, category) { + ) : PickerItemCategory(name, EmojiCategory.Recent) { private val recentsJsonList: List? get() = try { @@ -356,12 +355,12 @@ private class EmojiPicker( } } - private val views = EmojiPickerDialogBinding.inflate(activity.layoutInflater) - private lateinit var pickerCategries: List private val adapter = GridAdapter() + private val views = EmojiPickerDialogBinding.inflate(activity.layoutInflater) + private val ibSkinTone = listOf( Pair(R.id.btnSkinTone0, 0), Pair(R.id.btnSkinTone1, 0x1F3FB), @@ -393,6 +392,19 @@ private class EmojiPicker( private var bInstanceHasCustomEmoji = false + private var lastSelectedCategory: EmojiCategory? = null + private var lastSelectedKeyword: String? = null + + private var recentCategory: PickerItemCategoryRecent? = null + + private val density = activity.resources.displayMetrics.density + private val cancelY = density * 16f + private val interceptX = density * 8f + private var tracker: VelocityTracker? = null + private var dragging = false + private var startX = 0f + private var startY = 0f + private val textWatcher = object : TextWatcher { override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) = Unit @@ -401,11 +413,6 @@ private class EmojiPicker( } } - private var lastSelectedCategory: EmojiCategory? = null - private var lastSelectedKeyword: String? = null - - private var recentCategory: PickerItemCategoryRecent? = null - private val pickerItemClickListener = View.OnClickListener { val targetEmoji: EmojiBase val targetName: String @@ -552,7 +559,7 @@ private class EmojiPicker( category.createFiltered(keywordLower) .takeIf { it.items.isNotEmpty() } }.forEach { - add(it) + if (selectedCategory == null) add(it) addAll(it.items) val mod = it.items.size % gridCols if (mod > 0) { @@ -562,6 +569,141 @@ private class EmojiPicker( } } } + for (it in views.llCategories.children) { + val backgroundId = when (it.tag) { + selectedCategory -> R.drawable.bg_button_cw + else -> R.drawable.btn_bg_transparent_round6dp + } + it.background = ContextCompat.getDrawable(it.context, backgroundId) + + if (it.tag == selectedCategory) { + val oldScrollX = views.svCategories.scrollX + val visibleWidth = views.svCategories.width + log.i("left=${it.left},r=${it.right},s=$oldScrollX") + when { + oldScrollX > it.left -> + views.svCategories.smoothScrollTo(it.left, 0) + oldScrollX + visibleWidth < it.right -> + views.svCategories.smoothScrollTo(it.right - visibleWidth, 0) + } + } + } + } + + private fun addCategoryButton(category: EmojiCategory) { + val density = activity.resources.displayMetrics.density + val wrapContent = FlexboxLayout.LayoutParams.WRAP_CONTENT + val minWidth = (density * 48f + 0.5f).toInt() + val padTb = (density * 4f + 0.5f).toInt() + val padLr = (density * 6f + 0.5f).toInt() + AppCompatButton(activity).apply { + tag = category + layoutParams = FlexboxLayout.LayoutParams(wrapContent, wrapContent) + background = + ContextCompat.getDrawable(context, R.drawable.btn_bg_transparent_round6dp) + minWidthCompat = minWidth + minHeightCompat = minWidth + setPadding(padLr, padTb, padLr, padTb) + text = activity?.getString(category.titleId) + setOnClickListener { + views.etFilter.removeTextChangedListener(textWatcher) + views.etFilter.setText("") + views.etFilter.addTextChangedListener(textWatcher) + showFiltered(category, null) + } + }.let { views.llCategories.addView(it) } + } + + private fun movePage(delta: Int) { + val categories = buildList { + addAll(pickerCategries.map { it.category }.distinct().sorted()) + } + val newIndex = when ( + val oldIndex = categories.indexOfFirst { it == lastSelectedCategory } + ) { + -1 -> if (delta > 0) 0 else categories.size - 1 + else -> (oldIndex + categories.size + delta) % categories.size + } + val newCategory = categories[newIndex] + showFiltered(newCategory, null) + } + + fun handleTouch(ev: MotionEvent, wasIntercept: Boolean) = when (ev.actionMasked) { + MotionEvent.ACTION_CANCEL -> { + log.i("ACTION_CANCEL wasIntercept=$wasIntercept") + dragging = false + wasIntercept + } + MotionEvent.ACTION_UP -> { + log.i("ACTION_UP wasIntercept=$wasIntercept") + try { + if (dragging) { + tracker?.let { + it.addMovement(ev) + it.computeCurrentVelocity(1000) + val vx = it.xVelocity + val vy = it.yVelocity + val vxDp = vx / density + val aspect = abs(vx) / abs(vy) + log.i("vx=$vx vy=$vy") + if (aspect < 2f) { + log.i("not gesture: aspect=$aspect") + } else if (abs(vxDp) < 40f) { + log.i("not gesture: vxDp=$vxDp") + } else { + movePage((vxDp.sign * -1f).toInt()) + } + } + } + } catch (ex: Throwable) { + log.e(ex) + } + dragging = false + wasIntercept + } + MotionEvent.ACTION_DOWN -> { + log.i("ACTION_DOWN wasIntercept=$wasIntercept") + // ドラッグ開始 + if (!dragging) { + dragging = true + if (tracker == null) { + tracker = VelocityTracker.obtain() + } + tracker?.clear() + startX = ev.x + startY = ev.y + } + wasIntercept + } + MotionEvent.ACTION_MOVE -> { + if (!dragging) { + wasIntercept + } else { + // 移動量追跡 + tracker?.addMovement(ev) + val deltaX = ev.x - startX + val deltaY = ev.y - startY + when { + // すでにインターセプトしている + wasIntercept -> true + // 上下方向に大きく動かしたらそれ以上追跡しない + abs(deltaY) > cancelY -> { + log.i("not intercept!") + dragging = false + false + } + // 横方向に大きく動かしたらインターセプトする + abs(deltaX) > interceptX -> { + log.i("intercept!") + true + } + else -> false + } + } + } + else -> { + log.w("handleTouch $ev") + } } suspend fun start() { @@ -569,47 +711,17 @@ private class EmojiPicker( bInstanceHasCustomEmoji = pickerCategries.any { it.category == EmojiCategory.Custom } - val wrapContent = FlexboxLayout.LayoutParams.WRAP_CONTENT - val density = activity.resources.displayMetrics.density - val minWidth = (density * 48f + 0.5f).toInt() - val padTb = (density * 4f + 0.5f).toInt() - val padLr = (density * 6f + 0.5f).toInt() - arrayOf( - null, - EmojiCategory.Recent, - EmojiCategory.Custom, - EmojiCategory.People, - EmojiCategory.ComplexTones, - EmojiCategory.Nature, - EmojiCategory.Foods, - EmojiCategory.Activities, - EmojiCategory.Places, - EmojiCategory.Objects, - EmojiCategory.Symbols, - EmojiCategory.Flags, - EmojiCategory.Others, - ).forEach { - AppCompatButton(activity).apply { - layoutParams = FlexboxLayout.LayoutParams(wrapContent, wrapContent) - background = - ContextCompat.getDrawable(context, R.drawable.btn_bg_transparent_round6dp) - minWidthCompat = minWidth - minHeightCompat = minWidth - setPadding(padLr, padTb, padLr, padTb) - text = activity?.getString(it?.titleId ?: R.string.all) - setOnClickListener { _ -> - views.etFilter.removeTextChangedListener(textWatcher) - views.etFilter.setText("") - views.etFilter.addTextChangedListener(textWatcher) - showFiltered(it, null) - } - }.let { views.llCategories.addView(it) } + pickerCategries.map { it.category }.distinct().sorted().forEach { + addCategoryButton(it) } views.etFilter.addTextChangedListener(textWatcher) showFiltered(null, null) + views.giGrid.intercept = { handleTouch(it, wasIntercept = false) } + views.giGrid.touch = { handleTouch(it, wasIntercept = true) } + views.rvGrid.adapter = adapter views.rvGrid.layoutManager = GridLayoutManager( activity, @@ -628,13 +740,16 @@ private class EmojiPicker( dialog.setContentView(views.root) dialog.setCancelable(true) dialog.setCanceledOnTouchOutside(true) - val w = dialog.window - // XXX Android 11 で SOFT_INPUT_ADJUST_RESIZE はdeprecatedになった - @Suppress("DEPRECATION") - w?.setSoftInputMode( - WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE or - WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN - ) + dialog.setOnDismissListener { + tracker?.recycle() + } + dialog.window?.let { w -> + @Suppress("DEPRECATION") + w.setSoftInputMode( + WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE or + WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN + ) + } dialog.show() } } diff --git a/app/src/main/java/jp/juggler/subwaytooter/view/GestureInterceptor.kt b/app/src/main/java/jp/juggler/subwaytooter/view/GestureInterceptor.kt new file mode 100644 index 00000000..c5b85b1d --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/view/GestureInterceptor.kt @@ -0,0 +1,25 @@ +package jp.juggler.subwaytooter.view + +import android.content.Context +import android.util.AttributeSet +import android.view.MotionEvent +import android.widget.FrameLayout +import androidx.annotation.AttrRes +import androidx.annotation.StyleRes + +class GestureInterceptor @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + @AttrRes defStyleAttr: Int = 0, + @StyleRes defStyleRes: Int = 0, +) : FrameLayout(context, attrs, defStyleAttr, defStyleRes) { + var intercept: (MotionEvent) -> Boolean = { false } + var touch: (MotionEvent) -> Boolean = { false } + + override fun onInterceptTouchEvent(ev: MotionEvent) = + intercept(ev) + + @Suppress("ClickableViewAccessibility") + override fun onTouchEvent(event: MotionEvent?) = + event?.let { touch(it) } ?: false +} diff --git a/app/src/main/res/layout/emoji_picker_dialog.xml b/app/src/main/res/layout/emoji_picker_dialog.xml index 024833b0..3ce28fa8 100644 --- a/app/src/main/res/layout/emoji_picker_dialog.xml +++ b/app/src/main/res/layout/emoji_picker_dialog.xml @@ -61,14 +61,17 @@ android:inputType="text" /> + android:scrollbars="none" + android:fadingEdge="horizontal" + android:fadingEdgeLength="32dp" + android:cacheColorHint="#00000000" + > - + android:layout_weight="1"> + + +