絵文字ピッカーで左右フリックのジェスチャーを受け付ける

This commit is contained in:
tateisu 2022-06-25 15:32:07 +09:00
parent 7e265ba2ae
commit 4e109c6e7d
3 changed files with 218 additions and 68 deletions

View File

@ -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<PickerItem>()
@ -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<JsonObject>?
get() = try {
@ -356,12 +355,12 @@ private class EmojiPicker(
}
}
private val views = EmojiPickerDialogBinding.inflate(activity.layoutInflater)
private lateinit var pickerCategries: List<PickerItemCategory>
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()
}
}

View File

@ -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
}

View File

@ -61,14 +61,17 @@
android:inputType="text" />
<HorizontalScrollView
android:id="@+id/svCategories"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:fadeScrollbars="false"
android:padding="6dp"
android:paddingBottom="0dp"
android:scrollbarStyle="outsideOverlay"
android:scrollbars="horizontal">
android:scrollbars="none"
android:fadingEdge="horizontal"
android:fadingEdgeLength="32dp"
android:cacheColorHint="#00000000"
>
<LinearLayout
android:id="@+id/llCategories"
@ -77,14 +80,21 @@
android:orientation="horizontal" />
</HorizontalScrollView>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvGrid"
<jp.juggler.subwaytooter.view.GestureInterceptor
android:id="@+id/giGrid"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:clipToPadding="false"
android:fadeScrollbars="false"
android:padding="6dp"
android:scrollbarStyle="outsideOverlay"
android:scrollbars="vertical" />
android:layout_weight="1">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvGrid"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:fadeScrollbars="false"
android:padding="6dp"
android:scrollbarStyle="outsideOverlay"
android:scrollbars="vertical" />
</jp.juggler.subwaytooter.view.GestureInterceptor>
</LinearLayout>