添付メディア並び替えで、並び替えせずにスクロールしたい場合がある

This commit is contained in:
tateisu 2023-11-26 16:42:25 +09:00
parent acfd4dd9a0
commit 870dc6186d
3 changed files with 107 additions and 61 deletions

View File

@ -5,17 +5,18 @@ import android.app.Dialog
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MotionEvent import android.view.MotionEvent
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import jp.juggler.subwaytooter.ActPost
import jp.juggler.subwaytooter.R import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.databinding.AttachmentRearrangeDialogBinding import jp.juggler.subwaytooter.databinding.AttachmentRearrangeDialogBinding
import jp.juggler.subwaytooter.databinding.AttachmentsRearrangeItemBinding import jp.juggler.subwaytooter.databinding.AttachmentsRearrangeItemBinding
import jp.juggler.subwaytooter.defaultColorIcon import jp.juggler.subwaytooter.defaultColorIcon
import jp.juggler.subwaytooter.util.PostAttachment import jp.juggler.subwaytooter.util.PostAttachment
import jp.juggler.util.data.ellipsizeDot3 import jp.juggler.util.data.ellipsizeDot3
import jp.juggler.util.log.LogCategory
import jp.juggler.util.ui.attrColor import jp.juggler.util.ui.attrColor
import jp.juggler.util.ui.dismissSafe import jp.juggler.util.ui.dismissSafe
import jp.juggler.util.ui.dp import jp.juggler.util.ui.dp
@ -24,56 +25,60 @@ import kotlinx.coroutines.suspendCancellableCoroutine
import org.jetbrains.anko.backgroundColor import org.jetbrains.anko.backgroundColor
import kotlin.coroutines.resumeWithException import kotlin.coroutines.resumeWithException
suspend fun ActPost.dialogArrachmentRearrange( private val log = LogCategory("DlgAttachmentRearrange")
/**
* 投稿画面で添付メディアを並べ替えるダイアログを開きOKボタンが押されるまで非同期待機する
* OK以外の方法で閉じたらCancellationExceptionを投げる
*/
suspend fun AppCompatActivity.dialogArrachmentRearrange(
initialList: List<PostAttachment>, initialList: List<PostAttachment>,
): List<PostAttachment> = suspendCancellableCoroutine { cont -> ): List<PostAttachment> = suspendCancellableCoroutine { cont ->
val views = AttachmentRearrangeDialogBinding.inflate(layoutInflater) val views = AttachmentRearrangeDialogBinding.inflate(layoutInflater)
val dialog = Dialog(this) val dialog = Dialog(this).apply {
dialog.setContentView(views.root) setContentView(views.root)
setOnDismissListener {
if (cont.isActive) cont.resumeWithException(CancellationException())
}
}
cont.invokeOnCancellation { dialog.dismissSafe() } cont.invokeOnCancellation { dialog.dismissSafe() }
dialog.setOnDismissListener { val myAdapter = RearrangeAdapter(layoutInflater, initialList)
if (cont.isActive) cont.resumeWithException(CancellationException())
}
val rearrangeAdapter = RearrangeAdapter(layoutInflater, initialList)
views.btnCancel.setOnClickListener { views.btnCancel.setOnClickListener {
dialog.dismissSafe() dialog.dismissSafe()
} }
views.btnOk.setOnClickListener { views.btnOk.setOnClickListener {
if (cont.isActive) { if (cont.isActive) cont.resume(myAdapter.list) {}
cont.resume(rearrangeAdapter.list) {}
}
dialog.dismissSafe() dialog.dismissSafe()
} }
views.listView.apply { views.listView.apply {
layoutManager = LinearLayoutManager(context) layoutManager = LinearLayoutManager(context)
adapter = rearrangeAdapter adapter = myAdapter
rearrangeAdapter.itemTouchHelper.attachToRecyclerView(this) myAdapter.itemTouchHelper.attachToRecyclerView(this)
} }
dialog.window?.setLayout(dp(300), dp(440)) dialog.window?.setLayout(dp(300), dp(440))
dialog.show() dialog.show()
} }
/**
* 並べ替えダイアログ内部のRecyclerViewに使うAdapter
*/
private class RearrangeAdapter( private class RearrangeAdapter(
private val inflater: LayoutInflater, private val inflater: LayoutInflater,
initialList: List<PostAttachment>, initialList: List<PostAttachment>,
) : RecyclerView.Adapter<RearrangeAdapter.MyViewHolder>(), ) : RecyclerView.Adapter<RearrangeAdapter.MyViewHolder>(), MyDragCallback.Changer {
MyDragCallback.Changer {
val list = ArrayList(initialList) val list = ArrayList(initialList)
private var lastStateViewHolder: MyViewHolder? = null private var lastStateViewHolder: MyViewHolder? = null
private var draggingItem: PostAttachment? = null private var draggingItem: PostAttachment? = null
val itemTouchHelper by lazy { val itemTouchHelper = ItemTouchHelper(MyDragCallback(this))
ItemTouchHelper(MyDragCallback(this))
}
override fun getItemCount() = list.size override fun getItemCount() = list.size
@ -84,6 +89,7 @@ private class RearrangeAdapter(
holder.bind(list.elementAtOrNull(position)) holder.bind(list.elementAtOrNull(position))
} }
// implements MyDragCallback.Changer
override fun onMove(posFrom: Int, posTo: Int): Boolean { override fun onMove(posFrom: Int, posTo: Int): Boolean {
val item = list.removeAt(posFrom) val item = list.removeAt(posFrom)
list.add(posTo, item) list.add(posTo, item)
@ -91,18 +97,32 @@ private class RearrangeAdapter(
return true return true
} }
override fun onState(viewHolder: RecyclerView.ViewHolder?, actionState: Int) { // implements MyDragCallback.Changer
override fun onState(
caller: String,
viewHolder: RecyclerView.ViewHolder?,
actionState: Int,
) {
log.d("onState: caller=$caller, viewHolder=$viewHolder, actionState=$actionState")
val holder = (viewHolder as? MyViewHolder) val holder = (viewHolder as? MyViewHolder)
// 最後にドラッグ対象となったViewHolderを覚えておく
holder?.let { lastStateViewHolder = it } holder?.let { lastStateViewHolder = it }
// 現在ドラッグ対象のPostAttachmentを覚えておく
val pa = holder?.lastItem val pa = holder?.lastItem
draggingItem = when { draggingItem = when {
pa != null && actionState == ItemTouchHelper.ACTION_STATE_DRAG -> pa pa != null && actionState == ItemTouchHelper.ACTION_STATE_DRAG -> pa
else -> null else -> null
} }
// 表示の更新
holder?.bind() holder?.bind()
lastStateViewHolder?.takeIf { it != holder }?.bind() lastStateViewHolder?.takeIf { it != holder }?.bind()
} }
private val iconPlaceHolder = defaultColorIcon(inflater.context, R.drawable.ic_hourglass)
private val iconError = defaultColorIcon(inflater.context, R.drawable.ic_error)
private val iconFallback = defaultColorIcon(inflater.context, R.drawable.ic_clip)
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
inner class MyViewHolder( inner class MyViewHolder(
parent: ViewGroup, parent: ViewGroup,
@ -113,6 +133,7 @@ private class RearrangeAdapter(
var lastItem: PostAttachment? = null var lastItem: PostAttachment? = null
init { init {
// リスト項目のタッチですぐにドラッグを開始する
views.root.setOnTouchListener { _, event -> views.root.setOnTouchListener { _, event ->
if (event.actionMasked == MotionEvent.ACTION_DOWN) { if (event.actionMasked == MotionEvent.ACTION_DOWN) {
itemTouchHelper.startDrag(this) itemTouchHelper.startDrag(this)
@ -121,59 +142,71 @@ private class RearrangeAdapter(
} }
} }
fun bind(pa: PostAttachment? = lastItem) { fun bind(item: PostAttachment? = lastItem) {
pa ?: return item ?: return
lastItem = pa lastItem = item
val context = views.root.context val context = views.root.context
// ドラッグ中は背景色を変える
views.root.apply { views.root.apply {
when { when {
draggingItem === pa -> backgroundColor = draggingItem === item -> backgroundColor =
context.attrColor(R.attr.colorSearchFormBackground) context.attrColor(R.attr.colorSearchFormBackground)
else -> background = null else -> background = null
} }
} }
// サムネイルのロード開始
views.ivThumbnail.apply { views.ivThumbnail.apply {
val imageUrl = pa.attachment?.preview_url when (val imageUrl = item.attachment?.preview_url) {
if (imageUrl.isNullOrEmpty()) { null, "" -> {
val imageId = when (pa.status) { val iconDrawable = when (item.status) {
PostAttachment.Status.Progress -> R.drawable.ic_upload PostAttachment.Status.Progress -> iconPlaceHolder
PostAttachment.Status.Error -> R.drawable.ic_error PostAttachment.Status.Error -> iconError
else -> R.drawable.ic_clip else -> iconFallback
}
Glide.with(context).clear(this)
setImageDrawable(iconDrawable)
}
else -> {
Glide.with(context)
.load(imageUrl)
.placeholder(iconPlaceHolder)
.error(iconError)
.fallback(iconFallback)
.into(this)
} }
Glide.with(context).clear(this)
setImageDrawable(defaultColorIcon(context, imageId))
} else {
Glide.with(context)
.load(imageUrl)
.placeholder(defaultColorIcon(context, R.drawable.ic_hourglass))
.error(defaultColorIcon(context, R.drawable.ic_error))
.fallback(defaultColorIcon(context, R.drawable.ic_clip))
.into(this)
} }
} }
views.tvText.text = pa.attachment?.run {
"$type ${description?.ellipsizeDot3(40) ?: ""}" // テキストの表示
} ?: context.getString(R.string.attachment_uploading) views.tvText.text = item.attachment?.run {
"${type.id} ${description?.ellipsizeDot3(40) ?: ""}"
} ?: ""
} }
} }
} }
/**
* RectclerViewのDrag&Drop操作に関するコールバック
*/
private class MyDragCallback( private class MyDragCallback(
private val changer: Changer, private val changer: Changer,
) : ItemTouchHelper.SimpleCallback( ) : ItemTouchHelper.SimpleCallback(
ItemTouchHelper.UP or ItemTouchHelper.DOWN, ItemTouchHelper.UP or ItemTouchHelper.DOWN,
0 // no swipe 0 // no swipe
) { ) {
// アダプタに行わせたい処理のinterface
interface Changer { interface Changer {
fun onMove(posFrom: Int, posTo: Int): Boolean fun onMove(posFrom: Int, posTo: Int): Boolean
fun onState(viewHolder: RecyclerView.ViewHolder?, actionState: Int) fun onState(caller: String, viewHolder: RecyclerView.ViewHolder?, actionState: Int)
} }
override fun isLongPressDragEnabled() = false override fun isLongPressDragEnabled() = false
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) = Unit override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) = Unit
override fun onMove( override fun onMove(
@ -192,7 +225,7 @@ private class MyDragCallback(
actionState: Int, actionState: Int,
) { ) {
super.onSelectedChanged(viewHolder, actionState) super.onSelectedChanged(viewHolder, actionState)
changer.onState(viewHolder, actionState) changer.onState("onSelectedChanged", viewHolder, actionState)
} }
override fun clearView( override fun clearView(
@ -200,6 +233,6 @@ private class MyDragCallback(
viewHolder: RecyclerView.ViewHolder, viewHolder: RecyclerView.ViewHolder,
) { ) {
super.clearView(recyclerView, viewHolder) super.clearView(recyclerView, viewHolder)
changer.onState(null, ItemTouchHelper.ACTION_STATE_IDLE) changer.onState("clearView", viewHolder, ItemTouchHelper.ACTION_STATE_IDLE)
} }
} }

View File

@ -163,7 +163,7 @@
<ImageButton <ImageButton
android:id="@+id/btnAttachmentsRearrange" android:id="@+id/btnAttachmentsRearrange"
android:layout_width="40dp" android:layout_width="40dp"
android:layout_height="40dp" android:layout_height="48dp"
android:layout_marginEnd="4dp" android:layout_marginEnd="4dp"
android:background="@drawable/btn_bg_transparent_round6dp" android:background="@drawable/btn_bg_transparent_round6dp"
android:contentDescription="@string/rearrange" android:contentDescription="@string/rearrange"

View File

@ -9,47 +9,60 @@
<TextView <TextView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textColor="?attr/colorTextContent"
android:padding="8dp" android:padding="8dp"
android:gravity="center"
android:includeFontPadding="false"
android:text="@string/attachment_rearrange_desc" android:text="@string/attachment_rearrange_desc"
/> android:textColor="?attr/colorTextContent" />
<!--
並べ替えのRecyclerView
ドラッグせずにスクロールするためのタッチ領域を確保したいので、右端のpaddingが大きい
-->
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/listView" android:id="@+id/listView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_weight="1" android:layout_weight="1"
android:padding="8dp"
android:clipToPadding="false" android:clipToPadding="false"
/> android:fadeScrollbars="false"
android:paddingEnd="48dp"
android:paddingStart="8dp"
android:paddingVertical="8dp"
android:scrollbarStyle="outsideOverlay"
android:scrollbars="vertical" />
<View <View
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="1dp" android:layout_height="1dp"
android:background="?attr/colorSettingDivider"/> android:background="?attr/colorSettingDivider" />
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" android:orientation="horizontal">
>
<Button <Button
android:background="@drawable/btn_bg_transparent_round6dp" android:id="@+id/btnCancel"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_weight="1"
android:id="@+id/btnCancel" android:background="@drawable/btn_bg_transparent_round6dp"
android:text="@string/cancel"/> android:textColor="?attr/colorTextContent"
android:text="@string/cancel" />
<View <View
android:layout_width="1dp" android:layout_width="1dp"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="?attr/colorSettingDivider"/> android:background="?attr/colorSettingDivider" />
<Button <Button
android:background="@drawable/btn_bg_transparent_round6dp" android:id="@+id/btnOk"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_weight="1"
android:id="@+id/btnOk" android:background="@drawable/btn_bg_transparent_round6dp"
android:text="@string/ok"/> android:textColor="?attr/colorTextContent"
android:text="@string/ok" />
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>